Files
ironclaw/FEATURE_PARITY.md
Benjamin Kurrek 88fe2d01e9 refactor(channels): normalize ingress and split reply from delivery (#7477)
* fix(webui): only offer the web-app notification channel when a browser is enrolled

The "Web app" row in the notification-channels picker rendered READY with a
selectable checkbox even with zero enrolled browsers (no push subscription),
so a user could pick a channel that has nowhere to deliver. Selectability and
the pill now follow the account's enrollment count: with no enrolled browser
the web-app checkbox cannot be SELECTED and its pill drops from Ready to
Unavailable, while the nested "Enable notifications in this browser" affordance
shows how to fix it. An already-stored selection stays deselectable (disabled
only when unchecked), so a browser that unsubscribes never leaves a locked-on
checkbox.

The web-push row now owns its device hook (WebPushChannelRow) so the account
status query still mounts only when the row is present; the shared row label
was extracted (renderChannelRowLabel) so every other channel renders its
checkbox inline, unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* wip(ingress): declare authenticated-session ingress recipe

Add IngressVerificationRecipe::AuthenticatedSession — the trust class for a
channel whose caller the host's authenticated transport (T1) already verified,
so it needs no webhook signature. Handle it fail-closed in the two webhook host
sites: no evidence mint in channel_host, and a NotWebhookVerifiable rejection in
the ingress verifier (a session channel mounts no webhook route and must never
be attested verified-inbound through the webhook path).

Incremental checkpoint toward the generic-inbound pipeline (PR2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ingress): make channel route_suffix optional, paired with trust class

A channel's ingress mount now depends on its trust class. Webhook recipes (T2:
hmac/shared-secret/none) mount /webhooks/extensions/{id}/{suffix} and MUST
declare a route_suffix; an authenticated_session recipe (T1) is verified upstream
by the host transport, mounts no webhook route, and MUST NOT declare one.

- route_suffix becomes Option<RouteSuffix> (serde default+skip; existing webhook
  manifests parse unchanged into Some).
- ChannelDescriptor::validate pairs the recipe kind with route_suffix presence,
  fail-closed both ways (SessionIngressWithRouteSuffix / WebhookIngressWithoutRouteSuffix).
- Every mount/route-table consumer (active snapshot build + resolve + conflict,
  deployment channels resolve, lifecycle reserved-route check) fails closed when
  a session channel carries no route_suffix — it can never match a webhook route.

Groundwork for routing the web app's authenticated session through the one
generic inbound pipeline (PR2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(design): materialize the unified channel model (target architecture)

Every channel (web-app, Slack, Telegram) becomes one ChannelAdapter that
implements inbound + outbound/reply + notifications the same way. The only
per-channel variation is the declared ENTRYPOINT (webhook / api_key /
authenticated-session POST) and declared DELIVERY capabilities (reply mode
streaming|batched, optional max_message_chars, markdown, threads). Everything
between entrypoint and delivery is one abstract, channel-agnostic core:
idempotency -> bind(OwnedThread|ExternalRef) -> submit_turn -> durable reply
events -> per-mode reply sink.

Removes the current smell (two post-ingress cores; the web-app special-cased on
both inbound and reply). Records the migration deltas, the trust/security
invariants, and what stays on ProductSurface (the web-app's rich non-messaging
client API). Authoritative target for future agents touching channel code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(design): no channel-specific code — generic notification setup, kill web-push routes

Strengthen the unified channel model per direction: the hard invariant is that
NOTHING in the codebase is specific to a given channel for inbound, outbound, or
notifications. Every route is generic and extension_id-parameterized; channel
behavior lives only in the adapter (packages/*).

- Web-push enrollment becomes GENERIC channel notification setup: a channel
  declares notifications_require_setup; a generic status/enable/disable surface
  (by extension_id) dispatches to the adapter. VAPID/endpoints/subscription store
  move behind the web-app adapter.
- Delete /web-push/{subscribe,unsubscribe,status} and the web-app-specific
  message route; replace with generic session-inbound + notification-setup routes.
- Extend the specificity gate: zero channel names / channel-specific routes in
  generic crates. Rename web-push -> web-app (id/routes/constants) is in-scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(design): notification send is a generic facade over ChannelAdapter::deliver_notification

Channels implement their notification logic in the adapter
(ChannelAdapter::deliver_notification); the DeliveryCoordinator is the generic,
any-caller facade that dispatches to it by extension_id. Routines are one caller
among several (the model's outbound_deliver already is another) — callers own
WHEN/WHAT, never HOW or which channel. Delivery is already adapter-based, so this
is exposing the facade + adapter method, not a rebuild. Setup stays a separate
generic surface (7b). Migration renumbered 8-11 accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(inbound): trust-class + binding enums on the channel inbound contract

The unified-channel-model inbound vocabulary (§12.1 of
docs/internal/design/2026-08-10-unified-channel-model.md):

- ChannelInboundSurfaceRequest carries a trust-class enum
  (VerifiedInbound { evidence } | SessionCaller { caller }) and a binding
  enum (ExternalRef | OwnedThread { thread_id }) instead of bare webhook
  evidence, plus the session transports' requested_model hint.
- ProductInboundEnvelope carries the same pair (ProductInboundTrust /
  ProductInboundBindingDirective); auth_claim() is now Option, with
  require_verified_auth_claim() failing closed for session envelopes on
  every external-ref path (binding requests, command context, projection
  subjects).
- TrustedInboundContext::from_session_caller mints the session-arm context;
  the webhook constructors are unchanged in behavior.
- ProductInboundAck::Accepted gains optional submit-time metadata
  (AcceptedTurnSubmission) and the busy variants gain an optional
  BusyRunSnapshot, both serde-defaulted so ledger rows settled before this
  change still deserialize (pinned by ack_rows_without_submit_metadata_
  still_deserialize).
- ChannelInboundProductSurface gains a default-fail-closed inline-attachment
  admission door for session transports.
- ProductSurfaceRejectionKind gains DuplicateAction and ReplayUnavailable
  for the session-lane replay taxonomy; every exhaustive matcher classifies
  them explicitly.

Mechanical fallout: constructors updated across extension_host, openai_compat,
composition and the integration/parity harnesses; no behavior change on the
webhook lane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* feat(inbound): owned-thread session lane inside the one inbound core

The webhook core's InboundTurnService gains the session lane (§12.2): the
envelope's binding directive selects the arm, and everything below
TurnCoordinator::submit_turn stays shared.

- OwnedThread prepare: the authenticated caller is the binding authority —
  ownership-probed through SessionThreadService (missing and foreign threads
  are indistinguishable, no existence oracle), never created implicitly, and
  the external binding resolver never runs.
- Session replay probes the exact persisted browser binding-id schemes
  (caller-scoped primary + thread-scoped legacy) so messages accepted by
  earlier builds replay instead of double-accepting; a client action id
  replayed against a different thread fails as ClientActionReplayMismatch.
- The submit tail is lane-parameterized: webui-src/webui-reply ref prefixes,
  the raw client action id as the coordinator idempotency key, and the WebUi
  product context are preserved byte-for-byte for session turns; webhook
  submissions are unchanged.
- Fresh submissions carry AcceptedTurnSubmission metadata; busy outcomes
  carry the blocking-run snapshot; session busy replays report no run
  metadata (the dedicated browser path's exact shape).
- Session skill-activation hooks record between acceptance and submission
  and clear on busy/error, matching the browser path's ordering.
- New session-lane failures (OwnedThreadUnavailable 404,
  ClientActionReplayMismatch 409/duplicate, ReplayUnavailable 409,
  SkillActivationFailed internal, AttachmentLanderUnavailable 503) never
  settle the idempotency ledger.
- submit_inbound_inner admits only user-message payloads from session
  callers, and build_channel_envelope rejects mixed trust/binding arms fail
  closed: webhook trust/pairing machinery can never run for a browser
  message and vice versa.
- CapacityExceeded submissions now surface non-retryable, matching the
  workflow's own settle decision (turn_error_is_retryable).

Covered by the new session_lane suite in inbound_turn_contract (ownership
probe and cross-thread guards sabotage-verified) plus the serde-compat pins
from the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* refactor(inbound): route browser + OpenAI-compat submit_turn through the one core

RebornServices::submit_turn (the SUBMIT_TURN_COMMAND implementation both the
browser route and the OpenAI-compatible transport invoke) now builds the
neutral session inbound request and admits it through the same
DefaultProductSurface core webhook channels ride — durable idempotency
ledger → owned-thread binding → TurnCoordinator::submit_turn (§12.2–3) —
then renders the acks back into the unchanged RebornSubmitTurnResponse wire
shape (fresh Submitted from submit-time metadata; replays as
AlreadySubmitted with the run's current state; busy shapes with their
decision-time snapshots; ledger-replayed busy without run metadata).

The duplicate browser tail is deleted: replay_webui_send_message,
replay_accepted_message, AcceptedWebUiMessage, mark_message_submitted_or_
replay, reconcile_terminal_duplicate, resolve_webui_thread_metadata,
parse_replay_run_id, and the webui binding-id scheme fns now live only as
the session lane of the shared core (the schemes byte-identical, with
legacy replay fallback). The reborn_services module-charter map is updated
in the same change.

Composition wires the durable session ledger
(build_session_inbound_ledger over the extension filesystem, mirroring the
per-extension channel ledgers' mount/bounds/CAS discipline) into every
product-surface instance; standalone/test builds keep the in-memory
default. SessionLaneRejectingBindingResolver guards the session core's
external-ref door fail closed.

The full reborn_services_contract suite (278 tests) passes unchanged
through the re-plumbed path — caller-owns-thread, no implicit thread
creation, client_action_id replay (including legacy binding-id rows),
cross-thread reuse rejection, busy/deferred/steering shapes, attachment
landing, and skill-activation ordering all preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* feat(ingress): generic session-inbound route keyed by extension_id

The web-app-specific browser message route is deleted and replaced by the
generic session-inbound door (§12.4, §8):

- POST /api/webchat/v2/channels/{extension_id}/messages replaces
  POST /api/webchat/v2/threads/{thread_id}/messages. No route names a
  channel; the path extension_id overrides the body and the thread rides the
  body (the caller owns it). Route descriptor policy is unchanged
  (14 MiB body, 60/60s per-caller, TurnCoordinator effect path).
- ProductSubmitTurnRequest/SendMessage carry the optional extension_id; the
  product surface validates it against the new
  SessionChannelDirectory port (declared in
  ironclaw_product_contracts::session_ingress, implemented by the extension
  host over the deployment channel registry — manifest-derived, install-state
  free). Unknown or non-session extensions are 404, indistinguishable from an
  absent route; a missing directory fails closed as 503. Transports that
  predate the parameter (OpenAI-compat) submit under the legacy session
  surface identity, unchanged.
- The web-app manifest declares its entrypoint: inbound = true with the
  authenticated_session verification recipe, no route_suffix (a browser
  request can never reach the webhook mount), conversation_model isolated.
  The manifest-lockstep pin now asserts exactly that.
- The deployment's session channel is advertised to the SPA on
  GET /session (session_channel_extension_id, derived from the registry —
  exactly-one resolves, otherwise none and sends fail closed client-side);
  the frontend plugs it into the generic route and carries no channel name.
- e2e harness + raw-route scenarios read the session channel from
  GET /session; Playwright mocks match the generic pattern.

Caller-level coverage: directory-missing 503 / unknown-extension 404 /
declared-channel admit in reborn_services_contract; the session-channel
directory contract in extension_host; route-table, handler, and charter
gates updated in the same change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* feat(reply): channel-declared reply mode — streaming sinks never batch

The reply model's declaration half (§12.5–7): every channel declares how its
reply sink consumes the durable reply-event stream.

- ChannelDescriptor gains reply_mode = streaming | batched (default batched;
  validation pairs streaming with the authenticated-session entrypoint —
  a webhook vendor has no projection stream to consume).
- The web-app manifest declares streaming: the existing SSE/WebSocket
  projection forward IS this channel's reply sink, exactly as it runs today —
  a consumer of durable reply events, never a replacement (the gateway-events
  layering rule). Its max_message_chars is now undeclared: a streaming sink
  never batches or splits, so the channel is unlimited (§6). Slack and
  Telegram declare batched explicitly; their declared bounds are unchanged.
- ResolvedChannelDelivery carries the declared mode from the same
  generation-pinned snapshot read, and the DeliveryCoordinator gates both
  delivery doors: conversation-reply intents for a streaming channel return
  NoDelivery before any attempt is persisted (the projection stream is the
  delivery), while notification-class sends (BackgroundRunNotice,
  ModelDelivery) flow regardless of mode so the notifications capability
  keeps working. Pinned by
  streaming_channel_conversation_reply_skips_batched_delivery and
  streaming_channel_still_receives_notification_class_deliveries.
- max_message_chars stays adapter-enforced at render time (channel-specific
  splitting is adapter behavior by charter); the declaration remains the
  model-facing hint. The batched sink itself never splits for a streaming
  channel by construction.

No behavior change for any existing delivery: no streaming channel receives
conversation-reply deliveries today, so the gate is the fail-closed
materialization of the current structure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* feat(notify): ChannelAdapter::deliver_notification + the generic notify facade

The §7a send half of notification generalization (§12.8):

- ChannelAdapter gains deliver_notification(envelope, egress) — the
  channel-specific notification send, defaulting to the channel's ordinary
  delivery (a conversational channel's notification is a message; a
  notification-only channel's whole delivery IS this send). Only the generic
  DeliveryCoordinator calls it, never feature code.
- The coordinator classifies each policy-lane delivery before the request is
  consumed: a run-notification that is not source-routed (it targets a
  notification channel, not the originating conversation) and is not an
  explicitly routed final answer rides the adapter's notification send;
  everything else rides ordinary delivery. Pinned by
  notification_class_delivery_rides_the_adapters_notification_send /
  conversation_reply_rides_the_adapters_ordinary_delivery. Zero behavior
  change for shipped adapters — all three inherit the delegating default.
- run_delivery::notifications is the named any-caller facade over the
  coordinator: notify(target, content) for one explicit catalog-resolved
  channel target, notify_user(user, content) fanning out over
  resolve_user_notification_targets (the picker set). The routine driver's
  own notification internals now delegate to it — one send path, with the
  routine lane as one caller among any number. Callers own WHEN/WHAT, never
  HOW, and never name a channel.
- The coordinator's streaming-reply gate now reads the new lightweight
  ChannelDeliveryResolver::channel_reply_mode lookup instead of performing a
  second full resolution, preserving the single generation-pinned
  resolve_channel_delivery read the OUT contract pins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DectwBVo9eDqV5dhRDGkb

* feat(notifications): generalize notification setup behind ChannelAdapter (§7b)

Replace the bespoke /web-push/{status,subscriptions,subscriptions/remove}
routes with one generic per-channel surface keyed by extension_id:
GET/POST /api/webchat/v2/channels/{extension_id}/notifications{,/enable,/disable}.

- contracts: web_push descriptor module deleted; notification_setup module
  (status view + enable/disable command descriptors) and the
  RebornNotificationSetup* wire family replace the RebornWebPush* DTOs;
  body extension_id serde-defaulted (route path is canonical).
- assistant: reborn_services/web_push.rs deleted; notification_setup.rs adds
  ChannelNotificationSetupService + fail-closed Unsupported default +
  AdapterChannelNotificationSetupService dispatching to the channel adapter
  via ChannelDeliveryResolver (unknown extension -> 404, no-setup channel ->
  enabled:true + mutation 400, payload/detail byte bounds enforced).
- delivery coordinator: the streaming-reply gate now keys on the ROUTE, not
  the intent — a notification-routed send (RunNotification + non-live-source
  origin) flows to a streaming channel even with a conversation-shaped
  intent; pinned at the contract tier and by the blocked-fire push journey.
- web-push package: adapter implements the three setup operations over the
  slot runtime (scope byte-identical to the retired product service; detail
  carries vapid_public_key/subscription_count/subscriptions with
  endpoint_digest correlation).
- composition: wires AdapterChannelNotificationSetupService over the channel
  delivery resolver; WebPushComposition handle family deleted (the slot
  install inside assemble_web_push is now the single consumer).
- webui: route descriptors/router/handlers swapped to the generic surface;
  CONTRACT.md route table + outbound charter row updated.
- frontend: api.ts gains getNotificationSetupStatus/enable/disable keyed by
  extensionId; web-push.ts -> device-push.ts and useWebPushDevice ->
  useDevicePush re-read the channel-opaque detail; the notification panel's
  device row is matched by the GET /session-advertised session channel id —
  no channel name remains in the frontend; webPush.* i18n keys renamed
  devicePush.* across all 11 locales.
- tests: 5 new setup-dispatch contract tests + streaming-notification
  regression pin; product-api round-trip and delivery journey rewritten onto
  the generic surface; frontend suites updated (1241 pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(channels): rename web-push -> web-app and retire the old spelling (§12.11, §13)

Product identity rename: extension id / channel name / catalog target id are
now 'web-app'; package dir crates/extensions/packages/web-app (crate
ironclaw_web_app_extension); domain crate crates/domains/ironclaw_web_app;
WEB_PUSH_* constants -> WEB_APP_*, WebPush* types -> WebApp*. PROPOSAL §5
tree updated (check-target-tree: 66/66 OK). Space-separated 'Web Push'
protocol prose stays — the protocol keeps its RFC name; the CHANNEL does not.

Persisted coordinates deliberately keep pre-rename bytes, each commented in
place and pinned by the new gate's allowlist:
- secret-store credential handle value 'web_push_vapid' (renaming = VAPID
  rotation = every existing browser subscription breaks cryptographically);
- enrollment document path /web-push/subscriptions.json plus composition's
  /web-push per-user mount alias (the alias resolves to a physical subpath;
  renaming would orphan enrollments);
- binding-ref grammar mints web-app/v1/ and decodes legacy web-push/v1/
  forever (regression test added).
Documented residue, no migration: stored notification-channel selections
carrying the old 'web-push' target id render Unavailable until re-selected
(population ~QA-only; the channel shipped 2026-08-09).

The install-catalog hide for the host's own surface is no longer an id
match: is_builtin_host_surface consults the SessionChannelDirectory (the
manifest-derived authenticated_session fact), failing OPEN on an absent
directory; the production round-trip test covers the hidden-listing behavior
end-to-end.

Enforcement (§13): new architecture gate
reborn_web_push_vocabulary_retired.rs pins web-push/web_push/WebPush/
webPush/WEB_PUSH at zero occurrences across crates/ (frontend sources
included), tests/integration/, and skills/, with an exact-term shrink-only
allowlist over the five persisted-compat files, a stale-sanction check, and
an assertion that the session + notification-setup routes stay
{extension_id}-parameterized. The specificity gate's web-app carve-out doc
records the rename.

E2E journey vocabulary renamed on both the Rust and Python sides
(case ids, test names, delivery-target enum member).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): bump lru 0.18.1 -> 0.18.2 (RUSTSEC double-free advisory)

cargo-deny advisories began failing on every head when the lru advisory
published; 0.18.2 is the fixed release (lru-rs#238).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ci): raise composition arc_dyn ceiling 816 -> 818 (unified channel model)

Two net-new dyn seams wired at assembly, both genuine inversion ports:
the session-inbound lane's SessionChannelDirectory + durable
IdempotencyLedger, and the generic ChannelNotificationSetupService —
offset by the deleted WebPushComposition handle family. Observed on the
merged branch: 833 = 818 + 15 tolerance exactly, no slack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): migrate the last pre-unification callers and re-pin ratcheted ceilings

Everything here is fallout of surfaces this PR deliberately changed:

- smoke + composition webui_v2_e2e: the raw-HTTP browser-send helpers now
  discover the session channel from GET /session and post the generic
  /channels/{extension_id}/messages route (thread_id in the body) — the
  same flow the SPA ships.
- InboundUserMessageDispatch::Accepted is boxed (clippy large_enum_variant:
  the merged InboundTurnOutcome grew past the threshold; a rejection stays
  slim).
- journey coverage: a channel whose ingress verification is
  authenticated_session has no webhook mount — its inbound IS the WebUI
  session route, so it maps onto the webui journey evidence instead of
  demanding a per-channel label.
- attachments no-lander test pins the sharpened
  AttachmentLanderUnavailable variant (503, never settles the idempotency
  reservation) instead of the old generic rejection.
- body-limit contract test pins webui.v2.session_channel_message (14 MiB)
  after the route rename.
- contracts size ceilings re-pinned to measured merged values with
  rationale: extension_contracts 8_157 (AuthenticatedSession trust class,
  reply modes, §7b setup adapter surface), product_contracts 16_119
  (trust/binding enums, SessionChannelDirectory, setup descriptors + wire
  family), host_api 19_003 (doc churn referencing the renamed crate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): keep the catalog target id persisted, migrate e2e callers, pin notice routing

Review triage for #7477 (IronLoop + multi-agent review).

Persisted identity (IronLoop Medium, also flagged by the review): the
catalog target id now keeps its pre-rename `web-push` bytes. The
notification-channel picker stores its selection as target ids in each
user's communication preferences, so this is a persisted per-user identity
exactly like the VAPID handle, the mount alias, and the binding-ref prefix —
the three the PR already kept. Renaming it resolved every stored selection to
Missing and dropped those users from notification fan-out. Applying the PR's
own rule uniformly removes the documented residue rather than shipping it;
the integration test now pins the split (target id `web-push`, channel
`web-app`) so the two can't be conflated again.

E2E callers of the retired send route (the browser-lane CI failure): four
Playwright interceptions and one API helper still targeted
/threads/{id}/messages, so failure injection never fired. All now use the
generic /channels/{extension_id}/messages route, and every mocked GET
/session advertises session_channel_extension_id the way a real deployment
does — the SPA fail-closes without it.

deliver_notice asymmetry (review Medium, correctness): confirmed correct and
now pinned. Notice-class intents are source-routed, so none is ever
notification-routed and `deliver`'s carve-out cannot apply; for a streaming
channel the originating conversation IS the projection stream, and Retract /
React have no counterpart there (the adapter reports both unsupported). The
new test drives all seven notice intents plus the notification path in one
breath so they cannot drift.

Session surface is built once (review Low/Medium, hot path): submit_turn
rebuilt DefaultProductSurface plus ~5 Arc'd services per browser message;
every input is an immutable builder-wired Arc, so it is memoized behind a
OnceLock.

Docs the rename sweep left stale: tests/CLAUDE.md cited a test name that
never existed, the web-app README cited a VAPID handle value that doesn't
exist (the constant deliberately keeps the old value), the extensions
package-inventory row still called the channel outbound-only with no ingress,
and a merge left a duplicated comment block in inbound_turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(review): cover the notify_user fan-out and the web-app adapter's setup errors

Closes the two review findings that were test gaps rather than follow-ups —
the repo's own rule is that production-wired behavior ships with its
caller-level test, and both of these were new production surface with none.

notify_user (crates/product/ironclaw_assistant/tests/run_delivery_contract.rs):
the driver only ever calls the single-target notify, so the fan-out loop was
untested. Two contracts now pinned through the real facade: an unconfigured
user yields an empty result rather than an error, and a target whose channel
no longer resolves surfaces its own Err while its healthy sibling still
delivers (asserted at the adapter, not just the return value).

web-app adapter (crates/extensions/packages/web-app/tests/notification_setup_contract.rs):
the generic service tests drive a scripted adapter, so the real parse →
validate → store path had no error coverage. Six cases: non-JSON document,
missing key material, undecodable base64url keys, an endpoint on an
undeclared push host, a malformed unenrollment document, and every setup
operation with no runtime installed. Each asserts the store was never
touched, so a rejected payload can't leave the browser believing it is
enrolled with no server record behind it. All six passed on first run — the
arms were correct, just unproven.

observer.rs: the repeated fallible-from_envelope fallback is now one
`degradable_binding` helper — but only for the two sites that genuinely
merge 'no request' and 'no binding' into the same degrade. The delivery path
still propagates (a send with no binding is a fault), and the
rejection-hint path still distinguishes them (posted nothing vs handled by
staying silent); both reasons are documented on the helper rather than
flattened away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(channels): close the audit findings — dead per-channel routes, gate gaps, session-surface fallback

Fallout from the four channel-specificity audits, plus two defects those
audits found in MY OWN branch that would have kept CI red.

Introduced by this branch, now fixed:
- a contracts-crate doc comment named Slack/Telegram, an untracked
  specificity-gate violation (the gate strips #[cfg(test)], and this was
  production code)
- deleting the dead telegram frontend modules made five ALLOWLIST rows
  stale; that list is an EQUALITY ratchet (both <= and >=), so the rows are
  removed and the baseline drops 117 -> 112

Pre-existing, found by the audit:
- 871 lines of orphaned telegram-setup frontend code holding the only two
  channel-named URL literals in the SPA, pointing at routes the backend no
  longer serves. Deleted.
- a dead ironclaw_assistant -> ironclaw_web_app dependency edge: a generic
  product crate holding a compile-time edge to one channel's domain crate
  for nothing
- telegram_extension_gates.rs still documented the retired per-channel
  pairing route as live

New gate (§13's structural half): no source file may name a channel in a
/api/webchat/v2/channels/ route. It scans SOURCE rather than the descriptor
table, because the defect it exists to catch lived entirely in callers the
route table never knew about — the table was clean while 871 lines of
channel-named client code sat beside it. Sabotage-tested against the deleted
file, which it flags. Placeholders and test fixtures are exempt (tests may
name channels, per the extension-runtime overview).

Session-surface regression, found by CI on the composition e2e suite:
a deployment that installs no channel extension had NO route to submit a
browser turn, because the old /threads/{id}/messages route is gone and
/session advertised no channel id. That is a supported deployment shape
(assemble_web_app treats its slot as optional), so browser chat must not
depend on an installed extension. BUILTIN_SESSION_SURFACE_ID now lives in
product_contracts::session_ingress; composition advertises it when no channel
claims the surface, the product gate accepts it, and WebuiServeConfig
defaults to it rather than None — the transport defaulting the surface to
'absent' was the actual defect. 15/15 composition e2e tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(internal): channel output has two axes — reply and delivery

Design record for the follow-up train. The unified channel model unified the
pipeline; this reshapes the contract it drives. Recorded here rather than in
the follow-up PR so the decisions survive the conversation that produced them.

The root finding is not the eleven-method ChannelAdapter — that is the
symptom. It is that two independent concepts share one vocabulary:

- reply    = answering the run's input, SOURCE-routed, never without a run
- delivery = reaching someone out-of-band, TARGET-resolved, runs optional

They are orthogonal, not alternatives: one run can stream an answer into an
open tab AND push a notification because the user is not looking. Dispatching
on intent rather than on this axis already produced a real defect on this
branch — a gate prompt is a reply when a human is in the thread and a delivery
when a 3am routine is blocked, and keying the streaming skip on the intent
silently dropped the second case.

Decisions: three manifest sections (ingress/reply/delivery) replacing the
inbound/outbound/notifications booleans; OutboundRoute plus two transport
enums so nonsense combinations are unrepresentable; DeliveryOrigin keeping
model-chosen targets from inheriting user-configured trust; a streaming
delivery returns a projection cursor as evidence instead of NoDelivery,
closing an audit hole where browser replies produce no record at all;
activate/cleanup become an ingress-registration recipe; the attachment fetch
moves AFTER the ack (the durable write currently depends on it, which is what
puts a vendor round-trip on the webhook deadline path); enrollment moves
host-side with no adapter method, keeping one generic pre-storage check that
exists to prevent an SSRF primitive.

Five open questions and a six-step sequencing table are recorded; step one is
the smallest and closes both the no-op and the audit hole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): repair fmt, the last retired-route callers, and the contracts ceiling

Three red checks on 3e8d40bc74, all mechanical:

- cargo fmt: the export-list edit in ironclaw_assistant/src/lib.rs left an
  unformatted line (fmt reflows multi-item use blocks; I removed a symbol
  after the last fmt pass).
- composition webui_v2_serve: two tests still posted to the retired
  /threads/{id}/messages route and got 404. Migrated to the generic
  /channels/{extension_id}/messages with thread_id in the body. One of them
  exists specifically to pin 'the shape api.ts builds', so it has to track
  the SPA; the other pins the 14 MiB descriptor cap against Axum's 2 MiB
  Json default, which is unchanged by the route move.
- product_contracts size ceiling 16_119 -> 16_132: BUILTIN_SESSION_SURFACE_ID
  plus its doc, the built-in session surface that keeps the generic session
  route from depending on an installed channel extension.

Verified: cargo fmt --check clean. The suites are left to CI — another agent
is working in this worktree and a local battery would block it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* wip(channels): reply and delivery become two declared axes

Contract half of the channel-output redesign
(docs/internal/design/2026-08-11-channel-adapter-contract.md §1-§3).
INCOMPLETE — see the PR body / handoff for the stale-reference list.

- ChannelReplyMode -> ReplyTransport{Stream,Message} +
  DeliveryTransport{Push,Message}. Two enums so Stream-for-delivery and
  Push-for-reply are unrepresentable; a third transport joins as a
  variant rather than a reshape (§10.5).
- [channel.reply] and [channel.delivery] manifest sections replace the
  inbound/outbound/notifications/notifications_require_setup booleans
  and reply_mode. Absence of a section means the axis is unsupported,
  so a declaration can no longer say *that* a channel does something
  without saying *how* (§2, §9).
- max_message_chars moves from [channel.presentation] to
  [channel.reply]: a split bound is a property of the reply transport
  and is meaningless for transport = stream.
- ChannelDeliveryResolver::channel_reply_mode ->
  channel_reply_transport; notifications_require_setup ->
  requires_enrollment.

Fixes a live defect found while reshaping, not a rename: the stream
reply/session-ingress pairing check sat inside 'if let Some(ingress)',
so a channel declaring a stream reply with NO ingress validated
silently. The check now sits outside that block and the no-ingress arm
is pinned.

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

* refactor(channels): one derived projection for channel output facts

Replaces the three loose `max_message_chars` scalars an earlier pass in
this branch added beside `channel_presentation` at each carrier.

The bound legitimately moved out of [channel.presentation] into
[channel.reply] (it is a property of the reply transport, and is
meaningless for transport = stream). But pulling it out of a struct that
was ALREADY threaded through three carriers turned 'one type threaded
three times' into 'one type plus a loose scalar threaded three times' —
re-declaring the value at three layers with nothing keeping them in
agreement (.claude/rules/architecture.md §3).

ChannelOutputFacts is the fix: presentation + the reply bound, assembled
once by ChannelDescriptor::output_facts(), threaded exactly where
ChannelPresentation was. One manifest home per field, one projection,
carrier field count unchanged.

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

* wip(channels): three traits + declarative vendor-call recipes

Steps 2, 4, and 6 STARTED, NOT FINISHED — the ~20 consumers of the
removed ChannelAdapter are not yet updated. Does not compile.

- ChannelAdapter's 11 methods -> ChannelIngress::receive (async),
  ChannelReply::send_reply, ChannelDelivery::deliver/list_targets, held
  as ChannelSurfaces { ingress, reply, delivery }. A None is the same
  fact as a missing manifest section. A stream-reply channel implements
  no reply half at all.
- ChannelVendorCallRecipe: per-channel data, generic execution. Replaces
  activate/cleanup as [channel.ingress.registration]/[deregistration]
  and the attachment fetch as [channel.attachments], run post-ack.
- Telegram's setWebhook/deleteWebhook become manifest data; both method
  bodies go to zero.

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

* wip(channels): wire the three-trait split, the two-axis router, and host-owned enrollment

The workspace compiles again. `cd167ab8ed` defined ChannelIngress /
ChannelReply / ChannelDelivery and deleted ChannelAdapter without updating
~60 consumers; this wires them and lands the contract changes those consumers
were waiting on.

Step 6 — the split, with the check that earns it. ChannelSurfaces replaces
`Arc<dyn ChannelAdapter>` on ExtensionBindings, ActiveExtension,
DeploymentChannelBinding, ResolvedChannelDelivery and the composition binding.
`check_binding` now proves each `[channel.*]` section against its implementing
half at activation: webhook ingress <-> ingress half, `transport = "message"`
<-> reply half, `[channel.delivery]` <-> delivery half. Two axes are required
ABSENT and that is the point — a `stream` reply is published by the host and
`authenticated_session` ingress is normalized at the session door, so binding
a half there is dead code that reads as live. Without this check the three
Options would be a second copy of manifest facts with nothing keeping them in
agreement (architecture.md §3); with it, declaration and code cannot disagree
past activation. web-app now binds delivery ONLY.

Step 1 — OutboundRoute. The axis is computed once in DeliveryCoordinator from
the resolved routing decision and threaded through the drive chain in place of
the `as_notification` bool. The streaming gate keys on the route, not on
DeliveryIntent::is_conversation_reply — which is the exact conflation that
silently dropped blocked-routine pushes. A stream reply is no longer a silent
NoDelivery: `record_stream_reply` persists a full attempt row and returns
StreamDelivered { cursor }, so "was the user's answer delivered?" has one
answer and web-app stops being invisible in delivery audits (§4.1, §10.4).
Evidence is the projection ref the turn already wrote — §4.4's verify, not own.

Step 2 — activate/cleanup are gone. `[channel.ingress.registration]` /
`[channel.ingress.deregistration]` are executed generically by
`channel_vendor_calls`: `{handle}` substitution from non-secret config,
unresolved placeholders left for egress credential injection, body_credentials
forwarded by handle, single-pass substitution, JSON keys never templated.
Telegram's two method bodies became zero lines. Their assertions move with the
behavior to the host executor.

Step 5 — enrollment is host-owned. `ironclaw_auth::delivery_registrations`
stores an opaque, size-bounded document keyed (tenant, user, extension) with
the one security-critical check generic and pre-storage: the endpoint must
target a host declared in `[[channel.egress]]`, read from the same resolved
manifest egress policy enforces with. Without it enrollment is an SSRF
primitive. Placement is ironclaw_auth over ironclaw_outbound because the
adapter-facing view must live in extension_contracts and auth already names
it. Registrations ride the envelope and the adapter reports prunes — it holds
no store. A channel with zero registrations is a resolvable "no target" before
any adapter call. Pre-§8 documents migrate forward on read; `/web-push/
subscriptions.json` and its mount alias keep their exact bytes.

Still to do: --all-targets (test doubles, integration suites), step 4's
post-ack attachment fetch, docs, ratchets, PR body.

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

* wip(channels): carry the three-trait split through every test double and fixture

`cargo check --workspace --all-targets` is clean. The prior commit got the
lib and binary compiling; this carries the same change through the test
surface, which is where the behavioural pins live.

- Lifecycle: the three deleted Telegram `activate`/`cleanup` tests are
  re-pinned against the generic recipe executor, verbatim in what they assert
  — the bot token travels as a HANDLE and never as bytes, the shared secret
  rides `body_credentials` so the host inserts its VALUE at the manifest's
  declared pointer, the rendered body carries `url` but never `secret_token`
  nor the handle name, a missing config value and a vendor 5xx both fail
  activation, and deactivation calls deleteWebhook. Adds the arms the adapter
  tests could not reach: deregistration is best-effort and cannot strand a
  deactivation, and a channel declaring no recipes makes no vendor call.
- Binding: per-axis tests drop or add exactly one half against a manifest
  declaring the other two, so a failure names the axis. Plus the two absences
  that are the point — a `stream` reply and `authenticated_session` ingress
  must bind NO half, because the host publishes and the session door
  normalizes.
- web-app: `notification_setup_contract` becomes
  `registration_parsing_contract`, re-aimed at where the behaviour went.
  Endpoint admission and storage bounds are generic now and pinned in
  `ironclaw_auth`; what stays this package's is interpreting the opaque
  document at delivery. New coverage the old shape could not express: one
  unusable registration is pruned WITHOUT costing its siblings their
  notification, because the host owns the list and the adapter no longer
  reads its own store.
- outbound_delivery_contract: the §7b adapter-dispatch block becomes §8
  enrollment coverage. The security-critical arm is explicit — four hostile
  endpoint shapes (undeclared host, http, userinfo smuggling, suffix
  lookalike) are refused BEFORE storage, and the recording store proves
  nothing was written.
- Test doubles across assistant/host/composition/integration bind the halves
  their fixture manifests declare; the Acme fixture gains reply+delivery over
  one shared `send`, as a conversational vendor really behaves.

Reverted in this commit: an in-flight change making `receive` return a
COMPLETE message (attachment bytes + conversation context) so the two fetch
handles could leave the trait entirely. The design is right and is written up
for a fresh pass; landing it 70% done would repeat the breakage this branch
started from.

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

* refactor(channels): return complete inbound messages

Make ChannelIngress::receive the only vendor ingress call and return complete attachments and conversation context through manifest-restricted egress. Delete the host/product late-fetch callbacks while preserving exact byte validation, attachment budgets, policy reconciliation, batch recovery, and ack-after-commit semantics.

Keep Slack URL/history and Telegram two-hop file validation inside their packages. Also carry the selected manifest egress credential into host-owned lifecycle calls so Telegram setWebhook receives its declared token injection; the production libSQL journey covers activation, inbound bytes, dedupe, reply, and refresh.

* docs(channels): align capability contracts and ratchets

Document the ingress/reply/delivery capability split, host-owned session/stream modes, complete receive boundary, and delivery-registration ownership across the contract, package, product, and runtime guides. Amend the design record with the measured pre-ack order and the reasons a declarative attachment recipe cannot model Slack or Telegram safely.

Recapture the measured contract ceilings at extension_contracts 8,594 and loop_contracts 13,307, and the composition budget at 41,751 LOC / 837 Arc<dyn> sites. Widen the retired web-push scanner to Cargo.toml, E2E, and Python while preserving exact persisted-coordinate exceptions.

* fix(cli): warn when session channel is unavailable

Emit an operator-visible serve warning on the documented tracing target when composition resolves no session channel. Capture both the target and message in a regression test so the authenticated session route cannot disappear silently.

* fix(channels): align post-merge contracts

* fix(channels): finish normalized channel boundaries

* fix(channels): harden egress and notification setup

* fix(channels): make stream evidence and wiring explicit

* fix(review): close the verified audit and review findings

Critical — OpenAI-compat lane restored. submit_turn hard-404'd
extension_id: None while both compat workflows send None (the documented
lane: headless SDK clients cannot learn a channel id from GET /session).
Restores the None => BUILTIN_SESSION_SURFACE_ID arm exactly as the wire
doc specifies, keeps every Some strictly directory-gated, and pins the
builtin id as not route-addressable. Two-sided seam pins: the surface
half in reborn_services_contract, the caller half in the compat handlers
contract, plus a drift pin equating the contracts constant with the turn
kernel's WEBUI_SOURCE_CHANNEL.

Delivery reliability:
- Adapter report gate is coverage, not equality: vendor chunking reports
  one outcome per chunk (conformance legalizes >=); requiring == settled
  fully delivered chunked replies Unknown/Failed and invited duplicate
  resends. Under-reporting still settles Unknown, never retried.
- A crash-orphaned Prepared row re-validates and re-authorizes on replay
  (no vendor egress happened; the claim CAS stays the one transition
  authority) instead of wedging AlreadyInFlight forever. A revoked
  replay rejects via a distinct audit row, leaving the stable row for
  the claim fence. Sending-row recovery stays explicitly fail-closed
  per OUT-6; startup wiring needs a status index and is deferred with
  rationale in the PR discussion.
- Working-indicator notice refs are monotonic per run: the stable ref
  made every post-gate re-post settle AlreadyDelivered, so the
  indicator vanished after a gate cycle (and nudge refs reset the same
  way). First-post bytes are preserved.
- Reply-context store failures are logged before mapping to the unit
  port error (both the host source and the coordinator site).
- Partial web-app fan-out reasons carry the failing cause; the push
  status classification matrix (401/403/413/429/5xx/transport/mixed)
  is pinned.

OAuth binding compensation follows the credential: a terminally-failed
lifecycle activation revokes the extension credential, so the identity
binding now rolls back on exactly that arm instead of committing a
"connected with no usable credential" state; retryable dispatch
failures keep the binding (the credential remains valid and the replay
path never re-runs the hook). ContinuationDispatchFailure carries the
terminalization fact to the callback site.

Browser push enrollment un-broken (two-sided wire drift): the client
read the retired flat web-push detail shape while the backend emits
registrations/bootstrap — enroll was permanently dead and enrolled
browsers derived "another account". The client now reads the canonical
shape, project() emits per-registration endpoint_digest (lowercase hex
SHA-256 via ironclaw_common::hashing, matching endpointDigestHex), and
incomplete digest coverage reads correlation-unavailable, never
"not mine". Pinned by vitest parsing tests, the api mock now mirroring
the real shape, and a digest assertion in the integration round trip.

Session-ledger and feedback correctness:
- LlmConfigServiceError::Internal no longer settles a durable permanent
  PolicyDenied: a backend fault is transient, and the same
  client_action_id succeeds after recovery (pinned).
- Duplicate/replay rejections settle silently again instead of
  rendering the false DM-only command copy.
- ProductInboundTrust / ProductInboundBindingDirective persist
  snake_case tags (pinned before the first ledger row ships).
- session_inbound_request and sibling sites use the cause-logging
  internal_from constructor instead of dropping constructor errors.
- Attachment kind classification case-folds MIME at the boundary.
- The persisted webui-src/webui-reply prefixes are defined once.

Test-support honesty: the harness StaticSecretStore stores what
put_if_absent claims to create (and leases remember their handle), so
first-time VAPID bootstrap flows are testable; the SSRF reserved-key
strip in delivery_registrations is pinned; the session-channel catalog
hiding now has a directory-present test; web-app manifest label reads
"Web app".

Refuted with evidence (no change): the outbound record layer is already
CAS insert-if-absent + first-write-wins with the lost-race shape pinned
in outbound_state_store_contract; the WebUI session route needs no
route-level channel check because the product surface enforces the
directory fail-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gates): reword a test comment out of the retired vocabulary scan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): stack headroom for the crate-bucket lane; evidence-driven final-reply pin

The composition-core bucket SIGABRTs on Linux: the composed-runtime skills
turn overflows the 2 MiB default test-thread stack (first seen in
the_model_runs_a_skills_script_from_the_workdir_the_body_advertises).
Give the bucket lane the same 8 MiB headroom the integration lanes document;
deep subtrees stay Box::pin'd — this is headroom, not a substitute.

The webui grouping pin asserted a hardcoded 'isFinalReply: false' literal;
the stream-evidence rework made the marker evidence-driven
(isFinalReply: finalizedText from the durable projection's finalized bit).
Pin the derivation — the same in-flight guarantee, stated against the
stronger shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 18:46:30 +00:00

88 KiB
Raw Blame History

IronClaw ↔ OpenClaw Feature Parity Matrix

This document tracks feature parity between IronClaw (Rust implementation) and OpenClaw (TypeScript reference implementation). Use this to coordinate work across developers.

Legend:

  • Implemented
  • 🚧 Partial (in progress or incomplete)
  • Not implemented
  • 🔮 Planned (in scope but not started)
  • 🚫 Out of scope (intentionally skipped)
  • N/A (not applicable to Rust implementation)

Last reviewed against OpenClaw PRs: 2026-05-02 (merged 2026-03-11 through 2026-04-30, OpenClaw releases 2026.3.11 → 2026.4.30)


1. Architecture

Feature OpenClaw IronClaw Notes
Hub-and-spoke architecture Web gateway as central hub
WebSocket control plane Gateway with WebSocket + SSE
Single-user system Explicit instance owner scope for persistent routines, secrets, jobs, settings, extensions, and workspace memory
Multi-agent routing Workspace isolation per-agent
Session-based messaging Owner scope is separate from sender identity and conversation scope
Loopback-first networking HTTP binds to 0.0.0.0 but can be configured

Owner: Unassigned


2. Gateway System

Feature OpenClaw IronClaw Notes
Gateway control plane Web gateway with 40+ API endpoints
HTTP endpoints for Control UI Web dashboard with chat, memory, jobs, logs, extensions
Channel connection lifecycle ChannelManager + WebSocket tracker
Session management/routing SessionManager exists
Configuration hot-reload
Network modes (loopback/LAN/remote) 🚧 HTTP only
OpenAI-compatible HTTP API /v1/chat/completions, per-request model override
Canvas hosting Agent-driven UI
Gateway lock (PID-based)
launchd/systemd integration
Bonjour/mDNS discovery
Tailscale integration
Health check endpoints /api/health + /api/gateway/status + /healthz + /readyz, with channel-backed readiness probes
doctor diagnostics 🚧 16 checks: settings, LLM, DB, embeddings, routines, gateway, MCP, skills, secrets, service, Docker daemon, tunnel binaries
Agent event broadcast 🚧 SSE broadcast manager exists (SseManager). Reborn has a transport-neutral projection EventStreamManager with access/admission/rebase/lag/redaction contracts, product-safe capability activity events plus bounded display-preview events, live thinking projection updates, and the local-dev WebUI serve path now wires it into /events and /ws through the WebUI product facade; local-dev WebUI also persists terminal tool previews as ordered transcript items and includes their timeline message ids on live preview events. Production durable/live fanout remains follow-up work.
Channel health monitor Auto-restart with configurable interval
Presence system Beacons on connect, system presence for agents
Trusted-proxy auth mode Header-based auth for reverse proxies; trustedProxy.allowLoopback for same-host reverse proxies
APNs push pipeline Wake disconnected iOS nodes via push; iOS push relay with App Attest verification
Oversized payload guard 🚧 HTTP webhook has 64KB body limit + Content-Length check; no chat.history cap
Pre-prompt context diagnostics 🚧 Token breakdown logged before LLM call (conversational dispatcher path); other LLM entry points not yet covered
OpenAI-compat /v1/models, /v1/embeddings Discovery + embeddings on top of /v1/chat/completions
Outbound proxy routing proxy.enabled + proxy.proxyUrl/OPENCLAW_PROXY_URL with strict http forward-proxy validation, loopback bypass; openclaw proxy validate
Diagnostics export bundle Sanitized logs/status/health/config/stability snapshots for bug reports
Startup diagnostics timeline Opt-in lifecycle/plugin-load phase tracing
Event-loop readiness in /readyz Event-loop delay (p99/max), utilization, CPU ratio, degraded flag
OpenTelemetry exporter pipeline Bundled diagnostics-otel plugin: model-call, tool, exec, outbound, context-assembly, memory pressure, harness lifecycle spans/metrics; W3C traceparent propagation; signal-specific OTLP endpoints
Prometheus exporter Bundled diagnostics-prometheus plugin with protected scrape route
Stability snapshots / payload-free liveness Default-on stability recording, event-loop delay/CPU snapshots in stability bundles

Owner: Unassigned


3. Messaging Channels

Channel OpenClaw IronClaw Priority Notes
CLI/TUI - Ratatui-based TUI
HTTP webhook - axum with secret validation
REPL (simple) - For testing
WASM channels - IronClaw innovation; host resolves owner scope vs sender identity
WhatsApp P1 Baileys (Web), same-phone mode with echo detection
Telegram - WASM channel(MTProto), polling-first setup, DM pairing, caption, manifest-declared /start, bot_username, DM topics, web/UI ownership claim flow, owner-scoped persistence
Discord 🚧 P2 Gateway MESSAGE_CREATE intake restored via websocket queue + WASM poll; Gateway DMs now respect pairing; thread parent binding inheritance and reply/thread parity still incomplete
Signal P2 signal-cli daemonPC, SSE listener HTTP/JSON-R, user/group allowlists, DM pairing
Slack - WASM tool
iMessage P3 BlueBubbles or Linq recommended
Linq P3 Real iMessage via API, no Mac required
Feishu/Lark 🚧 P3 WASM channel with Event Subscription v2.0; Bitable/Docx tools planned
WeCom 🚧 P2 Standalone WASM channel focused on WeCom intelligent bot WebSocket inbound/outbound, pairing, group sessions, inbound media hydration, and direct Bot media upload/send; self-built app callback + Agent API deferred
LINE P3
WeChat (iLink bot) 🚧 P2 Packaged extension-first channel, single-account DM flow with QR login, typing, image send/receive, inbound file/voice/video handling, outbound image/video/file media, and SILK-to-WAV voice fallback; multi-account remains deferred
WebChat - Web gateway chat
Matrix P3 E2EE support
Mattermost P3 Emoji reactions, interactive buttons, model picker
Google Chat P3
MS Teams P3
Twitch P3
Voice Call P3 Twilio/Telnyx/Plivo, stale call reaper, voicecall setup/smoke, openclaw_agent_consult realtime tool, agent-scoped voice agents, dedicated STT/TTS providers (Deepgram, ElevenLabs, Mistral, OpenAI/xAI realtime)
Google Meet P3 Bundled participant plugin: Google OAuth, explicit URL joins, Chrome+Twilio realtime transports, paired chrome-node support, attendance/artifact exports, calendar-backed exports, googlemeet doctor
Yuanbao (Tencent) P3 External plugin (openclaw-plugin-yuanbao) for WebSocket bot DMs and group chats
WeCom P3 Official external plugin pinned to npm release
Nostr P3

Telegram-Specific Features (since Feb 2025)

Feature OpenClaw IronClaw Notes
Forum topic creation Create topics in forum groups; message thread create CLI; learns human topic names from service messages
channel_post support Bot-to-bot communication
User message reactions Surface inbound reactions
sendPoll Poll creation via agent
Cron/heartbeat topic targeting Messages land in correct topic; cron --thread-id, explicit :topic: precedence
DM topics support Agent/topic bindings in DMs and agent-scoped SessionKeys
Persistent ACP topic binding ACP harness sessions can pin to Telegram forum or DM topics
sendVoice (voice note replies) audio/ogg attachments sent as voice notes; prerequisite for TTS (#90)
Native quote replies + retry reply_parameters.quote with fallback when QUOTE_TEXT_INVALID
Polling stall watchdog + liveness Configurable pollingStallThresholdMs, status/doctor warnings, dedicated getUpdates confirmation
HTML mode + chunking Long HTML messages chunked, plain-text fallback
Photo dimension preflight Falls back to document send when photo dims invalid
Webhook-mode setWebhook recovery Retries setWebhook after recoverable network failures

Discord-Specific Features (since Feb 2025)

Feature OpenClaw IronClaw Notes
Forwarded attachment downloads Fetch media from forwarded messages
Faster reaction state machine Watchdog + debounce
Thread parent binding inheritance Threads inherit parent routing
Persistent components/forms across restarts Active buttons/selects/forms keep working across Gateway restarts until expiry
autoArchiveDuration per-channel 1h/1d/3d/1w archive duration for auto-created threads
Auto thread name generation LLM-generated concise titles (autoThreadName: "generated")
Voice channel responses channels.discord.voice.model LLM override; voice mode auto-rejoin after RESUMED
CJK reply chunking Splits long CJK replies at punctuation/code-point-safe boundaries

Slack-Specific Features (since Feb 2025)

Feature OpenClaw IronClaw Notes
Streaming draft replies Partial replies via draft message updates
Configurable stream modes Per-channel stream behavior
Thread ownership 🚧 Reply participation memory is restart-stable and TTL-bounded; once the bot joins a thread, follow-ups inherit channel visibility. Full thread-level ownership tracking is still missing
Download-file action On-demand attachment downloads via message actions
App Home tab views Default Home view on app_home_opened, included in setup manifests
Persistent thread participation Bot-participated threads tracked across restarts
Block Kit limit hardening Auto-truncates buttons/selects/values, drops oversized link URLs while preserving valid blocks
Socket Mode pong tuning clientPingTimeout, serverPingTimeout, pingPongLoggingEnabled
Native model picker (/models) Provider/model chooser via interactive components

Mattermost-Specific Features (since Mar 2026)

Feature OpenClaw IronClaw Notes
Interactive buttons Clickable message buttons with signed callback flow; slash callback validation hardened
Interactive model picker In-channel provider/model chooser
replyToMode thread reply control Top-level posts can start thread-scoped sessions; all/first/never modes
Streaming draft preview Thinking, tool activity, partial reply text streamed into a single draft post
WebSocket ping/pong keepalives Stale TCP drops reconnect instead of leaving monitoring idle
DM-vs-channel routing fixes DM replies stay top-level; channel/group reply roots preserved

Feishu/Lark-Specific Features (since Mar 2026)

Feature OpenClaw IronClaw Notes
Doc/table actions feishu_doc supports tables, positional insert, color_text, image upload, and file upload
Rich-text embedded media extraction Pull video/media attachments from post messages
Native interactive cards Outgoing replies sent as native cards with clickable buttons
Schema 2.0 card action callbacks Accept new context.open_chat_id shape
Streaming cards Single live card per turn with throttled edits, topic-thread streaming
WebSocket retry/backoff Monitor-owned reconnects after SDK retry exhaustion
Voice-note transcription Inbound voice via shared media audio path
Bitable placeholder cleanup Remove default-valued rows in create-app cleanup

QQBot-Specific Features (since Mar 2026)

Feature OpenClaw IronClaw Notes
Engine architecture rewrite Self-contained engine with QR onboarding, native /bot-approve, per-account resource stacks, credential backup/restore
Group chat full support History tracking, @-mention gating, activation modes, per-group config, FIFO queue
C2C stream_messages StreamingController lifecycle manager
Chunked media upload Unified sendMedia for large files

BlueBubbles-Specific Features (since Mar 2026)

Feature OpenClaw IronClaw Notes
Persistent inbound GUID dedupe File-backed cache survives restart, 7-12x cron-duplicate fix
Catchup replay Per-account cursor + /api/v1/message/query?after= pass on restart
Reply-context API fallback Opt-in fetch for reply-context cache misses
TTS opus-in-CAF voice memos Pre-transcoded native voice-memo bubbles via tts.voice.preferAudioFileFormat
Per-group systemPrompt injection Group-specific behavioral instructions with * wildcard
Per-message catchup retry ceiling catchup.maxFailureRetries to skip persistently failing messages

Channel Features

Feature OpenClaw IronClaw Notes
DM pairing codes ironclaw pairing list/approve, host APIs
Allowlist/blocklist 🚧 allow_from + pairing store + hardened command/group allowlists
Self-message bypass Own messages skip pairing
Mention-based activation bot_username + respond_to_all_group_messages
Per-group tool policies Allow/deny specific tools
Thread isolation Separate sessions per thread/topic
Per-channel media limits 🚧 Caption support plus mediaMaxMb enforcement for WhatsApp, Telegram, and Discord
Typing indicators 🚧 TUI + channel typing, with configurable silence timeout; richer parity pending
Per-channel ackReaction config Customizable acknowledgement reactions/scopes
Group session priming Member roster injected for context
Sender_id in trusted metadata Exposed in system metadata
Per-group systemPrompt injection Per-group/per-direct system prompts injected via GroupSystemPrompt (Telegram, Discord, WhatsApp, BlueBubbles)
Visible reply enforcement messages.visibleReplies requires output via message(action=send); group-scope override available
Active-run steering queue messages.queue steer mode (default) drains queued messages at next model boundary; queue legacy one-at-a-time
Tool-progress streaming into previews Tool progress shown in live preview edits (Discord/Slack/Telegram/Mattermost/Matrix)
dmPolicy="open" semantics 🚧 Public open-DM only with effective wildcard; pairing-store senders no longer count for DM audits (OpenClaw fixed across all channels)

Owner: Unassigned


4. CLI Commands

Command OpenClaw IronClaw Priority Notes
run (agent) - Default command
tool install/list/remove - WASM tools
gateway start/stop P2
onboard (wizard) - Interactive setup
tui - Ratatui TUI
config - Read/write config plus validate/path helpers
backup P3 Create/verify local backup archives
channels 🚧 P2 Reborn channels list stays visible in --help/completions but exits nonzero as unimplemented (stub disabled); enable/disable/status deferred pending config source unification
models 🚧 P1 Reborn now uses a shared composition provider-admin facade for CLI models list [<provider>] (--verbose, --json), models status, models set <model>, models set-provider <provider> [--model model], plus Product Workflow typed model set-provider ... parsing without touching v1 state. Remaining: live model fetching, OAuth/API-key login flows, and wiring the provider-admin ProductCommandService into product surfaces.
status - System status (enriched session details)
agents P3 Multi-agent management
sessions P3 Session listing (shows subagent models)
memory - Memory search CLI
skills - CLI subcommands (list, search, info) + agent tools + web API endpoints
pairing - list/approve, account selector
nodes P3 Device management, remove/clear flows
plugins P3 Plugin management
hooks 🚧 P2 Reborn hooks list stays visible in --help/completions but exits nonzero as unimplemented (stub disabled); v1 hooks list supports bundled + plugin discovery, --verbose, --json
cron 🚧 P2 list/create/edit/enable/disable/delete/history; TODO: cron run, model/thinking fields
webhooks P3 Webhook config
message send P2 Send to channels
browser P3 Browser automation
sandbox - WASM sandbox
doctor 🚧 P2 16 subsystem checks
logs 🚧 P3 Reborn CLI logs stays visible but exits nonzero as unimplemented (stub disabled). v1: logs (gateway.log tail), --follow (SSE live stream), --level (get/set). WebUI v2 exposes bounded in-memory log projection at /api/webchat/v2/logs for non-operators and /api/webchat/v2/operator/logs for operators, both with level/target and run/thread/turn/tool/source scoped filters. No DB-persisted log history.
traces 🚧 -
  • IronClaw-native Trace Commons client MVP, not an OpenClaw parity feature.
  • Local opt-in capture, redaction, queueing, queue-status diagnostics, scoped web APIs, revocation, and periodic credit notices.
  • CLI opt-in writes the runtime/web user-scope policy that autonomous capture reads, and credentialed submit/status/revoke calls use bounded no-redirect HTTP.
  • Authenticated web paths are user-scoped and keep ingestion endpoint/credential settings out of user-managed policy updates.
  • Private TraceDAO server ingest/review/export/audit/retention/vector/credit infrastructure now lives in the standalone tracedao-server repository, with IronClaw retaining CLI/client integration wrappers.
update P3 Self-update; OPENCLAW_NO_AUTO_UPDATE=1 kill-switch
completion - Shell completion
migrate P3 Bundled importers for Claude Code, Claude Desktop, Hermes (config, MCP servers, skills, command prompts, model providers, credentials)
proxy validate P3 Verify effective proxy config, reachability, allow/deny destinations
plugins registry P3 Inspect persisted plugin registry; --refresh repair
plugins deps P3 Inspect/repair bundled plugin runtime dependencies
infer model run --gateway P3 Raw model probes via Gateway; image --file + --prompt + --timeout-ms overrides
infer image describe/describe-many P3 Custom vision prompts/timeouts
qa (suite/telegram/credentials) P3 QA Lab CI runner with --allow-failures opt-out
voicecall setup/smoke/continue P3 Voice call provider readiness, dry-run smoke, gateway-delegated continue
googlemeet doctor/recover-tab P3 Meet OAuth/browser-state diagnostics, tab recovery
matrix verify/encryption setup P3 E2EE setup, recovery key rotation, cross-signing trust
nodes remove P3 Remove stale gateway-owned node pairing records
nodes list (paired view) P3 Default paired-node view with pending fallback
cron run / cron edit --thread-id 🚧 P2 Already partial; OpenClaw added cron stagger, finished-run webhook, --failure-alert-include-skipped
sessions export-trajectory P3 Per-run trajectory bundles with redacted transcripts/runtime events/prompts
/subagents spawn P3 Spawn subagents from chat
/export-session P3 Export current session transcript
/export-trajectory (chat) P3 Per-run exec-approved trajectory bundle, owner-only delivery
/diagnostics (owner-only) P3 Owner-only diagnostics export with sensitive-data preamble
/codex computer-use status/install P3 Codex desktop control setup with marketplace discovery
/dock-* route switches P3 Switch active session reply route through session.identityLinks
--container / OPENCLAW_CONTAINER P3 Run process commands inside running Docker/Podman container

Trace Commons incremental note: reviewer quarantine and active-learning queues now surface prioritization metadata, including review_age_hours, review_escalation_state, and review_escalation_reasons, so CLI non-JSON output can show SLA pressure and escalation causes during triage. DB-backed review leases now let reviewer/admin principals claim, release, claim the next available tenant-scoped quarantined trace, or claim a bounded prioritized batch through POST /v1/review/leases/claim-next, POST /v1/review/leases/claim-batch, ironclaw traces review-lease-claim-next, and ironclaw traces review-lease-claim-batch, using review escalation/SLA priority ordering before writing DB lease state and typed claim/release audit rows; they also expose lease assignment metadata in review queues, support all, mine, available, active, and expired lease filters in API/CLI/web operator queues, and block other reviewers from finalizing while a lease is active. Analytics can now suppress aggregate cells below a configured minimum count while reporting the suppression threshold and number of hidden buckets. Tenant token entries can now carry optional RFC3339 expires_at/expires attributes, and the ingest service can accept optional HS256 signed tenant claims that bind tenant id, actor principal, role, issuer/audience when configured, allowed consent scopes/uses, and expiry without enumerating every bearer token; claim allow-lists now constrain submission, replay exports, benchmark/ranker dataset generation, process-evaluation workers, and utility-credit jobs. Operator docs now pin production asymmetric upload-claim governance to managed issuer/key rotation with EdDSA/Ed25519, leaving static tokens and HS256 claims as internal bridge paths, and TRACE_COMMONS_REQUIRE_EDDSA_SIGNED_TOKENS now rejects those bridge credentials on every authenticated route when enabled. Keyed signed-token secrets and EdDSA public-key files support kid-selected rotation, deployments can cap signed-claim lifetimes by requiring iat and bounding exp - iat, require JWT IDs before accepting signed claims, emergency-denylist signed-claim JWT IDs by jti, and config status exposes only key/EdDSA-key/denylist/max-TTL/JTI-policy counts plus the EdDSA-required auth gate while submitted audit rows record only the safe auth method plus hashed principal. Retention maintenance also honors TRACE_COMMONS_LEGAL_HOLD_RETENTION_POLICIES so configured policy classes are skipped for new expiration and purge passes, and DB-backed maintenance runs now write durable retention job/item ledger rows for resumable expire/purge/revoke bookkeeping with admin-only API plus CLI and web-operator reads for tenant-scoped jobs and per-submission lifecycle items. Maintenance DB reconciliation now runs after the retention ledger write and reports DB retention job/item counts plus current-run retention job or item-count gaps as promotion blockers. Process-evaluation workers have a CLI submit helper for POST /v1/workers/process-evaluation, store bounded rubric metadata under the process_evaluation worker kind, mirror typed hash/count-only audit metadata, can optionally append idempotent training_utility delayed credit for the evaluated accepted submission using an external reference, preserve separate DB derived rows per evaluator version while feeding content-free process-evaluation analytics by label, rating, and score band without double-counting DB-backed submissions, and now require tenant policy or signed-claim evaluation-use ABAC before reading or labeling accepted trace bodies. Utility-credit workers now also require the source trace plus tenant policy or signed claim to allow the requested regression/evaluation, model-training, or ranking-training utility use before appending delayed credit. Object-primary envelope writes now use unique encrypted artifact object ids per logical snapshot so review/process-evaluation writes do not overwrite ciphertext behind older submitted-envelope object refs, terminal-trace status sync can explain retained-but-excluded delayed ledger rows without exposing them through contributor credit-event reads, web enqueue/submit and CLI queue writes reject crafted requests/envelopes that try to include message text or tool payloads disallowed by the standing policy, DB stores now reject derived rows, vector entries, and export manifest items whose object, derived, or vector refs do not belong to the same tenant/submission, periodic local credit notices now include delayed ledger deltas plus credit-event counts and a scoped durable retry outbox with safe delivery attempt hashes, CLI status sync resets credit notices when delayed-credit explanations change even without a numeric delta, and autonomous runtime capture skips ineligible current traces instead of leaving held queue files while preserving queue flush/credit notices.

This push also adds local autonomous queue diagnostics/status surfaces: CLI traces queue-status reports readiness, bearer-token environment presence, queue/held counts, retry/manual-review/policy hold counts, next retry time, durable flush/status-sync telemetry, retryable submission failure counters, last compaction reclaimed count, duplicate envelopes removed, orphan hold sidecars removed, malformed envelopes quarantined, sanitized held-reason counts, safe queue warning aggregates, warning severity, production-promotion blocking flags, safe recommended actions, sanitized failure classes, and local credit summaries; authenticated web /api/traces/queue-status reports scoped queue/held diagnostics plus the same durable telemetry, and /api/traces/credit-notice marks due notices. Due credit notices now carry local acknowledge/snooze state: CLI traces credit --notice --ack and authenticated POST /api/traces/credit-notice acknowledgement suppress the current credit fingerprint until credit changes, while --snooze-hours and the matching web action suppress it until a bounded deadline without exposing trace bodies or explanation text in the fingerprint. The agent loop now runs a periodic Trace Commons queue worker for opted-in owner/active-user scopes, stores retryable submission failures as typed redacted sidecars with capped backoff, skips retry-held envelopes until due, records durable scoped telemetry for queue/status-sync attempts, writes queue JSON through atomic temp-file replacement, compacts duplicate queued contribution envelopes and orphan held sidecars before submission, quarantines malformed active queue files locally instead of blocking later valid uploads, and broadcasts returned credit notices. Diagnostics warn on schema-version, consent-policy, redaction-pipeline, trace-card-redaction-pipeline, and malformed-envelope mismatches without raw bodies or raw observed mismatch values, and classify local failures into sanitized Endpoint, Credential, Network, NetworkOffline, NetworkDns, NetworkTimeout, NetworkConnectionRefused, HttpRejection, Policy, Queue, StatusSync, Submission, and Unknown buckets. EdDSA/Ed25519 public-key verification is available through default or kid-selected key config and JSON/file/guarded-HTTPS keysets with optional activation windows, with safe total/active/inactive/managed EdDSA config-status counts; managed EdDSA-required mode now accepts only active managed-keyset claims with issuer/audience checks. Autonomous clients can refresh short-lived EdDSA upload claims from guarded HTTPS issuers for queue flush, explicit submit, status sync, and remote revoke calls, and ingestion services now refresh guarded HTTPS issuer-managed Ed25519 keysets live with last-good preservation and optional max-stale fail-closed enforcement.

Trace Commons hardening note: required DB mirror mode, object refs, PostgreSQL/libSQL storage, RLS diagnostics, and encrypted artifact storage now live in the public zmanian/tracedao-server repo rather than Ironclaw's shared DB abstraction. Ironclaw retains local-first trace contribution capture, upload-claim fetching/validation, queue/status/credit notice behavior, and client-facing CLI/web helpers behind the Reborn-aligned TraceClientHost product facade for local redaction, queueing, status sync, and credit-notice delivery.

Trace Commons production-boundary note: PostgreSQL/libSQL now include a durable tenant-scoped revocation-propagation ledger for downstream invalidation and retry work across object refs, exports, vectors, derived artifacts, benchmark/ranker artifacts, credit settlements, and physical delete receipts. The revocation worker can now reverse exact tenant-scoped delayed-credit settlements with deterministic negative ledger rows, verify and physically delete exact service-local encrypted submitted/review envelope, vector worker-intermediate, benchmark artifact, and ranker export provenance object payloads for tenant-scoped object-ref items, mark matching object refs deleted, and upsert durable physical-delete receipt rows with evidence hashes, while marking unsupported stores and artifact kinds as skipped. Export call sites for replay, benchmark, and ranker slices now create and validate short-lived tenant/principal/purpose/dataset-kind access grants before producing artifacts. TRACE_COMMONS_OBJECT_STORE=remote_service parses production remote object-store intent but deliberately fails closed behind a disabled service-owned provider instead of falling back to plaintext files. Local autonomous status sync now keeps append-only safe history events and sanitizes server-returned credit explanations before periodic credit notices persist them, and local credit notice delivery now drains a scoped retry outbox so channel failures leave retry state instead of consuming the notice.

Trace Commons revocation-worker note: the ingest service now recognizes a scoped revocation_worker role and exposes POST /v1/workers/revocation-propagation for DB-backed propagation runs. The worker claims due tenant-scoped ledger items, performs idempotent metadata/vector/export invalidation actions, reverses exact delayed-credit settlement targets with deterministic audit-safe ledger rows, physically deletes hash-verified service-local submitted/review envelope, vector worker-intermediate, benchmark artifact, and ranker export provenance payloads for exact object-ref targets, records durable physical-delete receipt items after successful or already-recorded service-local payload deletion, records unsupported physical-delete stores/artifact kinds as explicit skipped items, preserves other tenants' due work, and emits safe audit counts.

Trace Commons vector-lifecycle note: vector indexing now writes deterministic local redacted-summary feature embeddings into encrypted service-local WorkerIntermediate vector payload objects while keeping relational vector rows metadata-only. PostgreSQL/libSQL storage can invalidate one vector entry for a tenant/submission/vector id, revocation propagation requires a vector-entry target for vector invalidation instead of broad accidental invalidation, and service-local vector payload deletes verify the encrypted object as a vector artifact before marking the object ref deleted and recording a physical-delete receipt.

Trace Commons export-job note: replay dataset, benchmark conversion, ranker-candidate, and ranker-pair export call sites now mirror their short-lived one-shot access grants plus running/complete export job lifecycle rows into the PostgreSQL/libSQL DB control plane. Required DB mirror mode now fails closed if a durable export job cannot be started or completed, replay exports now mark already-started DB job rows failed when metadata or required object-ref body reads fail before publication, benchmark/ranker exports do the same for metadata/source collection, source object-ref revalidation, and source-read audit failures before artifact publication, and tests cover tenant-scoped grant/job persistence plus replay and benchmark/ranker failure terminalization.

Trace Commons worker-export note: export-worker automation now has dedicated replay and ranker export routes, GET|POST /v1/workers/replay-export, GET|POST /v1/workers/ranker/training-candidates, and GET|POST /v1/workers/ranker/training-pairs, plus matching CLI helpers. These routes reuse the same consent/use ABAC, access-grant, export-job, source-hash, audit, and delayed-credit behavior as the reviewer/admin routes while keeping scheduled automation off reviewer endpoints.

Trace Commons export-control observability note: admins can now list tenant-scoped durable export access grants and export jobs through GET /v1/admin/export/access-grants and GET /v1/admin/export/jobs, with status and dataset-kind filters plus matching CLI helpers. GET /v1/admin/operational-summary and ironclaw traces operational-summary now add an admin-only, tenant-scoped aggregate rollout view for submission status/risk, review SLA pressure, DB export manifests/jobs, retention jobs, vector coverage, and delayed-credit totals. Reads are DB-backed where applicable, admin-only, tenant-scoped, and audited without exposing trace bodies.

Trace Commons tenant-access grant note: PostgreSQL/libSQL now include a durable tenant-scoped trace_tenant_access_grants storage surface for issuer-authorized principals, roles, consent scopes, allowed uses, issuer/audience/subject attribution, status, expiry, revocation metadata, and safe metadata. Admin-token routes and CLI helpers can create, list, and revoke the current tenant's grants while writing safe hash/count-only grant-update audit metadata, and the local tenant-principal-ref CLI helper derives stored static-token or signed-claim principal refs without printing raw credentials. TRACE_COMMONS_REQUIRE_TENANT_ACCESS_GRANTS=true now fails closed on trace submission, contributor credit/status readback, reviewer/audit reads, review mutations, dataset/export paths, non-revocation worker mutations, maintenance, and admin ledger/observability reads unless the authenticated tenant/principal has an active exact-role grant. Signed EdDSA/Ed25519 claims must also match any issuer, audience, and JWT sub subject bindings configured on the grant before scope/use narrowing is applied, while static-token bridge grants ignore those signed-claim-only bindings. Grant consent/use allow-lists intersect with static-token or EdDSA claim allow-lists and cannot upgrade the request role; revocation/self-delete, revocation propagation, config-status, tenant-policy admin, and grant-management routes stay available for deprovisioning and recovery.

Trace Commons issuer/TenantCtx note: the server-side zmanian/tracedao-server split now owns the standalone EdDSA/Ed25519-only tracedao-upload-claim-issuer binary that signs short-lived contributor upload claims, authenticates workload JWTs with EdDSA only, enforces workload issuer/audience/expiry plus consent/use narrowing, optionally connects to PostgreSQL/libSQL from deployment config to require DB-backed contributor tenant-access grants using the same signed-principal hash shape as ingest, rejects RSA key material, and publishes the ingest-compatible kid/public_key_pem keyset shape. The ingest compatibility layer also now fails closed when file-backed submission metadata, derived rows, credit ledger rows, audit rows, revocation tombstones, replay manifests, export provenance, or benchmark artifacts are read from one authenticated tenant directory but carry a different embedded tenant id or tenant storage ref; service-local object-ref reads/deletes also verify tenant key refs, encrypted benchmark artifact reads verify the decrypted body tenant, and vector payload deletes verify the encrypted payload body's tenant storage ref before physical deletion.

Owner: Unassigned


5. Agent System

Feature OpenClaw IronClaw Notes
Pi agent runtime IronClaw uses custom runtime
RPC-based execution Orchestrator/worker pattern
Multi-provider failover FailoverProvider tries providers sequentially on retryable errors
Per-sender sessions
Global sessions Optional shared context
Session pruning Auto cleanup old sessions; oversized sessions.json rotation removed; entry/age caps enforced at load
Context compaction Auto summarization with deterministic retention-boundary secret redaction and fail-closed residual checks
Compaction model override Use a dedicated provider/model for summarization only; agents.defaults.compaction.memoryFlush.model exact override
Compaction mid-turn precheck agents.defaults.compaction.midTurnPrecheck triggers before next tool call instead of end-of-turn
Post-compaction read audit Layer 3: workspace rules appended to summaries
Post-compaction context injection Workspace context as system event
Compaction start/end notices Opt-in lifecycle notices during compaction
Custom system prompts Template variables, safety guardrails
Skills (modular capabilities) Prompt-based skills with trust gating, attenuation, activation criteria, catalog, selector; Reborn local-dev now uses catalog/list-first model-selected activation before loading full skill context
Skill Workshop plugin Captures reusable workflow corrections as pending or auto-applied workspace skills, threshold-based reviewer
Grouped skill directories skills/<group>/<skill>/SKILL.md discovery
Skill installer metadata One-click install recipes (npm/pip), API key entry, source metadata
Skill routing blocks 🚧 ActivationCriteria (keywords, patterns, tags) but no "Use when / Don't use when" blocks
Skill path compaction ~ prefix to reduce prompt tokens
Thinking modes (off/minimal/low/medium/high/xhigh/adaptive/max) 🚧 thinkingConfig for Gemini models; no per-level control yet; Anthropic Opus 4.7 xhigh+adaptive+max; DeepSeek V4 xhigh/max
Per-model thinkingDefault override Override thinking level per model; Anthropic Claude 4.6/4.7 defaults to adaptive
Adaptive→provider thinking maps /think adaptive maps to Gemini dynamic thinking, Anthropic adaptive, OpenAI flex
Native Codex app-server runtime New embedded Codex harness with PreToolUse/PostToolUse/PermissionRequest relay; replaces ACP for codex/* models
Codex Computer Use Desktop control setup with marketplace discovery, fail-closed MCP checks
Codex hooks bridge Codex-native tool hooks → OpenClaw plugin hooks/approvals
Codex sub-agent metadata Native Codex sub-agent session metadata without nested gateway patch
Codex context-engine integration Bootstrap, assembly, post-turn maintenance, engine-owned compaction in Codex sessions
Active Memory plugin Dedicated memory sub-agent right before main reply; configurable message/recent/full context modes; partial-recall on timeout; per-conversation allowedChatIds/deniedChatIds filters
Inferred follow-up commitments Opt-in hidden batched extraction with per-agent/per-channel scoping, heartbeat delivery, CLI management; commitments.enabled/maxPerDay
sessions_yield Orchestrators end current turn immediately, skip queued tool work, carry hidden follow-up payload to next turn
Subagent forked context Optional inherit-requester-transcript for native sessions_spawn
agents.defaults.contextInjection: "never" Disable workspace bootstrap injection per-agent
agents.defaults.experimental.localModelLean Drop heavyweight default tools for weaker local models
agents.files.get/set workspace tools 🚧 First-party scoped read/write/list/glob/grep/apply_patch capabilities exist through Reborn HostRuntime; OpenClaw-compatible agents.files.* aliases and realpath-via-fd hardening still pending
Trajectory export 🚧 QA-only and disabled by default: setting IRONCLAW_REBORN_REGRESSION_ARTIFACT_EXPORT=true lets WebChat v2 download caller-owned, deterministically redacted artifacts for one exact run (ironclaw.run_artifact.v1) or a complete multi-run thread (ironclaw.thread_artifact.v1), including replay metadata and bounded scoped logs; full-thread exports fail closed with 413 when the documented message/byte budget is exceeded; default-on local capture and full event/artifact parity remain follow-up
Block-level streaming
Tool-level streaming
Z.AI tool_stream Real-time tool call streaming
Plugin tools WASM tools
GSuite WASM tools 🚧 Reborn bundles operation-level Google Drive/Docs/Sheets/Slides WASM packages with host-mediated HTTP egress, product-auth scoped bearer injection, and manifest-declared Google OAuth setup metadata; full live-recorded parity remains follow-up
Hosted MCP extensions 🚧 Reborn composes host-mediated MCP runtime, bundles the current Notion MCP supported tool set, wires Notion ProductAuth OAuth exchange/refresh, can use Reborn ProductAuth DCR OAuth setup through the host callback origin, and can activate hosted MCP packages with live tools/list schema discovery through host-staged product-auth credentials
NEAR AI MCP extension 🚧 Host-bundled Reborn MCP extension exposes nearai.web_search via host-mediated HTTP and llm_nearai_api_key; local-dev startup now auto-seeds product-auth and activates the bundled MCP extension when NEARAI_BASE_URL plus NEARAI_API_KEY are configured, runtime credential resolution treats that seeded account as host-managed for the bundled nearai requester across WebUI SSO users in the same tenant/agent scope, with project-scoped host credentials limited to their project and tenant/agent-level host credentials covering project-scoped runtime calls, without exposing it to other requesters/providers, and WebChat v2 no longer projects that host-managed credential as extension setup work while NEAR remains a static supported-tool adapter
Tool policies (allow/deny) Reborn now stores scoped persistent AlwaysAllow approval policies for manifest-allow capabilities and replays them at the current sandbox scope; WebChat v2 exposes authenticated caller-scoped tool approval settings at /api/webchat/v2/settings/tools so regular multi-user sessions do not need operator config access; product-facing revoke paths remain follow-up while the policy-store revoke interface is available
Exec approvals (/approve) TUI approval overlay
Tool inventory cache Coalesced effective-tool inventory cache with channel-registry invalidation
Pending exec approval errorMessage cleanup Failed restart-interrupted approval-pending sessions instead of replaying stale ids
Elevated mode Privileged execution
Subagent support Task framework; spawn-by-account-aware bindings, model overrides preserved; Reborn spawn_subagent is blocking-only while background delivery is deferred (#4147)
/subagents spawn command Spawn from chat
Auth profiles Multiple auth strategies; replaceDefaultModels migration semantics
Generic API key rotation Rotate keys across providers
Stuck loop detection Exponential backoff on stuck agent loops; unknown-tool guard default-on
llms.txt discovery Auto-discover site metadata
Multiple images per tool call Single tool call, multiple images
Web search extension 🚧 Host-bundled web-access extension provides no-config Exa MCP search and saved-result content retrieval; Brave backend and generic fetch parity still pending
URL allowlist (web_search/fetch) Restrict web tool targets
suppressToolErrors config Hide tool errors from user
Intent-first tool display Details and exec summaries
Transcript file size in status Show size in session status
Stuck-session recovery Conservative recovery releases stale lanes while preserving active embedded runs/replies
Runner: in /status Reports embedded Pi/CLI-backed/ACP harness in session status
Voice Wake routing Wake phrases can target named agent or session via gateway routing APIs

Owner: Unassigned


6. Model & Provider Support

Provider OpenClaw IronClaw Priority Notes
NEAR AI - Primary provider
Anthropic (Claude) 🚧 - Via NEAR AI proxy; Opus 4.7 (default, adaptive+xhigh+max), Opus 4.6, Sonnet 4.6
OpenAI 🚧 - Via NEAR AI proxy; GPT-5.5 default, GPT-5.4-pro forward-compat, Codex OAuth, Responses API; image generation (gpt-image-2) via Codex OAuth
OpenAI Codex (native app-server) - App-server >=0.125.0 with native MCP hooks, dynamic tools, approval relay
AWS Bedrock - Native Converse API; Claude Opus 4.7 thinking profile (xhigh/adaptive/max); IAM bearer token refresh for Mantle
Google Gemini - OAuth (PKCE + S256), function calling, thinkingConfig, generationConfig; TTS (gemini-embedding-2-preview); image gen native API; ADC-backed Vertex
Google Gemini Live (realtime) - Realtime voice provider for Voice Call/Google Meet, bidirectional audio + function calls
io.net P3 Via ionet adapter
Mistral P3 Via mistral adapter; Voice Call streaming STT
Yandex AI Studio P3 Via yandex adapter
Cloudflare Workers AI P3 Via cloudflare adapter
NVIDIA API P3 Via nvidia adapter; OpenClaw added bundled provider with API-key onboarding, static catalog, literal model-ref picker, NIM string-content compat
OpenRouter - Via OpenAI-compatible provider; OpenClaw added native video generation, openrouter:auto/openrouter:free aliases, Hunter/Healer Alpha, free-model fallback for models scan
Tinfoil - Private inference provider (IronClaw-only)
OpenAI-compatible - Generic OpenAI-compatible endpoint (RigAdapter); OpenAI-style image inputs default missing image_url.detail to auto
GitHub Copilot - Dedicated provider with OAuth token exchange; default Opus model is claude-opus-4.7; GUI/RPC wizard device-code auth; gpt-5.4 xhigh thinking
Ollama (local) - OpenClaw added Cloud + Local + cloud-only modes, browser sign-in, signed /api/experimental/web_search, params.num_ctx/params.think/params.keep_alive, /api/show capability detection
Perplexity P3 Freshness parameter for web_search
MiniMax P3 Regional endpoint selection; portal OAuth + Token Plan + MINIMAX_API_KEY; image-01, music-2.6, video; MiniMax-VL-01 for vision
GLM-5 P3 Via Z.AI provider (zai) using OpenAI-compatible chat completions
Tencent Cloud (TokenHub) P3 Bundled provider; Hy3 catalog with tiered pricing
DeepInfra P3 Bundled provider with DEEPINFRA_API_KEY, dynamic OpenAI-compatible discovery, image gen/edit, image/audio understanding, TTS, text-to-video, embeddings
Cerebras P3 Bundled plugin with onboarding, static catalog, manifest endpoint metadata
Z.AI / GLM-5 - OpenClaw added bundled GLM catalog/auth in plugin manifest, params.preserveThinking for reasoning_content replay
Qwen / Model Studio P3 Standard DashScope endpoints (CN + global) + Coding Plan; vLLM Qwen thinking controls
DeepSeek P3 V4 Pro/V4 Flash bundled, V4 Flash onboarding default, native xhigh/max thinking levels, reasoning_content replay support
Moonshot / Kimi P3 Kimi K2.6 default; native Anthropic-format tool calls; CN API endpoint support; kimi-coding web search via KIMI_API_KEY
xAI P3 Image gen (grok-imagine-image/pro), reference-image edits, six TTS voices (MP3/WAV/PCM/G.711), grok-stt audio transcription, realtime STT for Voice Call
Tencent Yuanbao P3 External plugin (openclaw-plugin-yuanbao) for chat
Vercel AI Gateway P3 Provider-owned thinking levels for trusted upstream refs
Codex/OpenAI image generation P2 gpt-image-2/gpt-image-1.5 via Codex OAuth or API key; multipart reference-image edits; Azure deployment-scoped image URLs
OpenRouter image/video generation P3 Image gen + reference edits; native video generation through video_generate
MiniMax music/video P3 music-2.6, video_generate, MiniMax-portal registration
Google Veo (video gen) P3 Direct MLDev video.uri downloads; REST predictLongRunning fallback
fal Seedance 2.0 P3 Reference-to-video with multi-image/video/audio input
Comfy (image/video/music) P3 plugins.entries.comfy.config workflow + cloud auth
node-llama-cpp - OpenClaw made it optional (no longer auto-installed); local embeddings now opt-in
llama.cpp (native) 🔮 P3 Rust bindings

Model Features

Feature OpenClaw IronClaw Notes
Auto-discovery Manifest-backed modelCatalog with aliases/suppressions; cold installed-index fast path
Failover chains FailoverProvider with configurable fallback_model
Cooldown management Lock-free per-provider cooldown in FailoverProvider
Per-session model override Model selector in TUI
Model selection UI TUI keyboard shortcut; OpenClaw added Quick Settings, mobile-aware picker
Per-model thinkingDefault Override thinking level per model in config
1M context support Anthropic extended context beta + OpenAI Codex GPT-5.4 1M context; Claude Opus 4.7 + claude-cli normalized to 1M
Fast mode (/fast) Anthropic service_tier + OpenAI gpt-5.4-fast; /fast toggle, TUI/Control UI/ACP, per-model defaults
Tiered model pricing Pricing tiers from cached catalogs (Moonshot Kimi K2.6/K2.5, Hy3) for usage reports
models scan (free-model fallback) Public OpenRouter free-model metadata when no OPENROUTER_API_KEY
Model catalog stale cache fallback Serve last successful catalog while stale reloads refresh in background
models.pricing.enabled Skip startup OpenRouter/LiteLLM pricing-catalog fetches for offline installs
Auth status card OAuth token health + provider rate-limit pressure with models.authStatus RPC
Model fallback metadata model.fallback_step trajectory events with from/to + chain position + final outcome
prompt_cache_key opt-in compat.supportsPromptCacheKey per-provider opt-in
Replay normalization Repair displaced/missing tool results, Anthropic/Bedrock thinking signature stripping, OpenAI Responses orphaned reasoning, Codex aborted-output replay

TTS / STT / Realtime Voice

Feature OpenClaw IronClaw Priority Notes
TTS (Microsoft / Edge) P3 Auto-enabled bundled provider; legacy messages.tts.providers.edge voices
TTS (OpenAI) P3 OpenAI-compatible /audio/speech
TTS (ElevenLabs v3) P3 eleven_v3 model surfaced; PCM telephony
TTS (Google Gemini) P3 audioProfile + speakerName prompt control; PCM-to-Opus voice notes
TTS (Azure Speech) P3 Bundled provider, Speech-resource auth, SSML, native Ogg/Opus
TTS (Inworld) P3 Streaming synthesis, voice-note + PCM telephony
TTS (Volcengine/BytePlus Seed Speech) P3 Bundled provider, Ogg/Opus voice notes, MP3 file output
TTS (Xiaomi MiMo) P3 MP3/WAV + voice-note Opus transcoding
TTS (Local CLI) P3 Bundled local command speech provider with file/stdout/Opus/PCM
TTS (Gradium) P3 Bundled TTS provider with voice-note + telephony output
TTS (OpenRouter) P3 OpenAI-compatible /audio/speech via OPENROUTER_API_KEY
TTS (xAI) P3 Six grok voices, MP3/WAV/PCM/G.711
TTS (DeepInfra) P3 Bundled provider
TTS (MiniMax) P3 Portal OAuth + Token Plan; HD model ids
TTS (Tinfoil/local MLX) P3 macOS Talk experimental MLX provider
TTS personas P3 Provider-aware personas with deterministic provider binding, /tts persona, Gemini audio-profile-v1, OpenAI instructions
Auto-TTS controls P3 /tts latest, /tts chat on|off|default; per-account/per-agent overrides
Talk Mode (browser realtime) P3 OpenAI Realtime + Google Live WebRTC/WS; ephemeral client secrets; openclaw_agent_consult handoff
STT (OpenAI Realtime) P3 Voice Call streaming transcription
STT (xAI realtime) P3 Voice Call streaming via grok-stt
STT (Deepgram) P3 Voice Call streaming
STT (ElevenLabs Scribe v2) P3 Batch + streaming inbound transcription
STT (Mistral) P3 Voice Call streaming
STT (SenseAudio) P3 Bundled batch audio transcription via tools.media.audio
STT (local Whisper CLI) P3 Configured/key-backed STT preferred over auto-detected local Whisper

Owner: Unassigned


7. Media Handling

Feature OpenClaw IronClaw Priority Notes
Image processing (Sharp) P2 Resize, format convert
Configurable image resize dims P2 Per-agent dimension config
Multiple images per tool call P2 Single tool invocation, multiple images
Audio transcription P2 Multiple providers (see TTS/STT subsection in Section 6)
Video support P3 OpenRouter native video gen, MiniMax video, Google Veo, fal Seedance, OpenAI Sora
PDF analysis tool P2 Native Anthropic/Gemini path with text/image extraction fallback; bundled document-extract plugin owns pdfjs-dist
PDF parsing 🚧 P2 Uploaded document attachments and Reborn builtin.read_file parse PDFs via pdf-extract; no pdfjs-dist fallback path
MIME detection P2 Bounded MIME sniff + ZIP archive preflight
Media caching P3
Vision model integration P2 Image understanding; agents.defaults.imageModel, Codex app-server image turns, configured-provider exact match
Image generation P2 OpenAI gpt-image-2 / gpt-image-1.5, OpenRouter, Gemini, MiniMax image-01; quality + format + background hints
Music generation P3 MiniMax music-2.6, fal, video-to-music workflows
Multimodal memory indexing P3 Image + audio indexing for memorySearch.extraPaths via Gemini gemini-embedding-2-preview
Audio-as-voice routing P2 [[audio_as_voice]] directives on text tool-result MEDIA: payloads
TTS providers P2 See TTS/STT subsection in Section 6
Incremental TTS playback P3 iOS progressive playback
Sticker-to-image P3 Telegram stickers
Per-channel media limits 🚧 P2 mediaMaxMb enforcement (already in Section 3); Signal getAttachment honors mediaMaxMb with base64 headroom

Owner: Unassigned


8. Plugin & Extension System

Feature OpenClaw IronClaw Notes
Dynamic loading WASM modules
Manifest validation WASM metadata; modelCatalog, channelConfigs, setup.providers, setup.requiresRuntime, activation.onStartup contracts
HTTP path registration Plugin routes
Workspace-relative install ~/.ironclaw/tools/
Channel plugins WASM channels
Auth plugins
Memory plugins Custom backends + selectable memory slot
Context-engine plugins Custom context management + subagent/context hooks; info.id slot match enforced
Tool plugins WASM tools
Hook plugins Declarative hooks from extension capabilities
Provider plugins Manifest-backed catalogs/aliases/suppressions; setup auth metadata
Plugin CLI (install, list) tool subcommand
ClawHub registry Discovery; install scope --profile, npm: install prefix to skip ClawHub lookup, clawhub: install records
git: plugin installs First-class git: install with ref checkout, commit metadata, plugins update for git sources
before_agent_start hook modelOverride/providerOverride support
before_agent_finalize hook New finalize hook with run/message/sender/session/trace correlation
before_message_write hook Pre-write message interception
before_dispatch hook Canonical inbound metadata; route handled replies through normal final delivery
before_compaction/after_compaction hooks Codex-native compaction lifecycle
llm_input/llm_output hooks LLM payload inspection (Codex app-server included)
model_call_started/ended hooks Metadata-only, no prompts/responses/headers/raw provider request IDs
cron_changed hook Typed cron lifecycle observer
gateway_start hook context Startup config, workspace dir, live cron getter
agent_end observation hooks 30s timeout for non-settling hooks
Plugin SDK state store SQLite-backed api.runtime.state.openKeyedStore for restart-safe keyed registries with TTL/eviction
Plugin SDK Codex extensions Async tool_result middleware, after_tool_call for Codex tool runs
Persisted plugin registry Cold registry index, openclaw plugins registry inspection, --refresh repair
plugins deps --repair Bundled runtime-deps inspect + repair without rerunning plugin runtime
Plugin install conflict-aware writes Install/uninstall config writes are conflict-aware; managed plugin files removed only after config commit
Plugin compatibility registry Central deprecation registry with dated owners + replacements + 3-month removal targets
Layered runtime-deps roots OPENCLAW_PLUGIN_STAGE_DIR resolves read-only preinstalled deps before installing missing deps
Bundled provider catalogs in manifest DeepInfra, Cerebras, Mistral, Moonshot, DeepSeek, Tencent, StepFun, Venice, Fireworks, Together, Groq, Qianfan, Xiaomi, BytePlus, Volcano Engine, NVIDIA

Owner: Unassigned


9. Configuration System

Feature OpenClaw IronClaw Notes
Primary config file ~/.openclaw/openclaw.json .env Different formats
JSON5 support Comments, trailing commas
YAML alternative
Environment variable interpolation ${VAR}
Config validation/schema Type-safe Config struct + openclaw config validate; OpenClaw added top-3 issue surface for config.set/patch/apply
Hot-reload Many plugins now re-read live runtime config (memory-lancedb, active-memory, github-copilot, ollama, openai, amazon-bedrock, codex, skill-workshop, diffs, gateway-tool); OPENCLAW_NO_AUTO_UPDATE=1 kill-switch
Legacy migration OpenClaw dropped automatic migrations older than two months
State directory ~/.openclaw-state/ ~/.ironclaw/
Credentials directory Session files
Full model compat fields in schema pi-ai model compat exposed in config
models.pricing.enabled Skip OpenRouter/LiteLLM pricing fetches for offline installs
agents.list[].contextTokens Per-agent context window override
gateway.handshakeTimeoutMs Tunable WebSocket pre-auth handshake budget
--profile <name> Plugin install destinations resolve from active profile state dir
Config recovery on clobber Restore last-known-good config on critical clobber signatures (missing metadata, missing gateway.mode, sharp size drops); foreground/service notices include rejected paths
Modular $include files Single-file top-level includes for isolated mutations; plugins install/update updates plugins.json5 instead of flattening
config set --merge/--replace Additive vs intentional clobber for provider model maps
Wrapper-based service install (Reborn) --wrapper/OPENCLAW_WRAPPER validated executable LaunchAgent/systemd wrappers; Reborn's ironclaw service install covers launchd (macOS)/systemd (Linux) with a webui-token-file fallback and atomic install + rollback on failure

Owner: Unassigned


10. Memory & Knowledge System

Feature OpenClaw IronClaw Notes
Vector memory pgvector
Session-based memory
Hybrid search (BM25 + vector) RRF algorithm; vectorScore + textScore exposed alongside combined score
Temporal decay (hybrid search) Opt-in time-based scoring factor
MMR re-ranking Maximal marginal relevance for result diversity
LLM-based query expansion Expand FTS queries via LLM
OpenAI embeddings
Bedrock embeddings Reuses Bedrock region/profile auth for Titan Text Embeddings V2
Gemini embeddings gemini-embedding-2-preview with configurable output dimensions, automatic reindex on dim change
GitHub Copilot embeddings Provider with token refresh, payload validation, remote overrides
Ollama embeddings OpenClaw moved to /api/embed with batched input; per-host cache keys; non-batch concurrency knob
Local embeddings node-llama-cpp now optional install
Asymmetric embedding endpoints inputType/queryInputType/documentInputType for retrieval prefixes (Ollama: nomic-embed-text, qwen3-embedding, mxbai-embed-large)
SQLite-vec backend IronClaw uses PostgreSQL; bundled-plugin runtime-deps mirror sqlite-vec
LanceDB backend Configurable auto-capture max length; cloud storage support; OpenAI-compatible float embeddings, ZhiPu/DashScope normalization
QMD backend Multi-collection -c filters, --mask collection patterns, opt-in memory.qmd.update.startup
Active Memory plugin Memory sub-agent before main reply; partial recall on timeout; allowedChatIds/deniedChatIds; visible status fields
Memory wiki (people-aware) Canonical aliases, person cards, relationship graphs, privacy/provenance reports, search modes (find-person/route-question/source-evidence/raw-claim)
Dreaming (REM cycles) ## Light Sleep/## REM Sleep phase blocks; dreaming.storage.mode = "separate" default; dreaming.model override
recallMaxChars cap Bound recall embedding queries for small Ollama embedding models
corpus=sessions ranking Session transcript hits with visibility/agent-to-agent policy
Atomic reindexing
Embeddings batching embed_batch on EmbeddingProvider trait
Citation support
Memory CLI commands memory search/read/write/tree/status CLI subcommands
openclaw ltm list Real LanceDB LTM rows with --limit/createdAt ordering
Flexible path structure Filesystem-like API
Identity files (AGENTS.md, etc.)
Daily logs
Heartbeat checklist HEARTBEAT.md
Hybrid post-compaction reindex agents.defaults.compaction.postIndexSync; memorySearch.sync.sessions.postCompactionForce

Owner: Unassigned


11. Mobile Apps

Feature OpenClaw IronClaw Priority Notes
iOS app (SwiftUI) 🚫 - Out of scope initially
Android app (Kotlin) 🚫 - Out of scope initially
Apple Watch companion 🚫 - Send/receive messages MVP
Gateway WebSocket client 🚫 -
Camera/photo access 🚫 -
Voice input 🚫 -
Push-to-talk 🚫 -
Location sharing 🚫 -
Node pairing 🚫 -
APNs push notifications 🚫 - Wake disconnected nodes before invoke
Share to OpenClaw (iOS) 🚫 - iOS share sheet integration
Background listening toggle 🚫 - iOS background audio

Owner: Unassigned (if ever prioritized)


12. macOS App

Feature OpenClaw IronClaw Priority Notes
SwiftUI native app 🚫 - Out of scope
Menu bar presence 🚫 - Animated menubar icon
Bundled gateway 🚫 -
Canvas hosting 🚫 - Agent-controlled panel with placement/resizing
Voice wake 🚫 - Overlay, mic picker, language selection, live meter
Voice wake overlay 🚫 - Partial transcripts, adaptive delays, dismiss animations
Push-to-talk hotkey 🚫 - System-wide hotkey
Exec approval dialogs - TUI overlay
iMessage integration 🚫 -
Instances tab 🚫 - Presence beacons across instances
Agent events debug window - Operator-only prompt, activity, statistics, and bounded tool-detail inspector (?debug=true)
Sparkle auto-updates 🚫 - Appcast distribution

Owner: Unassigned (if ever prioritized)


13. Web Interface

Feature OpenClaw IronClaw Priority Notes
Control UI Dashboard - Web gateway with chat, memory, jobs, logs, extensions; modular Overview/Chat/Config/Agent/Session views, command palette, mobile bottom tabs
Channel status view 🚧 P2 Gateway status widget, full channel view pending
Agent management P3 Agent Tool Access panel with compact live-tool chips, collapsible groups, per-tool toggles
Model selection - TUI only
Config editing P3 Raw config pending-changes diff panel with redacted reveal
Debug/logs viewer - Real-time log streaming with level/target filters
WebChat interface - Web gateway chat with SSE/WebSocket; Reborn serves canonical SPA routes at /chat, /settings, and /extensions, while legacy /v2/* browser URLs temporarily redirect to root equivalents and /api/webchat/v2/* stays unchanged
Canvas system (A2UI) P3 Agent-driven UI, improved asset resolution; macOS canvas hosts pushed A2UI without auto-reload
Control UI i18n P3 English, Chinese, Portuguese; expanded with Persian (fa), Dutch (nl), Vietnamese (vi), Italian (it), Arabic (ar), Thai (th), Traditional Chinese (zh-TW)
WebChat theme sync P3 Sync with system dark/light mode
Partial output on abort P2 Preserve partial output when aborting
PWA + Web Push P3 PWA install (root-scope service worker) + Web Push notifications: the web-app channel delivers RFC 8030/8291/8292 browser pushes for automation notices and model-directed deliveries
Talk Mode (browser realtime voice) P3 OpenAI Realtime + Google Live WebSocket; Gateway-minted ephemeral secrets; backend realtime relay
Steer queued messages P3 Steer action on queued messages injects follow-up into active run without retyping
Quick Settings dashboard P3 Refreshed grid + presets + quick-create flows + assistant avatar overrides
Markdown preview dialog P3 Lazy markdown preview + @create-markdown/preview v2 system theme
Cron job dashboard P3 Cron prompts/run summaries as sanitized markdown
Personal identity (operator) P3 Browser-local operator name + avatar through shared chat/avatar path
Trajectory export UI P3 Owner-private export approval flow
Restart-impacting Dreaming confirm P3 Restart warning before applying Dreaming mode changes
Mobile chat settings sheet P3 Persists mobile state through Lit-managed view-state

Owner: Unassigned


14. Automation

Feature OpenClaw IronClaw Priority Notes
Cron jobs - Routines with cron trigger; runtime state split into jobs-state.json; sessionTarget: "current"/session:<id> bindings
Reborn scheduled trigger loop 🚧 P2 Reborn-native trigger persistence, backend parity, atomic fire claim/update APIs, poller core, caller-level harness, first-party trigger_* capabilities, and composition-owned worker lifecycle are in progress; automation panel runs now link canonical thread ids; trigger-owned threads are openable, watchable, approvable, and cancelable by automation owners via automation-visibility authorization; scoped pause/resume/rename/delete state transitions are available through first-party capabilities and WebUI v2 controls; first-class one-shot triggers (TriggerSchedule::Once, schedule.kind = once) are implemented (completion is derived from the schedule; the old year-pinned-cron + completion_policy workaround was removed); run-scoped source-inherited and explicit external result targets are durably sealed and revalidated before delivery, including paired Telegram DMs enumerated through the generic channel target registry; remaining follow-ups: legacy pre-fix rows without a stored thread_id remain unopenable, production readiness policy, active-run retention/tombstone semantics, and production jitter source selection
Per-job model fallback override P2 payload.fallbacks overrides agent-level fallbacks
Cron stagger controls P3 Default stagger for scheduled jobs
Cron finished-run webhook P3 Webhook on job completion
--thread-id cron CLI 🚧 P2 Telegram forum topic delivery for scheduled announcements
failureAlert.includeSkipped P3 Persistently skipped jobs alert without counting skips as exec errors
delivery.threadId (gateway cron schemas) P2 Telegram forum topics + threaded channel destinations
Cron nested lane P3 cron.maxConcurrentRuns applies to dedicated cron-nested lane; non-cron flows keep their own lane
Cron stuck-session timeout P3 Aborts/cleans timed-out isolated turns before recording timeout
Timezone support - Via cron expressions; --at honors local wall-clock time across DST
One-shot/recurring jobs - Manual + cron triggers; Reborn one-shot uses first-class TriggerSchedule::Once (schedule.kind = once); completion is derived from the schedule
Channel health monitor P2 Auto-restart with configurable interval
beforeInbound hook P2
beforeOutbound hook P2
beforeToolCall hook P2
before_agent_start hook P2 Model/provider override
before_agent_finalize hook P2 Run/message/sender/session/trace correlation
before_message_write hook P2 Pre-write interception
before_dispatch hook P2 Canonical inbound metadata; idempotency-key dedupe for hook agent deliveries
before_compaction/after_compaction P3 Codex-native compaction lifecycle
onMessage hook - Routines with event trigger
Structured system-event routines P2 system_event trigger + event_emit tool for event-driven automation
onSessionStart hook P2
onSessionEnd hook P2
transcribeAudio hook P3
transformResponse hook P2
llm_input/llm_output hooks P3 LLM payload inspection (Codex app-server included)
model_call_started/ended hooks P3 Metadata-only model/provider call telemetry
cron_changed hook P3 Typed gateway-owned cron lifecycle observer
Cron jobId hook context P3 Hook context carries originating job id
Bundled hooks P2 Audit + declarative rule/webhook hooks
Plugin hooks P3 Registered from WASM capabilities.json
Workspace hooks P2 hooks/hooks.json and hooks/*.hook.json; realpath-fail-closed
Outbound webhooks P2 Fire-and-forget lifecycle event delivery
Heartbeat system - Periodic execution; heartbeat.skipWhenBusy for nested lane pressure; deferred under cron load
Gmail pub/sub P3
Inferred follow-up commitments P3 Heartbeat-delivered reminders; opt-in batched extraction

State migration (v1/engine-v2 → Reborn) (historical — the migration crate crates/ironclaw_reborn_migration was deleted with the unified extension runtime reconcile, PR #6116; this paragraph records what it did and did not convert): the crate converted persisted automations. Cron routines and cron missions convert to Reborn TriggerRecords (mission threads land under ThreadScope.mission_id). Because Reborn's TriggerSourceKind is Schedule-only, event / system-event / webhook / manual routines and non-cron mission cadences have no TriggerRecord target and are recorded in the migration manifest rather than converted — even where the runtime supports the behavior via hooks/event_emit, the durable automation row does not carry over. Guardrails, notify config, run counters, routine_runs history (no public run-history insert), and mission-only fields (focus/approach/success-criteria) likewise have no target. The full mapping + gap catalog lived in the crate's CLAUDE.md; recover it from git history (git show f7da7dd7b^:crates/ironclaw_reborn_migration/CLAUDE.md).

Owner: Unassigned


15. Security Features

Feature OpenClaw IronClaw Notes
Gateway token auth Bearer token auth on web gateway; per-request resolution for secrets.reload; method-specific least-privilege scopes for CLI Gateway calls
Device pairing Single-use bootstrap setup codes; metadata-upgrade auto-approval for shared-secret loopback; scope/role/metadata pairing approval flows
Tailscale identity Tailscale-authenticated Control UI bypass for browser device identity
Trusted-proxy auth Header-based reverse proxy auth; trustedProxy.allowLoopback
OAuth flows 🚧 NEAR AI OAuth + Gemini OAuth (PKCE, S256) + hosted extension/MCP OAuth broker; external auth-proxy rollout still pending; OpenClaw added bootstrap-token redemption scope allowlist. Reborn serve now has browser SSO login for WebChat v2 (Google + GitHub; Google PKCE S256, state CSRF, cleartext-redirect guard), with fail-closed verified-email-domain admission and per-user identity binding (distinct OAuth identity → distinct user, stateless tenant-bound HMAC session). Local-dev trigger polling also seeds admitted WebUI SSO users into trigger-fire access when enabled
DM pairing verification ironclaw pairing approve, host APIs
Allowlist/blocklist 🚧 allow_from + pairing store; canonical dmPolicy="open" only with effective wildcard across all channels
Per-group tool policies Group-id validation against session/spawned context before applying group-scoped tool policies
Exec approvals TUI overlay; allow-once idempotent grace; PATH-resolved basenames; secret redaction in approval prompts; Unicode normalization + zero-width stripping
Owner allowlists commands.ownerAllowFrom bootstrapped from first approved DM pairing; channel-prefixed entries scoped to matching providers
TLS 1.3 minimum reqwest rustls
SSRF protection WASM allowlist; OpenClaw extended SSRF guard to BlueBubbles, Synology Chat, LINE, QQBot direct-upload, Tlon uploads, browser tabs/snapshots, voice-call Twilio webhooks, web fetch (incl. fc00::/7 opt-in)
SSRF IPv6 transition bypass block Block IPv4-mapped IPv6 bypasses
Cron webhook SSRF guard SSRF checks on webhook delivery
Loopback-first 🚧 HTTP binds 0.0.0.0
Docker sandbox Orchestrator/worker containers; opt-in sandbox.docker.gpus passthrough; Reborn defines a typed SandboxProcessPlan contract (ironclaw_sandbox) with plan validation only — no production execution backend is wired for it yet
Podman support --container accepts both Docker + Podman
WASM sandbox IronClaw innovation
Sandbox env sanitization 🚧 Shell tool scrubs env vars (secret detection); Reborn process sandbox rejects sensitive raw env values in plans and uses placeholders for brokered credentials, but production secure-capture and MITM transport wiring remain partial
OPENCLAW_* env block Untrusted workspace .env cannot inject OpenClaw runtime-control vars
Workspace .env injection blocks Block CLOUDSDK_PYTHON, ambient Homebrew, Windows system PATH vars, MINIMAX_API_HOST, npm_execpath
Tool policies
Elevated mode
Safe bins allowlist Hardened path trust; non-user-writable absolute helpers for CLI/ffmpeg/OpenSSL
LD*/DYLD* validation Block Mercurial/Rust/Make env redirects in host exec sanitization
Path traversal prevention Including config includes (OC-06) + workspace-only tool mounts; realpath-via-fd safety on agents.files.get/set
Credential theft via env injection 🚧 Shell env scrubbing + command injection detection; no full OC-09 defense
Session file permissions (0o600) Session token file set to 0o600 in llm/session.rs
Skill download path restriction Validated download roots prevent arbitrary write targets
Skill installer metadata validation Strict per-PM regex allowlists; URL protocol allowlist; sanitize metadata for terminal output
Webhook signature verification Padded timing-safe compare even on wrong-length signatures (Nextcloud Talk, Feishu, LINE, Zalo)
Media URL validation Reject non-HTTP(S) inbound attachment URLs; reject remote-host file:// URLs in webchat embedding path
Prompt injection defense Pattern detection, sanitization; OpenClaw added chat-template special-token stripping (Qwen/ChatML, Llama, Gemma, Mistral, Phi, GPT-OSS)
Internal scaffolding stripping <system-reminder>/<previous_response> stripped at final delivery boundary
Leak detection Secret exfiltration; complete and malformed private-key blocks within one message are bounded for safe value redaction, while cross-message matches fail closed
Dangerous tool re-enable warning Warn when gateway.tools.allow re-enables HTTP tools
OpenGrep static analysis Bundled rulepack + source-rule compiler + provenance check; PR/full scan workflows + SARIF upload to GitHub Code Scanning
Logging redaction expansion Tencent/Alibaba/HuggingFace/Replicate API keys; payment credential field names; sk-*/Bearer/Authorization tokens at console + file sinks
Trace context propagation W3C traceparent from trusted model-call context; replaces caller-supplied values
Forwarded-header IP detection Treat any Forwarded/X-Forwarded-*/X-Real-IP as proxied before pairing locality checks
Trusted-content sanitization Group/channel names rendered through fenced untrusted-metadata JSON; vCard/contact/location free-text neutralization
Per-tool MCP loopback policy Owner-only tool visibility derived from authenticated owner-vs-non-owner bearers; no caller-controlled owner header
Mobile pairing TLS requirement Plaintext ws:// only on loopback; OPENCLAW_ALLOW_INSECURE_PRIVATE_WS for trusted private nets
Webhook auth rate-limit Pre-auth 429 for bad webhook secrets (Zalo, etc.)

Owner: Unassigned


16. Development & Build System

Feature OpenClaw IronClaw Notes
Primary language TypeScript Rust Different ecosystems
Build tool tsdown cargo
Type checking TypeScript/tsgo rustc
Linting Oxlint clippy
Formatting Oxfmt rustfmt
Package manager pnpm cargo
Test framework Vitest built-in
Coverage V8 tarpaulin/llvm-cov
CI/CD GitHub Actions GitHub Actions
Pre-commit hooks prek - Consider adding
Docker: Chromium + Xvfb Optional browser in container
Docker: init scripts /openclaw-init.d/ support
Browser: extraArgs config Custom Chrome launch arguments

Owner: Unassigned


Implementation Priorities

P0 - Core (Already Done)

  • TUI channel with approval overlays
  • HTTP webhook channel
  • DM pairing (ironclaw pairing list/approve, host APIs)
  • WASM tool sandbox
  • Workspace/memory with hybrid search + embeddings batching
  • Prompt injection defense
  • Heartbeat system
  • Session management
  • Context compaction
  • Model selection
  • Gateway control plane + WebSocket
  • Web Control UI (chat, memory, jobs, logs, extensions, routines)
  • WebChat channel (web gateway)
  • Slack channel (WASM tool)
  • Telegram channel (WASM tool, MTProto)
  • Docker sandbox (orchestrator/worker)
  • Cron job scheduling (routines)
  • CLI subcommands (onboard, config, status, memory)
  • Gateway token auth
  • Skills system (prompt-based with trust gating, attenuation, activation criteria)
  • Session file permissions (0o600)
  • Memory CLI commands (search, read, write, tree, status)
  • Shell env scrubbing + command injection detection
  • Tinfoil private inference provider
  • OpenAI-compatible / OpenRouter provider support

P1 - High Priority

  • 🚧 Slack channel (real implementation): Slack mounts as a generic extension channel surface (the bespoke host-beta serve lane — serve_slack / with_slack_channel_routes — was removed) with Slack Events API signing, extension-card personal OAuth setup that binds Slack authed_user.id to the authenticated Reborn user, DM/app-mention routing through Product Workflow/Reborn, final-reply delivery, admin-managed allowed-channel picker, durable WebUI channel-route assignment APIs, provider-side default outbound target inventory for shared channels and explicitly provisioned personal DMs, and one unified host-bundled slack extension manifest (provider slack) declaring both the Slack channel surface and the user-scoped tool surfaces; DMs execute as the OAuth-bound actor, while shared channel turns route to allowed dynamic or static channel subjects and fail closed for unrouted channels in admin-managed mode; broader production install/setup hardening remains follow-up.
  • Telegram channel (WASM, polling-first setup, DM pairing, caption, /start)
  • WhatsApp channel
  • Multi-provider failover (FailoverProvider with retryable error classification)
  • Hooks system (core lifecycle hooks + bundled/plugin/workspace hooks + outbound webhooks)

P2 - Medium Priority

  • Media handling (images, PDFs)
  • Ollama/local model support (via rig::providers::ollama)
  • Configuration hot-reload
  • Tool-driven webhook ingress (/webhook/tools/{tool} -> host-verified + tool-normalized system_event routines)
  • Channel health monitor with auto-restart
  • Partial output preservation on abort

P3 - Lower Priority

  • Discord channel
  • Matrix channel
  • Other messaging platforms (Yuanbao, WeCom, Google Meet, Voice Call)
  • TTS/audio features (12+ providers added in OpenClaw; see Section 6 TTS/STT subsection)
  • Video support (OpenRouter/MiniMax/Veo/fal/Sora)
  • 🚧 Skills routing blocks (activation criteria exist, but no "Use when / Don't use when")
  • Plugin registry / persisted plugin index / git: installs
  • Streaming (block/tool/Z.AI tool_stream)
  • Memory: temporal decay, MMR re-ranking, query expansion, multimodal indexing, people-aware wiki
  • Control UI i18n (now 12+ locales upstream)
  • Stuck loop detection
  • Codex native app-server runtime + Computer Use
  • Talk Mode / realtime voice (browser + backend)
  • OpenTelemetry diagnostics + Prometheus exporter
  • Active Memory + Skill Workshop; 🚧 Trajectory export (caller-owned, redacted single-run export; full parity remains pending)
  • Outbound proxy routing + proxy validate
  • migrate (Claude/Hermes import)

How to Contribute

  1. Claim a section: Edit this file and add your name/handle to the "Owner" field
  2. Create a tracking issue: Link to GitHub issue for the feature area
  3. Update status: Change to 🚧 when starting, when complete
  4. Add notes: Document any design decisions or deviations

Coordination

  • Each major section should have one owner to avoid conflicts
  • Owners can delegate sub-features to others
  • Update this file as part of your PR

Deviations from OpenClaw

IronClaw intentionally differs from OpenClaw in these ways:

  1. Rust vs TypeScript: Native performance, memory safety, single binary distribution
  2. WASM sandbox vs Docker: Lighter weight, faster startup, capability-based security
  3. PostgreSQL + libSQL vs SQLite: Dual-backend (production PG + embedded libSQL for zero-dep local mode)
  4. NEAR AI focus: Primary provider with session-based auth
  5. No mobile/desktop apps: Focus on server-side and CLI initially
  6. WASM channels: Novel extension mechanism not in OpenClaw
  7. Tinfoil private inference: IronClaw-only provider for private/encrypted inference
  8. GitHub WASM tool: Native GitHub integration as WASM tool
  9. Prompt-based skills: Different approach than OpenClaw capability bundles (trust gating, attenuation)

These are intentional architectural choices, not gaps to be filled.