The gateway API endpoints (/api/engine/missions, etc.) call bridge
query functions that return empty results when the engine state hasn't
been initialized yet. Previously, initialization only happened lazily
on the first chat message via handle_with_engine().
Now when ENGINE_V2=true, the engine is initialized in Agent::run()
before channels start, so the self-improvement mission and other
engine state is available to gateway API endpoints immediately.
Also rename get_or_init_engine → init_engine and make it public so
it can be called from agent_loop.rs at startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add a full Missions page to the web gateway with list view, detail view,
and action buttons (Fire, Pause, Resume).
Backend: add /api/engine/missions/summary endpoint returning counts by
status (active/paused/completed/failed).
Frontend:
- New "Missions" tab between Jobs and Routines
- Summary cards showing mission counts by status
- Table with name, goal, cadence type, thread count, status, actions
- Detail view with goal, cadence, current focus, success criteria,
approach history, spawned thread list, and action buttons
- Fire/Pause/Resume actions with toast notifications
- i18n support (English + Chinese)
- CSS following the existing routines/jobs patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wire engine v2 into the full submission pipeline and expose threads,
projects, and missions through the web gateway REST API.
Bridge routing — route ExecApproval, Interrupt, NewThread, and Clear
submissions to engine v2 when ENGINE_V2=true. Previously only UserInput
and ApprovalResponse were handled; all other control commands fell
through to disconnected v1 sessions.
Bridge query layer — add 11 read-only query functions and 6 DTO types
so gateway handlers can inspect engine state (threads, steps, events,
projects, missions) without direct access to the EngineState singleton.
Gateway endpoints — new /api/engine/* routes:
GET /threads, /threads/{id}, /threads/{id}/steps, /threads/{id}/events
GET /projects, /projects/{id}
GET /missions, /missions/{id}
POST /missions/{id}/fire, /missions/{id}/pause, /missions/{id}/resume
SSE events — add ThreadStateChanged, ChildThreadSpawned, and
MissionThreadSpawned AppEvent variants. Expand the bridge event mapper
to forward StateChanged and ChildSpawned engine events to the browser.
Engine crate — add ConversationManager::clear_conversation() for /new
and /clear commands.
Code quality — replace 10 .expect() calls with proper error returns,
remove dead AgentConfig.engine_v2 field, log silent init errors, fix
duplicate doc comment, improve fallthrough documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(extensions): channel-relay auth dead-end, add observability and relay URL override
Fix a bug where clicking Activate on the Slack relay extension produces
a dead-end "Authentication required" error with no OAuth URL. The root
cause: `auth_channel_relay()` used `is_relay_channel()` to check auth
status, but that function returns true as soon as the extension is
*installed* (in-memory set), before OAuth completes. This short-circuits
the OAuth flow so the authorization URL is never offered.
Changes:
1. **Bug fix** — `auth_channel_relay()` now uses `has_stored_team_id()`
which only checks the persistent settings store for an actual team_id.
The extension list `authenticated` field uses the same check so the UI
accurately reflects OAuth completion status.
2. **Observability** — Added debug/warn/info tracing to all channel-relay
code paths that were previously silent on failure:
- `activate_channel_relay`: team_id retrieval, relay config, signing
secret fetch, hot_add, cache operations
- `auth_channel_relay`: auth check, OAuth initiation, nonce storage
- `extensions_activate_handler`: request entry, auth fallback flow
- `slack_relay_oauth_callback_handler`: team_id persistence (was
silently ignored with `let _`)
- `RelayClient`: initiate_oauth, get_signing_secret, proxy_provider
all log URL, status, and errors
- `has_stored_team_id`: store read success/failure
3. **Per-extension relay URL override** — Users can now override the
CHANNEL_RELAY_URL via Settings > Extensions > Reconfigure. Stored
under `extensions.{name}.relay_url` in settings. Both auth and
activate read this override before falling back to the env default.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review feedback — clear relay_url override and improve log message
1. Allow clearing the relay_url override: when an optional setup field
with a setting_path is submitted empty, delete the stored setting so
the system reverts to the env/default value. Previously empty values
were silently skipped, making it impossible to undo an override from
the UI.
2. Improve the OAuth callback team_id persistence error log to be
self-contained without referencing implementation details.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: collapse nested if per clippy::collapsible_if
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review feedback — security, scope consistency, and error handling
1. OAuth callback team_id persistence is now fatal: if set_setting fails,
the callback returns an error instead of proceeding to activate (which
would re-read from the store and fail anyway).
2. effective_relay_url uses owner scope (self.user_id) for reads, matching
configure() which writes under the same scope. Prevents multi-user
mismatch where an override saved via Reconfigure was invisible during
auth/activation.
3. has_stored_team_id uses owner scope for the same reason — the OAuth
callback stores team_id under state.owner_id (= self.user_id).
4. Security: effective_relay_url validates the override URL — only
http/https without embedded credentials (userinfo) is accepted. This
prevents API-key exfiltration if a user points relay_url at an
attacker-controlled host. Logs only host portion, not full URL.
5. Fixed effective_relay_url docstring to match behavior (returns Option,
callers handle the fallback).
6. get_setup_schema for ChannelRelay now logs a warning on settings store
errors instead of silently returning None.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling
Finishes the remaining isolation work from phases 2–4 of #59:
Phase 2 (DB scoping): Fix /status and /list commands to use _for_user
DB variants instead of global queries that leaked cross-user job data.
Phase 3 (Runtime isolation): Per-user workspace in routine engine's
spawn_fire so lightweight routines run in the correct user context.
Per-user daily cost tracking in CostGuard with configurable budget via
MAX_COST_PER_USER_PER_DAY_CENTS. Multi-user heartbeat that cycles
through all users with routines, auto-detected from GATEWAY_USER_TOKENS.
Phase 4 (Provider/tools): Per-user model selection via preferred_model
setting — looked up from SettingsStore on first iteration, threaded
through ReasoningContext.model_override to CompletionRequest. Works
with providers that support per-request model overrides (NearAI).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: use selected_model setting key to match /model command persistence
The dispatcher was reading "preferred_model" but the /model command
(merged from staging) persists to "selected_model". Since set_setting
is already per-user scoped, using the same key makes /model work as
the per-user model override in multi-tenant mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: heartbeat hygiene, /model multi-tenant guard, RigAdapter model override
Three follow-up fixes for multi-tenant isolation:
1. Multi-user heartbeat now runs memory hygiene per user before each
heartbeat check, matching single-user heartbeat behavior.
2. /model command in multi-tenant mode only persists to per-user
settings (selected_model) without calling set_model() on the shared
LlmProvider. The per-request model_override in the dispatcher reads
from the same setting. Added multi_tenant flag to AgentConfig
(auto-detected from GATEWAY_USER_TOKENS).
3. RigAdapter now supports per-request model overrides by injecting the
model name into rig-core's additional_params. OpenAI/Anthropic/Ollama
API servers use last-key-wins for duplicate JSON keys, so the override
takes effect via serde's flatten serialization order.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review — cost model attribution, heartbeat concurrency, pruning
Fixes from review comments on #1614:
- Cost tracking now uses the override model name (not active_model_name)
when a per-user model override is active, for accurate attribution.
- Multi-user heartbeat runs per-user checks concurrently via JoinSet
instead of sequentially, preventing one slow user from blocking others.
- Per-user failure counts tracked independently; users exceeding
max_failures are skipped (matching single-user semantics).
- per_user_daily_cost HashMap pruned on day rollover to prevent
unbounded growth in long-lived deployments.
- Doc comment fixed: says "routines" not "active routines".
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: /status ownership, model persistence scoping, heartbeat robustness
Addresses second round of PR review on #1614:
- /status <job_id> DB path now validates job.user_id == requesting user
before returning data (was missing ownership check, security fix).
- persist_selected_model takes user_id param instead of owner_id, and
skips .env/TOML writes in multi-tenant mode (these are shared global
files). handle_system_command now receives user_id from caller.
- JoinSet collection handles Err(JoinError) explicitly instead of
silently dropping panicked tasks.
- Notification forwarder extracts owner_id from response metadata in
multi-tenant mode for per-user routing instead of broadcasting to
the agent owner.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: cost pricing, fire_manual workspace, heartbeat concurrency cap
Round 3 review fixes:
- Cost tracking passes None for cost_per_token when model override is
active, letting CostGuard look up pricing by model name instead of
using the default provider's rates (serrrfirat).
- fire_manual() now uses per-user workspace, matching spawn_fire()
pattern (serrrfirat).
- Removed MULTI_TENANT env var — multi-tenant mode is auto-detected
solely from GATEWAY_USER_TOKENS presence (serrrfirat + Copilot).
- Multi-user heartbeat capped at 8 concurrent tasks to avoid flooding
the LLM provider (serrrfirat + Copilot).
- Fixed inject_model_override doc comment accuracy (Copilot).
- Added comment explaining multi-tenant notification routing priority
(Copilot).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* feat: user-scoped webhook endpoint for multi-tenant isolation
Adds POST /api/webhooks/u/{user_id}/{path} — a user-scoped webhook
endpoint that filters the routine lookup by user_id, preventing
cross-user webhook triggering when paths collide.
The existing /api/webhooks/{path} endpoint remains unchanged for
backward compatibility in single-user deployments.
Changes:
- get_webhook_routine_by_path gains user_id: Option<&str> param
- Both postgres and libsql implementations add AND user_id = ? filter
when user_id is provided
- New webhook_trigger_user_scoped_handler extracts (user_id, path)
from URL and passes to shared fire_webhook_inner logic
- Route registered on public router (webhooks are called by external
services that can't send bearer tokens)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* feat: add TenantCtx for compile-time tenant isolation
Implements zmanian's architectural proposal from #1614 review:
two-tier scoped database access (TenantScope/AdminScope) so handler
code cannot accidentally bypass tenant scoping.
TenantScope (default): wraps user_id + Arc<dyn Database>, auto-binds
user_id on every operation. ID-based lookups return None for cross-
tenant resources. No escape hatch — forgetting to scope is a compile
error.
AdminScope (explicit opt-in): cross-tenant access for system-level
components (heartbeat, routine engine, self-repair, scheduler, worker).
TenantCtx bundles TenantScope + workspace + cost guard + per-user
rate limiting. Constructed once per request in handle_message, threaded
through all command handlers and ChatDelegate.
Key changes:
- New src/tenant.rs (~920 lines): TenantScope, AdminScope, TenantCtx,
TenantRateState, TenantRateRegistry
- All command handlers: user_id: &str → ctx: &TenantCtx
- ChatDelegate: cost check/record/settings via self.tenant
- System components: store field changed to AdminScope
- Config: TENANT_MAX_LLM_CONCURRENT, TENANT_MAX_JOBS_CONCURRENT env vars
- Fixes bug: /status <job_id> cross-tenant leak (now auto-filtered)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* Fix REPL single-message hang and cap CI test duration
* Fix Clippy nested-if lint in REPL startup
* Fix single-message approval flow
* Handle empty single-message REPL exits
* Wait for one-shot event routines before exit
* feat(agent): thread per-tool reasoning from LLM through to REPL, HTTP, SSE, and DB
Add end-to-end agent reasoning summaries so users can see *why* the
agent chose specific tools, not just what it did.
- Add `reasoning: Option<String>` to `ToolCall` (all providers)
- Populate from LLM response content in `Reasoning::respond_with_tools`
and `select_tools`, with per-tool override when providers supply it
- Extend `Turn` with `narrative` and `TurnToolCall` with `rationale` +
`tool_call_id` for identity-based result matching
- Persist reasoning in DB via existing tool_calls JSON (no migration)
- Add `StatusUpdate::ReasoningUpdate` and `SseEvent::ReasoningUpdate` +
`SseEvent::JobReasoning` for real-time streaming
- Emit reasoning events in both chat dispatcher and worker job path
- Add `/reasoning [N|all]` command for inspecting turn reasoning
- Surface `narrative` and `rationale` in HTTP `/api/chat/history`
Based on the design from #361 and #456, reconstructed cleanly with
Option<String> to minimize blast radius (vs mandatory String that broke
compilation in #456).
Closes#456
Co-Authored-By: panosAthDBX <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review feedback from Gemini and Copilot
- Fix `_ => Ok(None)` in agent_loop.rs to avoid accidental shutdown
- Fix fallback in record_tool_result_for/record_tool_error_for to use
first pending call instead of last_mut (parallel execution safety)
- Include per-tool decisions in WASM channel reasoning messages
- Apply truncate_at_tool_tags + clean_response to shared_reasoning in
select_tools (parity with respond_with_tools)
- Persist turn-level narrative to DB in tool_calls JSON wrapper
- Parse both old (array) and new (object) tool_calls formats in
build_turns_from_db_messages for backward compatibility
- Populate reasoning from action.reasoning in execute_plan ToolCalls
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address second round of review comments + merge fixes
- Add reasoning: None to new github_copilot.rs ToolCall sites (from staging merge)
- Run cargo fmt on 4 files with formatting diffs
- Truncate narrative to 1000 chars before DB persistence
- Clone turn data and drop session lock in /reasoning command
- Extract ToolDecisionDto::from_json_array shared helper (deduplicate
worker/job.rs and orchestrator/api.rs)
- Add unit tests for wrapped tool_calls JSON format with narrative
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address third round of review comments (Copilot + serrrfirat)
- Reword ToolCall.reasoning docstring to reflect provider-supplied or
fallback contract
- Sanitize narrative through SafetyLayer before storage/emission
- Clean per-tool reasoning via truncate_at_tool_tags + clean_response
in select_tools (parity with shared reasoning)
- Convert 4 approval-path recording sites in thread_ops.rs to
identity-based record_tool_result_for/record_tool_error_for
- Preserve tool_call_id and reasoning through restore_from_messages
- Fix has_result/has_error to reject JSON null values
- Truncate tool_call_id to 128 chars before DB persistence
- Add 4 unit tests for record_tool_result_for/error_for edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address zmanian review — sanitize JobDelegate reasoning + warn on dropped results
- Sanitize narrative and per-tool rationale through SafetyLayer in
JobDelegate reasoning events (parity with ChatDelegate)
- Add tracing::warn when record_tool_result_for/error_for drops a
result because no matching or pending tool call exists
- Add 3 unit tests for reasoning normalization (thinking tags,
tool tags, empty-after-cleaning)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address 4 remaining unreplied review comments
- Clean per-tool reasoning in respond_with_tools via truncate_at_tool_tags
+ clean_response (parity with select_tools)
- Handle wrapped JSON format in rebuild_chat_messages_from_db so cold
hydration works after persist_tool_calls format change
- Update persist_tool_calls doc comment to describe new JSON shape
- Sanitize per-tool rationale through SafetyLayer in ChatDelegate before
emission and storage (parity with JobDelegate)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address zmanian review round 2
- Add tracing::debug on fallback-to-pending path in record_tool_result_for
and record_tool_error_for (item 1)
- Add comment explaining why /reasoning is special-cased in agent_loop.rs
(item 4)
- Items 2 (narrative persistence), 3 (rationale sanitization), and 5
(catch-all fix) were already addressed in prior commits
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: ensure LLM calls always end with user message (closes#763)
Claude 4.6 models (claude-sonnet-4-6, claude-opus-4-6) no longer support
assistant message prefill — any LLM call where the conversation ends on an
assistant message is rejected with HTTP 400 "This model does not support
assistant message prefill".
The same root cause also triggers NEAR AI's "No user query found in messages"
400 error for the routine engine path.
Two fixes:
1. src/worker/container.rs — before_llm_call()
After poll_and_inject_prompt(), if no user follow-up arrived and
handle_text_response() left an assistant message at the end of the
conversation, inject a sentinel "Continue." user message before
the next LLM call.
2. src/agent/routine_engine.rs — execute_lightweight_with_tools()
Before the force_text final completion call, ensure messages end
with a user-role message. Tool result messages (Role::Tool) satisfy
Anthropic but not NEAR AI; assistant messages satisfy neither.
Also updates the worker system prompt to instruct the agent to include
the phrase "The job is complete" in its final message, so the agentic
loop can detect termination reliably.
Tested with claude-sonnet-4-6 and claude-opus-4-6.
Workaround: ANTHROPIC_MODEL=claude-sonnet-4-20250514 (still supports prefill).
* fix: broaden sentinel guard to any non-user message (per review)
Gemini suggested the Role::Assistant check in before_llm_call() is too
specific. Changed to !Role::User to match the routine_engine.rs fix and
cover tool results too.
* fix: address zmanian review — JobDelegate sentinel, shared helper, NearAI complete() flattening
- Extract ensure_ends_with_user_message() to src/util.rs with 4 unit tests
(empty list, after assistant, after tool result, no-op when already user)
- Add sentinel guard to JobDelegate::before_llm_call() in src/worker/job.rs
so scheduler jobs (CreateJob / /job path) no longer hit Claude 4.6 / NEAR AI 400s
- Replace inline guards in ContainerDelegate and routine_engine.rs with the
shared helper — all 3 call sites now use one implementation
- Fix complete() in nearai_chat.rs to apply flatten_tool_messages when
flatten_tool_messages=true — previously only complete_with_tools() flattened,
so force_text paths could still send role:"tool" messages to NEAR AI
- Update stale comment in container.rs: "assistant message" → "non-user message"
- Add flatten tests in nearai_chat.rs covering the complete() path
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* ci: fix fmt and tar advisory
---------
Co-authored-by: Jacob Lasky <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
Wire the self-improvement loop as a Mission with OnSystemEvent cadence,
inspired by karpathy/autoresearch's program.md approach. The mission
fires when threads complete with issues, receives trace data as trigger
payload, and uses tools directly to diagnose and fix problems.
Key changes:
Engine self-improvement (Phase A+B from design doc):
- Add fire_on_system_event() to MissionManager for OnSystemEvent cadence
- Add start_event_listener() that subscribes to thread events and fires
matching missions when non-Mission threads complete with trace issues
- Add ensure_self_improvement_mission() with autoresearch-style goal
prompt (concrete loop steps, not vague instructions)
- Add process_self_improvement_output() for structured JSON fallback
- Seed fix pattern database with 8 known patterns from debugging
- Runtime prompt overlay via MemoryDoc (build_codeact_system_prompt now
async + Store-aware, appends learned rules from prompt_overlay docs)
- Pass Store to ExecutionLoop for overlay loading
Bridge review fixes (P1/P2):
- Scope engine v2 SSE events to requesting user (broadcast_for_user)
- Per-user pending approvals via HashMap instead of global Option
- Reset tool-call limit counter before each thread execution
- Only persist auto-approval when user chose "always", not one-off "yes"
- Remove dead store/mission_manager fields from EngineState
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: extract AppEvent to crates/ironclaw_common
SseEvent was defined in src/channels/web/types.rs but imported by 12+
modules across agent, orchestrator, worker, tools, and extensions — it
had become the application-wide event protocol, not a web transport
concern.
Create crates/ironclaw_common as a shared workspace crate and move the
enum there as AppEvent. Also move the truncate_preview utility which
was similarly leaked from the web gateway into agent modules.
- New crate: crates/ironclaw_common (AppEvent, truncate_preview)
- Rename SseEvent → AppEvent, from_sse_event → from_app_event
- web/types.rs re-exports AppEvent for internal gateway use
- web/util.rs re-exports truncate_preview
- Wire format unchanged (serde renames are on variants, not the enum)
Aligned with the event bus direction on refactor/architectural-hardening
where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: add AppEvent::event_type() helper, deduplicate match blocks
Address Gemini review: extract the variant→string match into a single
method on AppEvent, replacing the duplicated 22-arm matches in sse.rs
and types.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: rename leftover sse vars/tests to match AppEvent rename
Address Copilot review: rename sse_event vars to app_event in
orchestrator/api.rs and ws.rs, rename test functions from
test_ws_server_from_sse_* to test_ws_server_from_app_event_*, and
update stale SSE comments.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: add Deserialize to AppEvent, round-trip test, fix stale comments
Address zmanian review:
- Add Deserialize derive to AppEvent so downstream consumers can
deserialize incoming events
- Add event_type_matches_serde_type_field test that round-trips every
variant through serde and asserts event_type() matches the serialized
"type" field — catches drift between serde renames and the manual match
- Add round_trip_deserialize test for basic Serialize/Deserialize parity
- Update remaining "SSE" references in comments across server.rs,
manager.rs, ws_gateway_integration.rs, and worker/job.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(cli): show credential auth status in `tool info`
`ironclaw tool info` now checks the secrets store and shows whether
each required credential is configured or missing, consolidated into
a single Auth section that deduplicates across http.credentials,
auth, and setup.required_secrets. Secrets already shown in Auth are
filtered from the Secrets section to avoid redundancy.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): address review feedback on tool info auth status
- Fix clippy collapsible-if by using `if let` + `&&`
- Use HashMap<String, usize> for O(1) dedup instead of HashSet + linear scan
- Add --user flag to `tool info` for checking non-default user credentials
- Show "? unknown" on secrets store errors instead of silently reporting missing
- Surface secrets store init failure via eprintln instead of silent .ok()
- Sort auth entries by secret name for deterministic output
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): only filter secrets when auth section renders, add regression test
When the secrets store fails to initialize, the Auth section is not
rendered. Previously, secret names were still filtered from the Secrets
section, causing credential names to disappear entirely. Now secrets
are only filtered when the Auth section will actually be displayed.
Adds test verifying auth secret deduplication across auth, setup, and
http.credentials sections, plus secrets store existence checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(cli): extract collect_auth_secrets helper, always render Auth section
Address review feedback:
- Extract dedup logic into `collect_auth_secrets()` so the test exercises
the same code path as production (not a re-implementation)
- Always render the Auth section when auth secrets exist, showing
"? unknown" status when the secrets store is unavailable instead of
hiding credential names entirely
- Lazily init secrets store only when capabilities contain auth secrets,
avoiding spurious warnings for tools with no auth
- Add test for empty capabilities edge case
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style(cli): move HashMap/HashSet imports to top of file
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): use correct tagged JSON format for credential location in test
The CredentialLocationSchema uses serde tagged enum format
({"type": "bearer"}), not a bare string ("AuthorizationBearer").
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
When the model calls routine_create, routine_list, routine_fire,
routine_pause, routine_resume, or routine_delete, the bridge now
routes them to the MissionManager instead of blocking with an error.
Mapping:
routine_create → mission_create (with cadence parsing)
routine_list → mission_list
routine_fire → mission_fire
routine_pause → mission_pause
routine_resume → mission_resume
routine_update → mission_pause/resume (based on params)
routine_delete → mission_complete (marks as done)
Routine tools removed from v1-only blocklist and restored in
available_actions(). The model can use either "routine" or "mission"
vocabulary — both work.
Still blocked: create_job, cancel_job, build_software (need v1
Scheduler/ContainerJobManager refs).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: remove stale stream_token gate from channel-relay activation
The relay architecture now uses instance-scoped bearer auth + webhook
callbacks, not streaming. The `relay:<name>:stream_token` secret was
never written by the current OAuth flow, so activation always failed
with AuthRequired.
Replace stream_token with the team_id setting (already stored by the
OAuth callback) as the persistent "auth completed" marker:
- is_relay_channel(): check team_id setting instead of stream_token secret
- activate_channel_relay(): gate on team_id emptiness, not stream_token
- removal flow: delete team_id setting + oauth_state secret
- configure(): return empty allowed-secrets set (relay is OAuth-only)
- configure_token(): return AuthRequired (no manual token entry)
- list(): surface activation_error for relay channels (was hardcoded None)
- Clean up stale comments referencing stream_token / "stored token"
- Update test to match OAuth-only model (no secrets to pass)
Made-with: Cursor
* fix: address CI and review feedback
- Fix pre-existing tunnel/mod.rs test compilation (missing GatewayConfig
fields: memory_layers, user_tokens, workspace_read_scopes)
- Log warnings on failed team_id/oauth_state cleanup during removal
instead of silently ignoring errors (gemini review)
- Also delete legacy stream_token secret during removal for backward
compatibility with pre-webhook installs (codex review)
Made-with: Cursor
Missions are now callable from CodeAct Python code:
```python
# Create a daily briefing mission
result = mission_create(
name="Tech News",
goal="Daily AI/crypto/software news briefing",
cadence="0 9 * * *"
)
# List all missions
missions = mission_list()
# Manually fire a mission
mission_fire(id="...")
# Pause/resume
mission_pause(id="...")
mission_resume(id="...")
```
Implementation:
- MissionManager created on engine init, cron ticker started
- EffectBridgeAdapter intercepts mission_* function calls before tool
lookup and routes to MissionManager
- parse_cadence() handles: "manual", cron expressions, "event:pattern",
"webhook:path"
- Mission functions documented in CodeAct system prompt
- MissionManager set on adapter via set_mission_manager() after init
(avoids circular dependency)
System prompt updated with mission_create, mission_list, mission_fire,
mission_pause, mission_resume documentation.
151 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): case-insensitive channel match and user_id filter for event triggers (#1051, #1076)
Event-triggered routines had two bugs preventing them from firing:
1. Channel comparison was case-sensitive (e.g., "Telegram" != "telegram"),
while emit_system_event already used eq_ignore_ascii_case. Fixed to match.
2. No user_id scoping — routines from any user were evaluated against every
message. Added ownership check so routines only fire for their owner's
messages.
Also adds periodic event cache refresh (every ~60s) in the cron ticker so
web/CLI mutations are picked up without requiring the tool path. Upgrades
skip-reason logging from trace to debug for debuggability.
Closes#1051
Refs #1076
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: correct refresh_every from 6 to 4 to match 15s default interval
The default cron_check_interval_secs is 15s, not 10s. With refresh_every=6,
the cache would refresh every 90s instead of the intended ~60s. Fix to 4
ticks (4 * 15s = 60s).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): address #1211 review -- extract routine_matches_message, fix refresh interval
Extract user/channel filter logic from check_event_triggers into a
standalone pure function routine_matches_message(). Rewrite tests to
call this function directly with controlled Routine and IncomingMessage
values, so they exercise the real code path and would catch a revert.
Add test_no_channel_filter_matches_any_channel for the None channel case.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* ci: re-trigger CI with latest changes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing IncomingMessage fields in test helper
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(agent): address review -- time-based refresh, trace-level user mismatch, scope guard (#1211)
- Use tokio::time::Instant for cache refresh instead of tick counting
- Downgrade user-mismatch log to trace to reduce noise
- Add early return false for non-Event triggers in routine_matches_message
- Fix doc comment to say 'user scope' instead of 'message sender'
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: run cargo fmt on agent_loop.rs
https://claude.ai/code/session_01ABGWibdKVQ3b6pEKtxPPkM
* fix(agent): resolve clippy warnings for unused binding and needless borrow
Fix unused `content` variable in event trigger guard (use `content: _`)
and remove redundant `&` on `message` which was already a reference.
https://claude.ai/code/session_01PzBK21BbUAuZbrfLpoz4Xb
* fix(test): update check_event_triggers call sites to new single-arg signature
The staging merge brought e2e_routine_heartbeat tests that still used
the old 3-argument check_event_triggers(user_id, channel, content)
signature. Updated all 11 call sites to pass &IncomingMessage directly.
[skip-regression-check]
https://claude.ai/code/session_012GrkTDrtDFkpJos2hkgTcE
* fix(agent): address review feedback on event trigger handling
- Use post-hook content for event trigger matching so BeforeInbound
hooks that rewrite input are respected
- Set MissedTickBehavior::Skip on cron ticker to avoid burst catch-up
after delays
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
* fix(routines): normalize status display across web and CLI surfaces (#1319)
- Use Display (lowercase) instead of Debug (PascalCase) for RunStatus serialization in web handler
- Update JavaScript status class mapping to match lowercase values from the API
- Enrich CLI `routines list` to show running/attention states by querying last run status
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(routines): address review -- batch last-run query, consistent status, simplify ternary (#1319)
- Parallelize last-run lookups with join_all to avoid N+1 sequential queries
- Normalize status in /api/routines/{id}/runs handler to match lowercase convention
- Remove redundant 'running' check in app.js runStatusClass logic
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(db): replace N+1 last-run-status queries with batch method
The CLI routines list was firing a separate list_routine_runs query per
routine to determine each one's last run status. For large routine sets
this overwhelms the connection pool.
Add batch_get_last_run_status to the Database trait with implementations
for both PostgreSQL (DISTINCT ON + ORDER BY) and libSQL (correlated
subquery + in-memory filter). Update the CLI to call the batch method
once instead of N times.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Three changes to make engine v2 visible in the web gateway:
1. SSE event streaming (AppEvent broadcast):
- ThreadEvent → AppEvent conversion via thread_event_to_app_event()
- Events broadcast to SseManager during the poll loop
- Covers: Thinking, ToolCompleted (success/error), Status, Response
- Web gateway receives real-time progress without any gateway changes
2. Conversation persistence to v1 database:
- After thread completes, writes user message + agent response to
v1 ConversationStore via add_conversation_message()
- Uses get_or_create_assistant_conversation() for per-user per-channel
- Web gateway reads from DB as usual — chat history appears
3. Final response broadcast:
- AppEvent::Response with full text + thread_id sent via SSE
- Web gateway renders the response in the chat UI
New EngineState fields: sse (Option<Arc<SseManager>>),
db (Option<Arc<dyn Database>>). Both populated from Agent.deps.
Agent.deps visibility widened to pub(crate).
Depends on: ironclaw_common crate with AppEvent type (PR #1615).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tunnel): target webhook server port instead of gateway port
start_managed_tunnel() always used the gateway port (3000) for the
tunnel target. Webhook routes live on the webhook server (HTTP_PORT,
default 8080), not the gateway. The old code never read
config.channels.http — no configuration could work around this.
Extracts resolve_tunnel_target() with regression tests.
* fix(tunnel): prevent SIGPIPE and fix default port fallback
Two fixes for managed tunnel subprocess lifetime:
1. After extracting the public URL from stdout/stderr, the pipe reader
was dropped (Rust ownership). The tunnel binary's next log write hit
the closed pipe and got SIGPIPE — killing it silently. Fix: drain
pipes in background tasks stored in TunnelProcess. Storing without
reading isn't enough — the OS pipe buffer fills up and the process
blocks instead.
2. When neither HTTP_PORT nor gateway is configured, the tunnel fell
back to 127.0.0.1:3000. But the webhook server defaults to
0.0.0.0:8080 in this case. Now the tunnel matches that fallback.
Affects ngrok (stdout), cloudflare (stderr), and custom (stdout).
Tailscale uses a daemon and is not affected by SIGPIPE.
* fix(tunnel): simplify drain loops and suppress CI false positives
Simplify `while let Ok(Ok(Some(line)))` drain pattern to
`while let Ok(Some(line))` — the extra Ok wrapper was unnecessary.
Add `// safety: test-only` to assert_eq! lines in test module to
suppress the "No panics in production code" CI check which greps
the diff without understanding Rust's #[cfg(test)] module boundaries.
---------
Co-authored-by: firat.sertgoz <[email protected]>
Merges refactor/extract-app-event-to-ironclaw-common which extracts
SseEvent into crates/ironclaw_common as AppEvent. This is the
prerequisite for engine v2 gateway integration — the bridge can now
emit AppEvents without depending on web gateway types.
Conflict resolution: workspace members includes all three crates
(ironclaw_common, ironclaw_safety, ironclaw_engine).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
SseEvent was defined in src/channels/web/types.rs but imported by 12+
modules across agent, orchestrator, worker, tools, and extensions — it
had become the application-wide event protocol, not a web transport
concern.
Create crates/ironclaw_common as a shared workspace crate and move the
enum there as AppEvent. Also move the truncate_preview utility which
was similarly leaked from the web gateway into agent modules.
- New crate: crates/ironclaw_common (AppEvent, truncate_preview)
- Rename SseEvent → AppEvent, from_sse_event → from_app_event
- web/types.rs re-exports AppEvent for internal gateway use
- web/util.rs re-exports truncate_preview
- Wire format unchanged (serde renames are on variants, not the enum)
Aligned with the event bus direction on refactor/architectural-hardening
where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Staging merge changed execute_tool_with_safety to take params by value
instead of by reference (perf optimization from PR #926). Updated
bridge adapter to clone params before passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): persist /model selection to .env, TOML, and DB
The /model command only wrote selected_model to the DB and config.toml,
but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest
priority in LlmConfig::resolve_model(). The .env value was never
updated, so it always shadowed the new model on restart.
Now persist_selected_model updates all three persistence layers:
1. The backend-specific model env var in ~/.ironclaw/.env (only if the
var already exists, to avoid injecting new vars)
2. The config.toml file (created if absent, since TOML > DB priority)
3. The DB settings table (for completeness)
Also adds diagnostic logging when the DB store is unavailable.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): address PR review — backend from deps, exact .env match
Review feedback:
- Use resolved llm_backend from AgentDeps instead of re-reading from
disk/env (fixes DB-only backend detection, eliminates redundant I/O)
- Match .env var with exact "KEY=" prefix and skip commented lines
(prevents false matches on NEARAI_MODEL_VERSION etc.)
- TOML is now loaded once (no double-read for backend + model update)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Replaces InMemoryStore with HybridStore:
- Ephemeral data (threads, steps, events, leases) stays in-memory
- MemoryDocs (lessons, specs, playbooks from reflection) persist to
the workspace at engine/docs/{type}/{id}.json
On engine init, load_docs_from_workspace() reads existing docs back
into the in-memory cache. This means:
- Lessons learned in session 1 are available in session 2
- The RetrievalEngine injects relevant past lessons into new threads
- The engine genuinely improves over time as reflection accumulates
Workspace paths:
engine/docs/lessons/{uuid}.json
engine/docs/specs/{uuid}.json
engine/docs/playbooks/{uuid}.json
engine/docs/summaries/{uuid}.json
engine/docs/issues/{uuid}.json
No new database tables. Uses existing workspace write/read/list.
workspace() accessor widened to pub(crate).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): add missing description, parameters, and improve credential prompts
Silence three categories of startup warnings emitted by
CapabilitiesFile::validate() and WasmToolLoader:
1. "description" field missing → add tool descriptions to all manifests
2. "parameters" field missing → add action-enum parameter schemas
3. Short credential prompts (<30 chars) → append source URLs
Affects: github, gmail, google-calendar, google-docs, google-drive,
google-sheets, google-slides, slack, telegram, llm-context, feishu.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(tools): auto-compact WASM tool schemas from module exports
Replace the manual `parameters` field in capabilities JSON with automatic
schema compaction. WasmToolSchemas::compact_schema() derives a compact
advertised schema from the WASM module's schema() export by keeping only
required and enum-constrained properties. The full schema remains
available via tool_info(detail: "schema").
This eliminates:
- The `parameters` field from CapabilitiesFile and all 11 sidecar JSONs
- The "missing parameters" startup warning from the loader
- Manual maintenance of duplicate schema data
The `description` field in capabilities JSON is retained.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tests): remove cap_file.parameters reference in test_rig
The parameters field was removed from CapabilitiesFile in the previous
commit. Update test_rig.rs to match — schema is now auto-compacted from
the WASM module export, no sidecar override needed.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): handle oneOf schemas in compact_schema, add tool name to warning
Address PR review feedback:
- compact_schema now collects properties from oneOf/anyOf/allOf variants,
fixing GitHub-style schemas that have no top-level properties
- Use HashSet for required lookup instead of Vec::contains
- Add tool name to "Capabilities file not found" warning for consistency
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): merge oneOf const values into enum, cap property collection
Address review feedback from @serrrfirat:
1. Merge const values across oneOf variants into a single enum array,
so the LLM sees all valid actions (not just the first variant's const).
2. Cap property collection at 100 to bound allocations.
3. Also keep properties with const constraint (single-variant case).
4. Update doc comment to describe variant collection and design choices
around variant-level required fields.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
"engine v2: initializing" and "engine v2: handling message" were
printing at INFO level, corrupting the REPL UI. All router logging
now uses debug! — only visible with RUST_LOG=ironclaw=debug.
Zero info! calls remain in crates/ironclaw_engine/ or src/bridge/.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Adds a complete approval flow that mirrors v1 behavior, using the
existing v1 security controls (Tool::requires_approval, auto-approve
sets, StatusUpdate::ApprovalNeeded).
## How it works
### Step 1: Tool blocked at execution
When the LLM's code calls a tool (e.g., `shell("ls")`):
1. EffectBridgeAdapter.execute_action() looks up the Tool object
2. Calls tool.requires_approval(¶ms) — returns ApprovalRequirement
3. If Always → EngineError::LeaseDenied (always blocks)
4. If UnlessAutoApproved → checks auto_approved HashSet → if not in set,
returns EngineError::LeaseDenied
5. If Never → proceeds to execution
### Step 2: Engine returns NeedApproval
The LeaseDenied error propagates through:
- CodeAct path: becomes Python RuntimeError, code halts, thread returns
NeedApproval with action_name + parameters
- Structured path: same via ActionResult.is_error
### Step 3: Router stores pending approval
- PendingApproval { action_name, original_content } stored on EngineState
- StatusUpdate::ApprovalNeeded sent to channel (shows approval card in
CLI/web with tool name, parameters, yes/always/no buttons)
- Returns text: "Tool 'shell' requires approval. Reply yes/always/no."
### Step 4: User responds
handle_message() intercepts Submission::ApprovalResponse when ENGINE_V2:
- 'yes' → auto_approve_tool(name) on EffectBridgeAdapter, re-processes
original message (tool now passes the approval check on second run)
- 'always' → same + logs for session persistence
- 'no' → returns "Denied: tool was not executed."
### Key design choice
Instead of pausing/resuming mid-execution (which needs engine changes
to freeze/restore the Monty VM state), we auto-approve the tool and
re-run the full message. The EffectBridgeAdapter's auto_approved set
persists across runs, so the second execution passes immediately.
This trades one extra LLM call for zero engine modifications.
## Files changed
- src/bridge/router.rs: PendingApproval struct, handle_approval(),
NeedApproval → StatusUpdate::ApprovalNeeded conversion
- src/bridge/mod.rs: export handle_approval
- src/agent/agent_loop.rs: intercept ApprovalResponse for engine v2
- src/bridge/effect_adapter.rs: fmt fixes
151 tests passing, clippy + fmt clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* feat: multi-tenant auth with per-user scoping
Multi-user authentication and authorization for IronClaw gateway:
- Token-based auth mapping tokens to user IDs via GATEWAY_USER_TOKENS
- Per-user SSE broadcast scoping
- Per-user rate limiting with poisoned lock recovery
- Handler auth and ownership checks for jobs, settings, routines
- Extension secrets scoped per-user
- Chat handlers use authenticated identity
- Reverse proxy deployment documentation
- Comprehensive integration tests for auth, SSE, rate limiting, and job isolation
* fix: scope memory tools per-user in multi-tenant mode
Memory tools (search, write, read, tree) held a single workspace
created at startup with GATEWAY_USER_ID. In multi-tenant mode, all
users' tool calls searched the default user's scope.
Add WorkspaceResolver trait that resolves workspaces per-request using
JobContext.user_id. In single-user mode, returns the startup workspace.
In multi-tenant mode (GATEWAY_USER_TOKENS configured), creates and
caches per-user workspaces on demand.
Includes regression tests for workspace resolution and user isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: comprehensive multi-tenant isolation audit
Address all review findings from @serrrfirat plus 7 additional gaps
found via full security audit:
Reviewer findings (5):
- WorkspacePool now applies search config, memory layers, embedding
cache, identity read scopes, and global config scopes (was bare)
- jobs_summary_handler uses per-user queries instead of global counters
- jobs_prompt_handler restructured to not 404 agent jobs + ownership check
- jobs_restart_handler agent branch now verifies user ownership
- agent_job_summary_for_user added to Database trait + both backends
Audit findings (7):
- Delete dead handlers/memory.rs (stale copies with no auth)
- Add AuthenticatedUser to logs_events, logs_level_get, logs_level_set
- Add AuthenticatedUser to extensions_tools_handler, gateway_status_handler
- Add auth + ownership checks to all 6 routines handlers
- Add auth to all 4 skills handlers with audit logging on mutations
- Scope extension setup SSE broadcast to user (broadcast_for_user)
- Fix pre-existing test compilation errors in extensions/manager.rs
17 new multi-tenant isolation tests covering:
- WorkspacePool config propagation and scope merging
- Jobs handler per-user isolation (summary, restart, prompt, cancel)
- Routines handler auth enforcement and cross-user rejection
- Auth middleware enforcement on logs, skills, status endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: second-pass multi-tenant audit — scope SSE broadcasts, DB queries, dead handlers
Second audit pass applying learned patterns across the codebase:
- OAuth callback SSE broadcasts now use broadcast_for_user (lines 773, 912)
- jobs_list_handler uses list_agent_jobs_for_user instead of fetching
all users' jobs and filtering in Rust
- list_agent_jobs_for_user added to Database trait + postgres + libsql
- Dead handler files (extensions.rs, static_files.rs) hardened with
AuthenticatedUser to prevent auth regression if migrated
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review findings — token hashing, broadcast scoping, error handling
Security fixes:
- Hash tokens with SHA-256 at construction time so authentication
compares fixed-size 32-byte digests, eliminating length-oracle
timing leaks
- Scope auth SSE broadcasts per-user in chat_auth_token_handler —
AuthRequired/AuthCompleted events were leaking across tenants
- Propagate DB errors in restart handlers instead of silently
swallowing via `if let Ok(Some(...))` pattern
Code quality:
- Log SSE serialization failures instead of silently producing empty
strings via unwrap_or_default()
- Remove dead `pub type AuthState = MultiAuthState` alias
- Replace `.unwrap()` with `Arc::clone(db)` in app.rs multi-tenant
workspace setup (db is guaranteed Some in context, but unwrap
violates project convention)
- Fix telegram setup test to inject UserIdentity into request
extensions (handler now requires AuthenticatedUser)
- Add safety comments on test-only expect/unwrap calls for CI
- Apply cargo fmt to fix pre-existing formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review findings — unify workspace pool, fix SSE regression, cache job owners
- Unify WorkspacePool and PerUserWorkspaceResolver: WorkspacePool now
implements WorkspaceResolver, eliminating duplicate per-user workspace
construction logic. app.rs uses WorkspacePool directly.
- Fix sse_tx: None scheduler regression: change scheduler/worker SSE
broadcasting from broadcast::Sender<SseEvent> to Arc<SseManager>,
restoring SSE event delivery for scheduled agent jobs.
- Cache job owner in orchestrator: add job_owner_cache to
OrchestratorState so job_event_handler avoids a DB round-trip on
every event after the first per job.
- Deduplicate ext_user_id computation in main.rs.
- Remove unused _gateway_state variable.
- Fix pre-existing test: first_token() returns None in multi-user mode
by design; align test assertion.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: fix formatting in app.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: extract memory handlers back into handlers/memory.rs
Move memory API handlers out of server.rs into their own module,
consistent with how jobs, routines, and skills handlers are organized.
The resolve_workspace() helper moves with them since it is only used
by memory handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: [email protected] <[email protected]>
Zero engine crate changes. All security controls enforced at the bridge
boundary in EffectBridgeAdapter:
1. Tool approval (v1: Tool::requires_approval):
- Checks each tool's approval requirement with actual params
- Always → returns EngineError::LeaseDenied (blocks execution)
- UnlessAutoApproved → checks auto_approved set, blocks if not approved
- Never → proceeds
- Per-session auto_approved HashSet (for future "always" handling)
2. Hook interception (v1: BeforeToolCall):
- Runs HookEvent::ToolCall before every execution
- HookOutcome::Reject → blocks with reason
- HookError::Rejected → blocks with reason
- Hook errors → fail-open (logged, execution continues)
3. Output sanitization (v1: sanitize_tool_output + wrap_for_llm):
- Leak detection: API keys in tool output are redacted
- Policy enforcement: content policy rules applied
- Length truncation: output capped at 100KB
- XML boundary protection: prevents injection via tool output
4. Sensitive param redaction (v1: redact_params):
- Tool's sensitive_params() consulted before hooks see parameters
- Redacted params sent to hooks, original params used for execution
5. available_actions() now sets requires_approval based on each tool's
default approval requirement, so the engine's PolicyEngine can
gate tools it hasn't seen before.
6. Actual execution timing measured via Instant::now() (replaces
placeholder Duration::from_millis(1)).
Accessor visibility: hooks() widened to pub(crate).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add Mission type and MissionManager for recurring thread scheduling
- Add ReliabilityTracker for per-capability success/failure/latency tracking
- Add reflection executor that spawns CodeAct threads for post-completion reflection
- Extend PolicyEngine with provenance-aware taint checking (LLM-generated data
requires approval for financial/external-write effects)
- Extend Store trait with mission CRUD methods
- Add conversation surface tracking, compaction token fix, context memory injection
- Wire new modules through lib.rs re-exports and bridge adapters
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Default new lightweight routines to tools-enabled
* Fix fmt and clippy on lightweight routine PR
* Use grouped execution field in routine no-tools fixture
* Align CLI routine defaults with tools-enabled lightweight mode
* feat(cli): add ironclaw models subcommands (list/status/set/set-provider)
Implements model management CLI (part of #83):
- `models list [provider] [--verbose] [--json]` — list providers; fetches
live model list from the provider API when a specific provider is given
- `models status [--json]` — show active provider/model
- `models set <model>` — set default model with validation
- `models set-provider <id> [--model <name>]` — set provider with alias
normalization
- fix conflicts
* fix(deps): update tar to 0.4.45 (RUSTSEC-2026-0067, RUSTSEC-2026-0068)
---------
Co-authored-by: firat.sertgoz <[email protected]>
From trace analysis: web_search returned a JSON string, which was
wrapped as serde_json::json!(string) creating a Value::String containing
JSON. When Monty got this as MontyObject::String, the Python code
couldn't index it with result['title'] → TypeError.
Fix: try parsing the tool output string as JSON first. If valid, use the
parsed Value (becomes a Python dict/list). If not valid JSON, keep as
string. This means web_search results are directly indexable in Python:
results = web_search(query="...")
print(results["results"][0]["title"]) # works now
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Root cause from trace analysis: the LLM writes `web_search()` (valid
Python identifier) but the tool registry has `web-search` (with hyphen).
The EffectBridgeAdapter couldn't find the tool → "Tool not found" error
→ model fabricated fake data instead.
Fixes:
- available_actions(): converts tool names from hyphens to underscores
(web-search → web_search) so the system prompt lists valid Python names
- execute_action(): tries the original name first, then falls back to
hyphenated form (web_search → web-search) for tool registry lookup
- Same conversion in router's capability registry builder
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* feat(workspace): multi-scope workspace reads
Adds the ability for a workspace to read from multiple user scopes
while keeping writes isolated to the primary scope. Configuration
via WORKSPACE_READ_SCOPES env var (comma-separated user IDs).
Includes identity file isolation (read_primary), multi-scope search,
list, and read operations, WorkspaceConfig refactor, and comprehensive
integration tests.
* fix: address review feedback for multi-scope workspace reads
- fix(memory): deduplicate timezone parsing for daily_log target
parse_timezone was called twice when target was "daily_log" without a
layer — once in path resolution, again in the fallback. Now computed
once and reused.
- fix(config): add character validation for WORKSPACE_READ_SCOPES and
layer scopes — both enforce [a-zA-Z0-9_-] to prevent path traversal
or injection via scope strings used as user_id in SQL queries.
- fix(config): use chars().take(32) instead of byte-index slicing for
scope length error messages (UTF-8 safety).
- fix(error): remove unused WorkspaceError::NotFound variant
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: downgrade search log to debug, add comments on list iteration
- Downgrade hybrid_search_multi tracing::info! to debug! — fires on
every multi-scope search with the default backend, too noisy for info
- Add comments explaining why list/list_all iterate per-scope instead
of using _multi trait methods (identity path filtering needs scope
attribution that merged results lose)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
After every thread completes, ThreadManager now automatically runs:
1. Retrospective trace analysis (non-LLM, always):
- Detects 8 issue categories (tool errors, code errors, missing
outputs, excessive steps, hallucination risk, etc.)
- Logs issues at warn level when found
2. Trace file recording (when ENGINE_V2_TRACE=1):
- Writes full JSON trace to engine_trace_{timestamp}.json
3. LLM reflection (when enable_reflection=true):
- Calls reflection pipeline to produce Summary, Lesson, Issue docs
- Saves docs to store for future context retrieval
- Enabled by default in the bridge router
All three run inside the spawned tokio task after exec.run() completes,
before saving the final thread state. No external wiring needed.
Removed duplicate trace recording from the router — it's now handled
by ThreadManager automatically.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Enable with ENGINE_V2_TRACE=1 to get full execution traces and
automatic issue detection after each thread completes.
Trace recording (executor/trace.rs):
- build_trace(): captures full thread state — messages (with full
content), events, step count, token usage, detected issues
- write_trace(): writes JSON to engine_trace_{timestamp}.json
- log_trace_summary(): logs summary + issues at info/warn level
Retrospective analyzer detects 8 issue categories:
- thread_failure: thread ended in Failed state
- no_response: no assistant message generated
- tool_error: specific tool failures with error details
- code_error: Python errors (NameError, SyntaxError, etc.) in output
- missing_tool_output: tool results exist but not in system messages
- excessive_steps: >10 steps (may be stuck in loop)
- no_tools_used: single-step answer without tools (hallucination risk)
- mixed_mode: text responses without code blocks (prompt not followed)
Thread state now saved to store after execution completes (for trace
access after join_thread).
Usage:
ENGINE_V2=true ENGINE_V2_TRACE=1 cargo run
# After each message: trace JSON + issue log in terminal
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>