Commit Graph
562 Commits
Author SHA1 Message Date
[email protected]andClaude Opus 4.6 ecbe3f5352 chore(engine): remove legacy Playbook doc type, superseded by Skill
Drop DocType::Playbook variant and all references — playbook extraction
mission was already renamed to skill extraction in the previous session.
Updates CLAUDE.md, architecture docs, context builder, retrieval weights,
mission comments, and store adapter path mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-27 19:46:05 -07:00
[email protected]andClaude Opus 4.6 96266cb46d feat(skills): credential specs in skill frontmatter, HTTP tool hardening, mission leases
Skills can now declare API credentials in YAML frontmatter (SkillCredentialSpec,
SkillCredentialLocation, SkillOAuthConfig, ProviderRefreshStrategy). Valid specs
are registered into SharedCredentialRegistry at startup; the HttpTool auto-injects
credentials for matching hosts — same zero-exposure model as WASM tools.

HTTP tool security hardening:
- Block LLM-provided auth headers for hosts with registered credentials
- Return structured authentication_required error for missing credentials
- Strip sensitive response headers (Set-Cookie, WWW-Authenticate, Authorization)
- Scan response body through LeakDetector before returning to LLM

Mission capability leases: registered mission_create/list/fire/pause/resume/delete
as a "missions" capability so threads receive leases. Removed routine_* aliases
from effect adapter — descriptions mention "routine" for LLM intent mapping.

Includes 10 integration tests (tests/skill_credential_injection.rs) covering
the full pipeline: YAML parsing → validation → registry → HttpTool wiring →
per-user isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-27 19:33:58 -07:00
[email protected]andClaude Opus 4.6 8e2349d12e feat(skills): extract ironclaw_skills crate and integrate with v2 engine
Extract the skills system into a standalone `ironclaw_skills` crate
(following the ironclaw_safety pattern) and wire it into the v2 engine
for deterministic skill selection, CodeAct code injection, and
confidence tracking.

**ironclaw_skills crate** (94 tests):
- Core types: SkillManifest, ActivationCriteria, LoadedSkill, SkillTrust
- V2 types: V2SkillMetadata, CodeSnippet, SkillMetrics, V2SkillSource
- Deterministic 4-phase selector (gating→scoring→budget→attenuation)
- apply_confidence_factor() for extracted skill scoring
- SKILL.md parser, validation/escaping, gating, registry, catalog
- Feature-gated: catalog (reqwest), registry (filesystem)

**Engine integration** (14 new tests):
- DocType::Skill with retrieval weight 0.45
- SkillSelector bridges MemoryDoc→LoadedSkill for shared scoring
- SkillTracker for usage/version/rollback confidence tracking
- System prompt injection via <skill> XML blocks
- CodeAct snippet injection via Monty NameLookup
- Skill extraction mission replaces playbook extraction
- ThreadManager.set_skill_selector() for runtime wiring

**Bridge + migration**:
- skill_migration.rs: v1 SKILL.md → v2 MemoryDoc (idempotent)
- init_engine() migrates v1 skills, builds SkillSelector
- src/skills/mod.rs → re-export shim

**E2E test** (tests/engine_v2_skill_codeact.rs):
- Full CodeAct loop: skill selected → LLM returns Python code →
  Monty executes http() → mock returns canned GitHub JSON →
  FINAL() terminates → thread completes with canned data
- GitHub SKILL.md in skills/github/ as reference implementation

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-27 16:24:07 -07:00
[email protected]andClaude Opus 4.6 a4f5c56d06 feat(engine): consolidate action execution, remove reflection, add learning missions
Three major changes to the v2 engine:

1. **Consolidated action execution** — `handle_execute_action` in Rust is now
   the single source of truth for lease lookup, policy check, lease consumption,
   action execution, event emission, and ActionResult message recording. The
   Python orchestrator no longer duplicates event/message logic. This fixes the
   empty call_id bug (OpenAI HTTP 400) and the missing tool_calls on assistant
   messages (Codex "No tool call found" error).

2. **Removed reflection system** — Deleted the per-thread reflection pipeline
   (pipeline.rs, executor.rs), ThreadState::Reflecting, ThreadType::Reflection,
   enable_reflection config, and all 3 reflection event kinds. Learning is now
   handled entirely by event-driven missions that fire selectively.

3. **Three learning missions** replace reflection:
   - `self-improvement` — fires on trace issues (error diagnosis, prompt fixes)
   - `playbook-extraction` — fires on successful 5+ step threads (reusable procedures)
   - `conversation-insights` — fires every 5 threads per project (user preferences,
     domain knowledge, workflow patterns)

Additional fixes:
- llm_query()/llm_query_batched() always include system message (Codex compat)
- handle_llm_complete adds assistant message with structured action_calls for
  Tier 0 responses (prevents "No tool call found" errors)
- Gateway broadcasts without thread_id emit as Status events instead of being dropped
- Comprehensive tests for call_id propagation and trace analysis (17 new tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-27 08:57:39 -07:00
[email protected]andClaude Opus 4.6 c48605106f feat(web): richer activity indicators for engine v2 execution
The gateway UI showed only generic "Thinking..." during engine v2
execution with no visibility into CodeAct code execution, tool calls,
or reflection. Now the event mapping produces detailed status updates:

Step lifecycle:
- "Calling LLM..." when a step starts (was "Thinking...")
- "Step complete — N in / M out tokens" when done (was "Processing...")

Tool execution:
- Emit ToolStarted + ToolCompleted SSE events so the frontend renders
  proper tool cards with spinner → checkmark/error transitions
- Duration shown in parameters field (e.g., "42ms")

CodeAct visibility:
- "Executing code..." when assistant produces a code block
- "Code executed" / "Code executed (no output)" for successful runs
- "Code error — retrying..." when Monty raises an exception

Reflection:
- "Reflecting on execution..." when post-thread analysis starts
- "Reflection complete — N insight(s) saved" when done

Also refactored thread_event_to_app_event → thread_event_to_app_events
(returns Vec<AppEvent>) to support emitting ToolStarted before
ToolCompleted in a single event handler pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 23:44:55 -07:00
[email protected]andClaude Opus 4.6 fad4272754 fix(engine): route messages to correct conversation by thread scope
Messages sent from a new conversation in the gateway always appeared in
the default assistant conversation because handle_with_engine ignored
the thread_id from the frontend.

Two fixes:

1. Engine conversation scoping — when the message carries a thread_id
   (from the frontend's conversation picker), use it as part of the
   engine conversation key: "gateway:<thread_id>" instead of just
   "gateway". This creates a distinct engine conversation per v1
   thread, so messages don't cross-contaminate.

2. V1 dual-write targeting — write user messages and assistant
   responses to the v1 conversation matching the thread_id (via
   ensure_conversation), not the hardcoded assistant conversation.
   Falls back to the assistant conversation when no thread_id is
   present (e.g., default chat).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 22:46:36 -07:00
[email protected]andClaude Opus 4.6 fa47dc30ab fix(web): support multiple gateway tabs by reducing SSE connections
Each browser tab opened 2 SSE connections (chat events + log events).
With the HTTP/1.1 per-origin limit of 6, the 3rd tab exhausted the
pool and couldn't load any data.

Three changes:

1. Lazy log SSE — only connect when the logs tab is active, disconnect
   when switching away. Most users rarely view logs, so this saves a
   connection slot per tab.

2. Visibility API — close SSE when the browser tab goes to background
   (user switches to another tab), reconnect when it becomes visible.
   Background tabs don't need real-time events.

3. Combined with the existing beforeunload cleanup, this means:
   - Active foreground tab: 1 connection (chat SSE only, +1 if logs tab)
   - Background tabs: 0 connections
   - Closed/refreshed tabs: 0 connections (beforeunload cleanup)

This allows many gateway tabs to coexist within the 6-connection limit.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 19:46:00 -07:00
[email protected]andClaude Opus 4.6 e508d2083b fix(web): close SSE connections on page unload to prevent connection starvation
The browser limits concurrent HTTP/1.1 connections per origin to 6.
Without cleanup, SSE connections from prior page loads linger after
refresh/navigation, eating into the pool. After 2-3 refreshes, all 6
slots are consumed by stale SSE streams and new API fetch calls queue
indefinitely — the UI shows "connected" (SSE works) but data never
loads.

Add a beforeunload handler that closes both eventSource (chat events)
and logEventSource (log stream) so the browser can reuse connections
immediately on page reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 19:33:18 -07:00
[email protected]andClaude Opus 4.6 47f83c1bfc feat(web): improve mission detail with markdown goal and thread table
- Goal rendered as full-width markdown block instead of plain-text
  meta item (uses existing renderMarkdown/marked)
- Current focus and success criteria also rendered as markdown
- Spawned threads shown as a clickable table with goal, type, state,
  steps, tokens, and created date instead of a UUID list
- Clicking a thread row opens an inline thread detail view showing
  metadata grid and full message history with markdown rendering
- Back button returns to the mission detail view
- Backend: mission detail now returns full thread summaries (goal,
  state, step_count, tokens) instead of just thread IDs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 19:15:54 -07:00
[email protected]andClaude Opus 4.6 6d56d9cb11 fix(engine): eagerly initialize engine v2 at startup
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]>
2026-03-26 19:07:35 -07:00
[email protected]andClaude Opus 4.6 7c153d584b feat(web): add Missions tab to gateway UI
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]>
2026-03-26 18:53:39 -07:00
[email protected]andClaude Opus 4.6 098addce27 feat(engine): complete v2 side-by-side integration with gateway API
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]>
2026-03-26 18:19:10 -07:00
[email protected] b07cc5e144 Merge remote-tracking branch 'origin/staging' into v2-architecture
# Conflicts:
#	Cargo.toml
2026-03-26 14:00:35 -07:00
adf4e25c8f fix(extensions): channel-relay auth dead-end, observability, and URL override (#1681)
* 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]>
2026-03-26 13:49:05 -07:00
rajulbhatnagarandGitHub ed4d92932a fix(agent): discard truncated tool calls when finish_reason == Length (#1631) (#1632) 2026-03-26 10:02:41 +03:00
firat.sertgozandGitHub b3fbef5287 fix(llm): filter XML tool-call recovery by context (#1641)
* fix(llm): filter XML tool-call recovery by context

* fix: address review comments on PR #1641
2026-03-26 07:37:59 +01:00
[email protected] a81c75494d Merge remote-tracking branch 'origin/staging' into v2-architecture
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/ironclaw_common/src/event.rs
#	crates/ironclaw_common/src/lib.rs
#	src/channels/web/server.rs
#	src/channels/web/sse.rs
#	src/channels/web/types.rs
#	src/channels/web/util.rs
#	src/channels/web/ws.rs
#	src/orchestrator/api.rs
#	src/worker/job.rs
#	tests/ws_gateway_integration.rs
2026-03-25 19:17:54 -07:00
4c043bf057 feat: complete multi-tenant isolation — phases 2–4 (#1614)
* 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]>
2026-03-25 17:24:48 -07:00
Henry ParkandGitHub 86d1143064 Fix libsql prompt scope regressions (#1651) 2026-03-25 14:36:53 -07:00
Henry ParkandGitHub ab0ad948f3 Normalize cron schedules on routine create (#1648)
* 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

* Fix MCP lifecycle trace user scope

* Normalize cron schedules on routine create
2026-03-25 13:47:12 -07:00
Henry ParkandGitHub 0341fcc940 Fix REPL single-message hang and cap CI test duration (#1643)
* 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
2026-03-25 11:45:29 -07:00
[email protected] d7a3e04cec Add checkpoint-based engine thread recovery 2026-03-25 10:02:45 -07:00
41ed0a0f98 feat(agent): thread per-tool reasoning through provider, session, and all surfaces (#1513)
* 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]>
2026-03-25 08:35:41 -07:00
6daa2f155f fix: ensure LLM calls always end with user message (closes #763) (#1259)
* 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]>
2026-03-25 10:31:44 +03:00
[email protected]andClaude Opus 4.6 8180a417ff feat(engine): self-improving engine via Mission system
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]>
2026-03-25 00:15:50 -07:00
706c3a1b47 refactor: extract AppEvent to crates/ironclaw_common (#1615)
* 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]>
2026-03-24 23:02:46 -07:00
656151783c feat(cli): show credential auth status in tool info (#1572)
* 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]>
2026-03-24 23:01:19 -07:00
Henry ParkandGitHub 82822d7b25 fix: restore owner-scoped gateway startup (#1625)
* fix: restore owner-scoped gateway startup

* fix: split gateway owner and sender scope

* fix: keep multi-user gateway sender identity

* test: cover gateway sender scope regression

* test: harden e2e startup teardown race

* fix: align gateway owner scope across auth modes
2026-03-24 16:11:53 -07:00
Henry ParkandGitHub dcb2d89e3a Fix hosted OAuth refresh via proxy (#1602)
* Fix hosted OAuth refresh via proxy

* Address OAuth refresh review feedback

* Address new OAuth refresh review comments

* Address additional OAuth refresh review feedback

* Harden proxy exchange redirects
2026-03-24 13:51:30 -07:00
[email protected]andClaude Opus 4.6 20cb92d4da feat(bridge): map routine_* calls to mission operations in v2
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]>
2026-03-24 11:51:39 -07:00
Zaki ManianandGitHub f3da30a454 perf(agent): optimize approval thread resolution (UUID parsing + lock contention) (#1592) 2026-03-24 11:48:30 -07:00
Pierre LE GUENandGitHub 5901451603 fix: remove stale stream_token gate from channel-relay activation (#1623)
* 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
2026-03-24 10:49:13 -07:00
[email protected]andClaude Opus 4.6 d5b267323b feat(bridge): wire MissionManager into engine v2 for CodeAct access
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]>
2026-03-24 09:37:21 -07:00
[email protected]andClaude Opus 4.6 536fafb096 feat(engine): implement MissionManager execution with meta-prompts
The MissionManager now builds evolving meta-prompts and processes
thread outcomes for continuous learning:

fire_mission() upgraded:
- Loads Project MemoryDocs via RetrievalEngine for context
- Builds meta-prompt from: goal, current_focus, approach_history,
  project knowledge docs, trigger payload, thread count
- Spawns thread with meta-prompt as user message
- Background task waits for completion and processes outcome
- Daily thread budget enforcement (max_threads_per_day)

Meta-prompt structure:
  # Mission: {name}
  Goal: {goal}
  ## Current Focus (evolves between threads)
  ## Previous Approaches (what we've tried)
  ## Knowledge from Prior Threads (lessons, playbooks, issues)
  ## Trigger Payload (webhook/event data if applicable)
  ## Instructions (accomplish step, report next focus, check goal)

Outcome processing:
- Extracts "next focus:" from FINAL() response → updates current_focus
- Detects "goal achieved: yes" → completes mission
- Records accomplishment in approach_history
- Failed threads recorded as "FAILED: {error}"

Cron ticker:
- start_cron_ticker() spawns tokio task, ticks every 60s
- Checks active Cron missions, fires those past next_fire_at

151 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-24 09:24:55 -07:00
[email protected]andClaude Opus 4.6 0b0e770774 feat(bridge): complete Phase 6 — v1-only tool blocking, rate limiting, call limits
Three security/stability improvements in EffectBridgeAdapter:

1. V1-only tool blocking:
   - routine_create, create_job, build_software (and hyphenated variants)
     return helpful error: "use the slash command instead"
   - Filtered out of available_actions() so system prompt doesn't list them
   - Prevents crash from tools needing RoutineEngine/Scheduler refs

2. Per-step tool call limit:
   - Max 50 tool calls per code block (AtomicU32 counter)
   - Prevents amplification: `for i in range(10000): shell(...)`
   - Returns "call limit reached, break into multiple steps"

3. Rate limiting:
   - Per-user per-tool sliding window via RateLimiter
   - Checks tool.rate_limit_config() before every execution
   - Returns "rate limited, try again in Ns"

Architecture plan updated:
- Gateway integration: DONE
- Routines: BLOCKED (gracefully, with slash command fallback)
- Rate limiting: DONE
- Call limit: DONE
- Phase 6 status: DONE (remaining: acceptance tests, two-phase commit)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-24 08:51:45 -07:00
d3d517fd67 fix(agent): case-insensitive channel match and user_id filter for event triggers (#1211)
* 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]>
2026-03-24 10:44:25 +01:00
01678be61d fix(routines): normalize status display across web and CLI (#1469)
* 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]>
2026-03-24 10:41:33 +01:00
[email protected]andClaude Opus 4.6 ccec19174d feat(bridge): integrate with web gateway via AppEvent + v1 conversation DB
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]>
2026-03-24 00:54:55 -07:00
fb3548956b fix(tunnel): managed tunnels target wrong port and die from SIGPIPE (#1093)
* 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]>
2026-03-24 08:46:22 +01:00
[email protected]andClaude Opus 4.6 d059844351 merge: integrate AppEvent refactoring (PR #1615)
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]>
2026-03-24 00:43:07 -07:00
[email protected]andClaude Opus 4.6 953b12585d 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]>
2026-03-24 00:28:42 -07:00
[email protected]andClaude Opus 4.6 af6fff816a fix(bridge): adapt to execute_tool_with_safety params-by-value change
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]>
2026-03-23 22:29:35 -07:00
[email protected] ac7c17ee40 Merge remote-tracking branch 'origin/staging' into v2-architecture 2026-03-23 22:25:43 -07:00
5847479fd8 fix(agent): persist /model selection to .env, TOML, and DB (#1581)
* 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]>
2026-03-23 22:24:26 -07:00
[email protected]andClaude Opus 4.6 3b309ccbc8 feat(bridge): persist reflection docs to workspace for cross-session learning
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]>
2026-03-23 22:11:03 -07:00
3fdb187796 refactor(tools): auto-compact WASM tool schemas, add descriptions, improve credential prompts (#1525)
* 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]>
2026-03-23 21:59:14 -07:00
[email protected]andClaude Opus 4.6 ad3529e6fb fix(bridge): demote all router info! logging to debug!
"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]>
2026-03-23 21:40:33 -07:00
[email protected]andClaude Opus 4.6 e82dcbd5e6 feat(bridge): implement tool approval flow for engine v2
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(&params) — 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]>
2026-03-23 21:10:40 -07:00
b441ebec02 feat: multi-tenant auth with per-user workspace isolation (#1118)
* 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]>
2026-03-23 20:50:05 -07:00
[email protected]andClaude Opus 4.6 38fc870223 feat(bridge): wire v1 security controls into engine v2 adapter
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]>
2026-03-23 20:34:24 -07:00