Commit Graph
25 Commits
Author SHA1 Message Date
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
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
a09c023642 feat(ux): complete UX overhaul — design system, onboarding, web polish (#1277)
* feat(ux): complete UX overhaul — design system, boot screen, onboarding, web polish

Shared design system: CSS custom properties for spacing, typography,
transitions, and color tokens used across web UI and boot screen.

Boot screen: compact feature-tags line showing enabled subsystems
(db, tools, routines, heartbeat, skills, sandbox, embeddings) at a
glance. Downgrade startup info logs (libSQL, webhook, workspace seed)
to debug level since the boot screen now covers this.

Onboarding wizard: model picker with live API fetch, provider-aware
auth flow, improved error recovery and progress display.

Web UI: ARIA attributes, welcome card, streaming debounce,
connection status banner, skeleton loaders, send cooldown.

CLI: doctor command enhancements, status command cleanup,
REPL banner consolidation, shared fmt module.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* feat(ux): Apple-level design refinements — spring physics, glass morphism, chat polish

Merge staging theme support (dark/light/system toggle) and layer UX
polish on top: spring-physics motion, glass morphism depth, chat
experience improvements, and responsive mobile refinements.

Design system:
- Restore and extend design token system (spacing, typography, timing,
  easing) with legacy aliases for theme compatibility
- Add shadow tiers, accent glow, glass morphism, spring easing tokens
- Tokens defined in both dark (:root) and light ([data-theme="light"])

Micro-interactions (Phase 2):
- Spring-overshoot message entry animation (slideUp)
- Spring-scale button press on all interactive buttons
- Tab crossfade animation, tool card smooth accordion (max-height)
- Modal scale(0.95) + blur(8px) entry, toast spring slide
- Sidebar width crossfade, card hover lift

Visual depth (Phase 3):
- Tab bar glass morphism + surface highlight + sliding indicator
- Active tab accent background pill
- Assistant message accent left border, user message bubble tail
- Floating input area (rounded + shadow + margin)

Chat polish (Phase 4):
- Smooth streaming cursor (cursorPulse), message hover timestamps
- Time separators (Today/Yesterday/date)
- Textarea smooth auto-expand, send button glow

Settings & forms (Phase 5):
- iOS-style toggle switches for boolean settings
- Input focus glow, save feedback spring animation
- Welcome card with gradient background + proper spacing
- Sticky settings group headers with glass backdrop

Accessibility & mobile (Phase 6):
- Animated focus ring, prefers-reduced-motion global kill-switch
- Touch target audit (44px min), mobile bottom-sheet modals
- Mobile bottom tab bar, toast redesign (icon + border + countdown)
- Thread hover translateX, badge in_progress pulse

Bug fixes:
- Gateway/TEE popover z-index (tab-bar z-index: 200, popovers 500)
- Connection lost banner as fixed top bar instead of flex child
- Sidebar collapse keeps toggle + new thread buttons visible
- Downgrade noisy startup logs (db, webhook, vector) to debug
- Remove green dot pulse animation on connected status
- Deduplicate confirm-modal in HTML, add tab-indicator div

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* feat(web): mobile layout improvements — sidebar toggle, settings drill-down, tab bar polish

- Fix mobile sidebar toggle: use expanded-mobile class instead of collapsed,
  add backdrop overlay, auto-close on thread select, outside-click dismiss
- Settings: replace cramped horizontal tabs with drill-down navigation
  (category list → detail view → back button)
- Bottom tab bar: add glass morphism, hide theme toggle, flip tab indicator
  to top edge
- Keep thread toggle button visible in collapsed 36px sidebar strip

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* feat(repl): interactive approval selector and transient status lines

- Replace ASCII-art approval box with clean horizontal rule card
- Add inquire-based interactive selector for tool approvals (↑↓ + Enter)
- Selector runs directly from send_status via spawn_blocking, with
  stdin_locked flag to prevent readline from competing for stdin
- Transient thinking/tool-started lines: each replaces the previous,
  all erased before final output (no clutter left in scrollback)
- Esc in selector sends denial so agent never gets stuck

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: widen TurnCost token fields to u64 and remove unused variable

- Change input_tokens/output_tokens from u32 to u64 in StatusUpdate::TurnCost,
  SseEvent::TurnCost, and the thread_ops emit site to avoid truncation on
  large conversations
- Remove unused _routine_engine_for_loop binding in agent_loop.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* chore: reduce startup log noise — demote info to debug

Demote routine startup messages (builder, WASM tools, tunnel, WASM
channels) from info to debug so the default log output stays clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(web): allow CDN scripts in CSP connect-src directive

Add cdn.jsdelivr.net and cdnjs.cloudflare.com to connect-src so the
browser can fetch marked.js and DOMPurify without CSP violations.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: fix cargo fmt in repl.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(web): gate turn_cost SSE handler on current thread

Prevents cost badge from attaching to the wrong message when
switching threads or receiving events from background threads.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* ci: retrigger CI

* fix: add missing extension_manager to webhook EngineContext

The webhook trigger path added in #736 was missing the
extension_manager field introduced by #1453.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* chore: ignore RUSTSEC-2026-0049 rustls-webpki CRL advisory

Low impact — requires compromised CA to exploit. Tracked for
upstream rustls-webpki upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(routines): use fields.join for cron normalization

Use split_whitespace fields instead of re-trimming the original string
to avoid preserving extra internal whitespace in cron expressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* feat(repl): Apple-style approval card — clean vertical flow

- Drop verbose tool description (the command IS the decision surface)
- Unified vertical pipe layout: ◆ header → │ params → │ selector
- Selector options show keyboard shortcuts inline: Approve (y)
- Compact help message, answered state uses └ to close the flow
- No horizontal rules, no blank-line padding — just breathing room

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* refactor(repl): replace inquire with crossterm for approval selector

Drop the inquire dependency (which pulled in crossterm 0.25, duplicating
the existing 0.28). The 3-option approval selector is now built directly
with crossterm raw mode — same UX, zero new dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* chore(deps): upgrade crossterm 0.28 → 0.29, eliminate duplication

termimad (via crokey) uses crossterm 0.29. Upgrading our direct
dependency from 0.28 to 0.29 collapses to a single crossterm version
in the dependency tree. Also migrated termimad::crossterm:: references
to the direct crossterm import.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address review comments — box_top off-by-one, smart_truncate overflow, mobile theme toggle

- Fix box_top() fill calculation: was off-by-one, producing boxes 1 char
  too wide (fmt.rs)
- Fix smart_truncate(): account for "..." in the budget so output never
  exceeds max_chars (repl.rs)
- Move theme toggle to settings sidebar on mobile instead of display:none,
  so mobile users can still switch themes (style.css, index.html, app.js)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: cargo fmt repl.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address review — retry duplication, CSP connect-src, deny color

- Remove failed message before retry to prevent duplicate user messages
- Revert connect-src to 'self' — CDN hosts only need script-src
- Use red for Deny confirmation in REPL approval selector

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-21 23:50:49 -07:00
09e1c97a27 fix(approval): make "always" auto-approve work for credentialed HTTP requests (#1257)
The HTTP tool returned `ApprovalRequirement::Always` for requests with
credentials, but `Always` is hardcoded to ignore the session auto-approve
set. This meant users who clicked "always" were re-prompted on every
subsequent HTTP call — the UI offered "always" but the backend ignored it.

Two fixes:
1. HTTP credentialed requests now return `UnlessAutoApproved` instead of
   `Always`, so the session auto-approve set is respected.
2. `StatusUpdate::ApprovalNeeded` now carries `allow_always: bool`. All
   channel UIs (Telegram, Slack, Signal, REPL, Web) conditionally hide
   the "always" option when a tool truly requires per-invocation approval
   (`ApprovalRequirement::Always`, e.g. destructive shell commands).

Also boxes `PendingApproval` in `AgenticLoopResult::NeedApproval` to fix
a pre-existing clippy `large_enum_variant` warning.

Regression tests included (test_credentialed_requests_respect_auto_approve,
test_allow_always_matches_approval_requirement) but CI heuristic cannot
detect them in cross-fork PR diffs.

[skip-regression-check]

Co-authored-by: Tyler <[email protected]>
2026-03-19 11:45:32 -07:00
Henry ParkandGitHub 878a67cdb6 Refactor owner scope across channels and fix default routing fallback (#1151)
* refactor: add explicit owner scope across channels

* fix: tighten routine owner target routing

* fix: address owner scope review feedback

* Fix owner-scope onboarding and event trigger isolation

* Tighten routing fallback and wizard owner validation

* fix: address owner-scope follow-up review

* fix: tighten owner-scope follow-up details

* fix: import Channel trait in telegram test

* fix: normalize http webhook sender ids

* fix: address remaining owner-scope review issues

* fix: reconcile config rebase fallout

* fix: reconcile extension manager rebase drift

* fix: address current copilot review regressions

* fix: restore clippy matrix after rebase
2026-03-16 13:31:03 -07:00
757d24bd90 feat(web): add follow-up suggestion chips and ghost text (#1156)
* feat(web): add follow-up suggestion chips and ghost text to chat UI

The LLM now always generates 1-3 follow-up command suggestions via
<suggestions> tags in its response. These are extracted server-side,
broadcast as SSE events, and rendered as clickable chips above the
chat input. The first suggestion also appears as ghost text in the
input field (Tab to accept). Includes debug logging for LLM responses
in the agentic loop and removes noisy NEAR AI status logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: resolve deferred review items from PR #1156 [skip-regression-check]

- Remove literal backslashes from raw string prompt (reasoning.rs)
- Make WASM channels skip Suggestions status (no-op instead of empty callback)
- Add !e.shiftKey guard to Tab-to-accept ghost text handler
- Cap extracted suggestions at 3 and trim whitespace-only entries
- Extract suggestions in approval-resume path (prevents tag leaking)
- Remove stale .has-ghost class during showSuggestionChips reset

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-14 18:57:24 +00:00
553c306c52 feat: full image support across all channels (#725)
* feat: full image support across all channels

End-to-end image handling: upload, generation, analysis, editing, and
rendering across web gateway, HTTP webhook, WASM (Telegram/Slack), and
REPL channels. Builds on the attachment infrastructure from #596 and
draws inspiration from PR #641's image pipeline approach — credit to
that PR's author for the sentinel JSON pattern and base64-in-JSON
upload design.

Key changes:
- Image upload in web UI (file picker, paste, preview strip)
- Image generation tool (FLUX/DALL-E via /v1/images/generations)
- Image edit tool (multipart /v1/images/edits with fallback)
- Image analysis tool (vision model for workspace images)
- Model detection utilities (image_models.rs, vision_models.rs)
- Sentinel JSON detection in dispatcher for generated image rendering
- StatusUpdate::ImageGenerated → SSE/WS/REPL/WASM broadcast
- HTTP webhook attachment support (base64, 5MB/file, 10MB total)
- WASM channel image download (Telegram via file API, Slack via host HTTP)
- Tool registration wiring in app.rs

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR #725 review comments (16 issues)

- SecretString for API keys in all image tools (image_gen, image_edit, image_analyze)
- Binary image read via tokio::fs::read instead of DB-backed workspace.read()
- Replace Arc<Workspace> with Option<PathBuf> base_dir (workspace has no filesystem API)
- ApprovalRequirement::UnlessAutoApproved for cost-sensitive image tools
- Scope sentinel detection to image_generate/image_edit tool names only
- Skip ToolResult preview broadcast for image sentinels (avoids multi-MB base64 in SSE)
- Extract shared media_type_from_path() to builtin/mod.rs
- Rename fallback_chat_edit → fallback_generate with tracing::warn
- Increase gateway body limit from 1MB to 10MB for image uploads
- Increase webhook body limit to 15MB (base64 overhead)
- Log warning on invalid base64 in images_to_attachments
- Client-side image size limits (5MB/file, 5 images max) in app.js
- aria-label on attach button for accessibility
- Update body_too_large test for new 10MB limit

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add Slack file size check before download (PR review item #15)

Skip downloading files larger than 20 MB in the Slack WASM channel to
avoid excessive memory use and slow downloads in the WASM runtime.
Logs a warning when a file is skipped. Also bumps channel versions
for Slack and Telegram (prior branch changes).

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(security): add path validation and approval requirement to image tools

Add sandbox path validation via validate_path() to both ImageAnalyzeTool
and ImageEditTool to prevent path traversal attacks that could exfiltrate
arbitrary files through external vision/edit APIs. Also fix
ImageAnalyzeTool::requires_approval to return UnlessAutoApproved,
consistent with ImageEditTool and ImageGenerateTool.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: post-download size guards and empty data_url sentinel check

- Slack: add post-download size check on actual bytes when metadata
  size_bytes is absent, preventing bypass of the 20MB limit
- Telegram: add 20MB download size limit (matching Slack) enforced
  in download_telegram_file() after receiving response bytes
- Dispatcher: skip broadcasting ImageGenerated SSE event when
  data_url is empty from unwrap_or_default(), log warning instead

Closes correctness issues #3, #4, #5 from PR #725 review.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use mime_guess for media type detection, add alt attrs and media_type validation

- Replace hardcoded media type mapping with mime_guess crate (already in deps)
- Add alt attributes to img elements in web UI for accessibility
- Validate media_type starts with "image/" in images_to_attachments()
- Update bmp test assertion to match mime_guess behavior

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki <[email protected]>
2026-03-09 03:41:27 +00:00
fe91ba2ab4 fix(repl): skip /quit on EOF when stdin is not a TTY (#724)
When running as a launchd/systemd daemon, stdin is /dev/null.
rustyline reads EOF immediately and the REPL thread was sending
a /quit message, causing the agent to shut down right after
startup — making service mode non-functional on both macOS and Linux.

Fix: check std::io::stdin().is_terminal() before sending /quit on
EOF. In daemon mode (no TTY) the REPL thread exits silently, leaving
other channels (gateway, telegram, …) running as expected.

Fixes #723

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Zaki Manian <[email protected]>
2026-03-08 20:40:56 +00:00
ReidandGitHub 068ad2d4b7 Fix single-message mode to exit after one turn when background channels are enabled (#719) 2026-03-08 12:54:18 -07:00
df3635d6be feat(timezone): add timezone-aware session context (#671)
* feat(timezone): add timezone-aware session context (#661)

All timestamps were UTC-only, causing daily logs to split at UTC midnight,
cron schedules to fire in UTC, and no quiet hours for heartbeat. This adds
timezone as a per-session property flowing from the client.

Key changes:
- New `src/timezone.rs` module with resolution chain, parsing, and detection
- `IncomingMessage` carries optional timezone from client
- `JobContext.user_timezone` flows timezone to tools
- `next_cron_fire()` accepts timezone for schedule evaluation
- `Trigger::Cron` stores optional timezone (backward-compatible)
- Workspace gains `_tz` variants for daily logs and system prompt
- Heartbeat supports quiet hours (`HEARTBEAT_QUIET_START/END`)
- Web frontend sends `Intl.DateTimeFormat().resolvedOptions().timeZone`
- REPL auto-detects system timezone
- `DEFAULT_TIMEZONE` env var / settings for server-wide default

Storage stays UTC. Conversion happens at display boundaries.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address review feedback on timezone-aware sessions

- Validate quiet hours values (0-23) in HeartbeatConfig::resolve()
- Fall back to settings values when env vars are unset for quiet hours
- Validate IANA timezone strings in routine_create/update with parse_timezone
- Add timezone field to routine_create tool schema
- Allow standalone timezone update on cron routines without changing schedule
- Return path from append_daily_log_tz to avoid TOCTOU race at midnight
- Delegate append_daily_log to append_daily_log_tz(entry, UTC) to avoid drift
- Preserve timezone through approval flow via PendingApproval.user_timezone
- Improve test_today_in_tz to not depend on hardcoded year
- Add 3 regression tests for quiet hours config validation

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in routine.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address second round of review feedback

- Remove .claude/scheduled_tasks.lock from repo and add to .gitignore
- Store resolved timezone (not raw message.timezone) in PendingApproval
- Carry forward user_timezone through chained approvals in thread_ops
- Wire quiet_hours_start/end from config to HeartbeatRunner
- Support X-Timezone header as fallback in chat_send_handler

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): include user's local time in time tool response

The time tool's "now" operation now returns local_iso and timezone
fields based on ctx.user_timezone, so the LLM can report time in
the user's timezone instead of always UTC.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in time.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address Copilot review round 3 — validation, deterministic tests, schema fixes

- Validate DEFAULT_TIMEZONE and HEARTBEAT_TIMEZONE at config load time
- Add timezone field to HeartbeatSettings and config::HeartbeatConfig
- Wire heartbeat timezone from config through agent_loop to HeartbeatRunner
- Add timezone to routine_update tool schema (was accepted but not advertised)
- Error on schedule/timezone update for non-cron routines
- Validate timezone in Trigger::from_db (coerce invalid to None with warning)
- Validate timezone in approval path (thread_ops.rs) before overwriting
- Time tool always includes timezone/local_iso fields (fallback to UTC)
- Make quiet hours tests deterministic using current UTC hour
- Add regression tests for config validation

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-08 08:01:56 +00:00
902492bcdb feat(web): show error details for failed tool calls (#490)
* feat(web): show error details and input params for failed tool calls

Failed tool calls in the gateway UI previously showed only a red X icon
with an empty expandable body. This change:

- Adds optional `error` and `parameters` fields to `ToolCompleted` SSE
  events so the browser receives failure details in real-time
- Auto-expands failed tool cards to make errors immediately visible
- Adds `StatusUpdate::tool_completed()` constructor that centralizes
  the 5 duplicated construction sites and applies `redact_params()` to
  prevent sensitive values (e.g. secret_save's "value" param) from
  leaking through SSE broadcasts
- Adds `sensitive_params()` trait method to `Tool` for declaring which
  parameters must be redacted before logging, hooks, and UI display
- Adds `redact_params()` utility and wires it through hooks, approvals,
  ActionRecord storage, and debug logs in dispatcher/worker
- Adds `SecretListTool` and `SecretDeleteTool` for LLM-driven secret
  management (values never returned, only names/metadata)
- Fixes auth flow: setup-only extensions show configure modal instead
  of OAuth card; auth_completed SSE dismisses both UI paths
- CI: release workflow creates PR instead of pushing directly to main
- Registry: MissingChecksum error enables source fallback for
  bootstrapping when checksums haven't been populated yet

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: keep original params in PendingApproval for execution, redact only for display

Address two PR review comments:

1. execute_chat_tool_standalone now redacts sensitive params before logging,
   matching the pattern already used in worker.rs.

2. PendingApproval previously stored redacted parameters, which meant
   approved tool calls received "[REDACTED]" instead of the actual values.
   Add a display_parameters field for UI/logs and keep parameters as the
   original values used for execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address PR review comments

- worker.rs: redact sensitive params before BeforeToolCall hook, matching
  dispatcher.rs — hooks in the autonomous job path now receive redacted
  params instead of raw values
- registry.rs: fix docstring for register_secrets_tools (list, delete,
  not save/list/delete — no SecretSaveTool is registered)
- app.js: fix double toast/loadExtensions in submitConfigureModal —
  for non-OAuth success the auth_completed SSE already handles both,
  so skip them in the HTTP response handler to avoid duplicates

[skip-regression-check]

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-04 15:38:26 -08:00
ibhagwanandGitHub c592a8f2de feat: add IRONCLAW_BASE_DIR env var with LazyLock caching (#397) 2026-02-27 17:43:43 +04:00
firat.sertgozGitHubIllia Polosukhingemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
3d4c647216 fix: map Esc to interrupt and Ctrl+C to graceful quit (#267)
* fix: map Esc to interrupt and Ctrl+C to graceful quit

* Apply suggestions from code review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-21 04:46:15 +00:00
097a26ace6 fix: harden openai-compatible provider, approval replay, and embeddings defaults (#237)
* fix: harden openai-compatible tool flow and local defaults

* fix: close approval replay gaps and harden openai-compatible flow

* fix: address review feedback and code improvements (takeover #112)

- Make ChatCompletionResponse.id Optional<String> to handle providers
  that omit or null the field
- Propagate HTTP client builder errors instead of silently dropping
  timeout configuration (openai_compatible_chat, nearai_chat)
- Add EMBEDDING_DIMENSION env var with smart per-model defaults instead
  of hardcoding 768/1536 everywhere
- Remove duplicated dimension inference logic from main.rs

Co-Authored-By: panosAthDBX <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: harden src/llm/ module from crate audit findings

- Replace 9x .expect() on RwLock with graceful poison recovery
  (nearai.rs: 7, nearai_chat.rs: 2) — eliminates production panics
- Propagate HTTP client builder errors in nearai.rs instead of
  silently dropping timeout config (NearAiProvider::new now returns Result)
- Make nearai_chat ChatCompletionResponse.id Optional<String>
  (mirrors openai_compatible_chat.rs fix for providers that omit id)
- Make nearai_chat usage fields optional with defensive parse_usage()
  helper (was required u32 fields that crash on null/missing)
- Truncate error responses to 512 chars in nearai_chat.rs error
  messages to prevent log bloat and potential data leakage
- Delegate 4 missing LlmProvider methods in FailoverProvider
  (model_metadata, seed_response_chain, get_response_chain_id,
  calculate_cost) to last-used provider instead of trait defaults

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(llm): add RetryProvider, remove openai_compatible_chat, harden decorators

- Add composable RetryProvider decorator wrapping any LlmProvider with
  exponential backoff + jitter, respecting RateLimited retry_after hints
- Remove openai_compatible_chat.rs — replaced by rig adapter + RetryProvider
- Remove internal retry loop from nearai.rs (was causing double-retry
  with external RetryProvider, up to 16 attempts instead of 4)
- Remove internal retry loop from nearai_chat.rs (same issue)
- Wire RetryProvider into main.rs composition chain: each provider gets
  its own retry wrapper before failover
- Move normalize_tool_name to rig_adapter.rs for all rig-based providers
- Reconcile is_retryable() vs is_transient() error classification:
  ModelNotAvailable no longer retryable, Json no longer transient
- Fix unchecked Duration subtraction panic in circuit_breaker.rs
- Make failover.rs use shared is_retryable() from retry.rs
- Remove stale #[allow(dead_code)] on NearAiResponse::id (field is used)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review feedback — error handling, dimension validation, libSQL warning

- Replace response.text().await.unwrap_or_default() with proper error
  propagation in nearai.rs and nearai_chat.rs (4 call sites). Failures
  now return LlmError::RequestFailed with context instead of silently
  proceeding with an empty string.
- Add embedding dimension validation in OllamaEmbeddings::embed_batch():
  returns EmbeddingError if Ollama returns embeddings with a dimension
  that doesn't match the configured value.
- Add runtime warning when libSQL backend is used with non-1536 embedding
  dimension, since the libSQL schema uses F32_BLOB(1536) and cannot store
  different-dimension vectors.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Apply suggestions from code review

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: panosAthDbx <[email protected]>
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Copilot <[email protected]>
2026-02-19 23:05:04 +00:00
63302ab406 feat: add polished boot screen on CLI startup (#118)
* feat: add polished boot screen on CLI startup

Replace the minimal one-liner REPL banner with an ANSI-styled status
panel that summarizes the agent's runtime state after initialization:
model, database, tool count, enabled features, active channels, and
the gateway URL. The boot screen is shown only in interactive CLI mode
(skipped for single-message -m mode).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review feedback on boot screen

- Stop logging gateway auth token in tracing::info! (security)
- Use info.agent_name instead of hardcoded "IronClaw" in header
- Display embeddings provider in features line: "embeddings (openai)"
- Add Display impl for DatabaseBackend, simplify main.rs match

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-17 05:50:57 +00:00
a53b2c10b5 fix: Fix wasm tool schemas and runtime (#42)
* feat: Move debug log truncation from agent loop to REPL channel

Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Flatten WASM tool schemas and fix host HTTP runtime contention

LLMs can't reliably follow oneOf + const discriminator patterns in JSON
Schema, causing tools like Google Calendar to receive malformed params
(e.g., {"operation":"list_events","data":{"calendarId":"primary"}} instead
of {"action":"list_events","calendar_id":"primary"}). Replace all 9 WASM
tool schemas with flat action enum + top-level properties. The serde
#[serde(tag = "action")] deserialization works identically.

Also fixes WASM host HTTP requests (channels and tools) stalling during
startup by replacing Handle::current().block_on() with a dedicated
single-threaded runtime per request, avoiding I/O driver contention.

Reduces verbose LLM debug logging (full request/response payloads) and
changes tower_http default from debug to warn.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Built-in OAuth credentials and combined Google scopes

Add infrastructure for shipping default OAuth credentials with the binary,
similar to how gcloud/rclone bake in their client_id. Credentials are set
at compile time via IRONCLAW_GOOGLE_CLIENT_ID / IRONCLAW_GOOGLE_CLIENT_SECRET
env vars, or can be hardcoded in src/cli/oauth_defaults.rs.

The fallback chain is: capabilities file > runtime env var > built-in defaults.

Also, when authing any Google tool, scopes from ALL installed Google tools
are now combined into a single OAuth request (they all share the same
google_oauth_token secret). One login covers Gmail, Calendar, Drive, etc.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Ship default Google OAuth credentials for zero-config auth

Google Desktop App credentials are not secret (per Google's own docs).
Hardcode them so `ironclaw tool auth <google-tool>` works out of the box
without requiring users to register their own OAuth app.

Credentials can still be overridden at compile time
(IRONCLAW_GOOGLE_CLIENT_ID) or runtime (GOOGLE_OAUTH_CLIENT_ID).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Consistent OAuth callback port and polished landing page

- Use fixed port 9876 instead of scanning 9876-9886 (one redirect URI
  to register in provider OAuth apps, deterministic behavior)
- Replace broken unicode checkmark with SVG icons (charset was missing,
  rendered as mojibake)
- Dark themed landing page with proper card layout for both success
  and error states
- Add charset=utf-8 to Content-Type headers

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: Unify OAuth callback server across all auth flows

All three OAuth flows (WASM tool auth, MCP server auth, NEAR AI login)
now share the same code from cli::oauth_defaults:

- Fixed port 9876 (one redirect URI to register per provider)
- Shared landing page HTML (dark card with SVG icons, proper charset)
- Parameterized wait_for_callback(listener, path, param, display_name)

Removes ~120 lines of duplicated callback/HTML code.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Support for oauth token refresh

* refactor: Replace bootstrap.json with ~/.ironclaw/.env for DATABASE_URL

Kill the 4-field BootstrapConfig JSON file. Only DATABASE_URL actually
needs disk persistence (chicken-and-egg before DB connect). The other
three fields are now derived: pool_size defaults to 10 via env var,
secrets master key is auto-detected (env then keychain probe), and
onboard_completed is inferred from DATABASE_URL presence.

The new format is a standard .env file loaded via dotenvy early in
main, so DATABASE_URL is available as a regular env var everywhere.

Handles three upgrade paths:
- Clean start: wizard writes .env, reload after wizard completes
- Returning user: .env loaded at startup, business as usual
- Legacy upgrade: bootstrap.json auto-migrated to .env on first run

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Address PR review findings

- Fix UTF-8 panic in truncate_for_preview (byte-slice on char boundary)
- Cap WASM guest timeout_ms at 5 minutes to prevent resource exhaustion
- Fix localhost detection in requires_auth() to avoid substring matches
  (e.g. "notlocalhost.com" no longer matches)
- Fix query param injection to insert before URL fragment
- Fix extract_host_from_url for IPv6 bracket notation
- Remove misleading schema defaults: Slack limit, Slides insertion_index,
  Docs index (per-action defaults documented in descriptions instead)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: Fix cargo fmt formatting

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: IPv6 loopback support for OAuth listener and localhost detection

- bind_callback_listener: try [::1] first, fall back to 127.0.0.1,
  so OAuth redirects work on systems where localhost resolves to ::1
- is_localhost_url: replace manual string parsing with url::Url for
  correct handling of IPv6 brackets, ports, userinfo, etc.
- Add url crate as direct dependency (already a transitive dep)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Address PR review feedback on runtime reuse, onboard check, and OAuth binding

- Remove session file check from check_onboard_needed(); DATABASE_URL is sufficient
- Detect AddrInUse on IPv6 bind and fail immediately instead of falling through to IPv4
- Reuse dedicated tokio runtime across HTTP calls in both tool and channel WASM wrappers

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: HTML-escape provider name in OAuth landing page, simplify Slack limit description

- Add html_escape() to prevent XSS in landing_html() where provider_name
  was interpolated directly into HTML (defense-in-depth, source is trusted
  but escaping costs nothing)
- Remove per-action default numbers from Slack limit field description to
  avoid confusing LLMs with conflicting defaults

Addresses review feedback from zmanian on PR #42.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Save all bootstrap fields from wizard, fix config module comment

- Wizard now saves secrets_master_key_source and database_pool_size to
  bootstrap.json (was only saving database_url and onboard_completed,
  which broke secrets after fresh onboard since SecretsConfig::resolve
  reads key source from bootstrap)
- Update config.rs module doc to reflect bootstrap.json priority chain
  instead of the removed ~/.ironclaw/.env approach

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: Replace BootstrapConfig with .env-based bootstrap

DATABASE_URL is the only setting that needs disk persistence before
the database is available. Instead of a custom bootstrap.json with 4
fields, use a standard ~/.ironclaw/.env file loaded via dotenvy.

- Remove BootstrapConfig struct entirely
- Restore ironclaw_env_path(), load_ironclaw_env(), save_database_url()
- SecretsConfig::resolve() now auto-detects (env var then keychain probe)
  instead of reading a saved source from bootstrap.json
- DatabaseConfig::resolve() reads DATABASE_URL from env only (dotenvy
  loads ~/.ironclaw/.env into the environment early in startup)
- check_onboard_needed() is now sync (just checks env vars)
- Wizard save_and_summarize() works for both postgres and libsql backends
- One-time migration from bootstrap.json to .env preserved

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Ensure load_ironclaw_env() runs in all Config paths, fix .env priority

- Config::from_env() and Config::from_db() now call load_ironclaw_env()
  internally (after dotenvy::dotenv()), so CLI commands like `memory`
  and `config` correctly load DATABASE_URL from ~/.ironclaw/.env
- Fix load order: standard ./.env first (higher priority), then
  ~/.ironclaw/.env, matching the documented priority chain
- Collapse nested if/if-let into let-chains (clippy::collapsible_if)
  in oauth_defaults.rs, tool.rs, and secrets/store.rs
- Fix rename_to_migrated to take &Path instead of &PathBuf

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Address PR review comments (quoting, SSRF, error mapping)

- Quote DATABASE_URL in .env writes so `#` in passwords isn't treated
  as a dotenv comment (e.g., `DATABASE_URL="postgres://..."`)
- Add SSRF defenses to refresh_oauth_token(): require HTTPS, reject
  private/loopback IPs (with DNS resolution), disable redirects.
  token_url comes from tool capabilities JSON, so a malicious tool
  could otherwise exfiltrate refresh tokens.
- Fix IPv4 bind error mapping: only map AddrInUse to PortInUse,
  use generic Io variant for other bind failures

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-14 21:21:22 +00:00
54e9206f0b feat: Move debug log truncation from agent loop to REPL channel (#65)
* feat: Move debug log truncation from agent loop to REPL channel

Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: truncating fmt layer for terminal, full logs for web gateway

Instead of truncating debug output at each LLM call site (fragile),
use a custom MakeWriter on the fmt layer that caps each tracing event
at 500 bytes before flushing to stderr. The web gateway WebLogLayer
still receives full untruncated content for /api/logs/events SSE.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: UTF-8 safe truncation in truncate_for_preview, remove double truncation

- Use char_indices() instead of byte-based slicing to find the cut
  point, preventing panics on multi-byte characters (emoji, CJK, etc.)
- Remove redundant truncation in REPL channel (agent loop already
  truncates ToolResult previews to 200 chars)
- Add 9 unit tests covering edge cases: empty, exact length, multi-byte
  UTF-8 (emoji, CJK), mixed scripts, newline collapsing, whitespace

Addresses PR #65 review comments.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-13 23:24:07 +00:00
ced83d5b4d feat: Sandbox jobs (#4)
* Orchestrating jobs and running them in sandboxes

* Fix heartbeat: dynamic max_tokens, empty content guard, notification fallback

- Query /v1/models API for context_length and set max_tokens to half
  (floor 4096) instead of hardcoded 1024; reasoning models like GLM-4.7
  need much larger budgets
- Guard against empty LLM content (reasoning models can burn all tokens
  on chain-of-thought and return content: null)
- Simplify notification routing: try configured channel first, fall back
  to broadcast_all so heartbeat alerts always reach someone
- Add ModelMetadata struct and model_metadata() to LlmProvider trait
- Refactor NearAiChatProvider::list_models into shared fetch_models()
- Add standalone test_heartbeat example for isolated debugging

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add job detail view with drill-down from jobs list

Click a job row to see full details across four sub-tabs:
Overview (metadata grid, description, state transitions timeline),
Actions (expandable tool call cards with input/output JSON),
Thinking (conversation messages styled by role), and
Files (embedded workspace tree browser).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Strip model-internal XML tags from LLM responses, fix Telegram parse_mode 400

Some models (GLM-4.7, etc.) emit <tool_call>tool_list</tool_call> in the
content field instead of using the OpenAI tool_calls array. This XML leaks
through to channels as text, and Telegram's Markdown parser chokes on the
underscores, returning 400 "can't parse entities".

Two fixes:
- Generalize clean_response() to strip <tool_call>, <function_call>,
  <tool_calls>, and pipe-delimited variants (<|tool_call|>) alongside
  the existing <thinking> tag stripping
- Add Telegram send_message helper with parse_mode fallback: try Markdown
  first, retry as plain text on "can't parse entities" 400 errors

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add SystemCommand submission type for thread-state-independent commands

System commands (/help, /model, /version, /tools, /ping, /debug) now
bypass thread-state checks and safety validation via a dedicated
Submission::SystemCommand variant. Previously these flowed through
process_user_input() which blocked them during Processing/AwaitingApproval
/Completed states.

- Add /model [name] for runtime model switching with provider validation
- Add active_model_name()/set_model() to LlmProvider trait with RwLock
  hot-swap in both NEAR AI providers
- Rewrite /help with aligned columns grouped by category
- Expand REPL tab-completion from 10 to 23 slash commands
- Remove REPL-local /help interception (now handled by agent)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add per-tool execution timeouts, auto-create sandbox project dirs, serve built files

The sandbox e2e pipeline (agent -> container -> built website -> browsable URL)
was broken by three gaps: hardcoded 60s timeouts killed sandbox jobs that need
minutes, no auto-created project directory meant container output vanished, and
no HTTP route to browse the built files.

- Add `execution_timeout()` to the `Tool` trait (default 60s), replace all four
  hardcoded `Duration::from_secs(60)` call sites (agent_loop, worker, scheduler,
  worker/runtime) with the per-tool value
- Override to 660s in `RunInSandboxTool` (10 min polling + 60s buffer)
- Auto-create `~/.ironclaw/projects/{uuid}/` when no `project_dir` is specified,
  so every sandbox job gets a persistent bind mount
- Include `project_dir` and `browse_url` in sandbox tool output JSON
- Add `/projects/{id}` and `/projects/{id}/{path}` static file serving routes
  to the web gateway with path traversal protection and MIME type detection
- Add `mime_guess` dependency for content-type detection

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Apply cargo fmt to wizard.rs after merge

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Persist sandbox jobs in DB, fix web UI, unify job model

Sandbox container jobs were invisible to the web UI because they lived
only in ContainerJobManager's in-memory HashMap while the API queried
ContextManager. This persists them to the agent_jobs table and fixes
all six front-end bugs (empty job list, broken back button, empty
actions/thinking tabs, wrong files tab, stuck status, no persistence).

Key changes:
- V4 migration adds project_dir and user_id columns to agent_jobs
- Embedded migrations via refinery (no external CLI needed)
- SandboxJobRecord CRUD in Store with fire-and-forget DB writes
- Unified job_id: sandbox tool generates UUID, passes to ContainerJobManager
- Web API queries DB for sandbox jobs, merges with ContextManager direct jobs
- New endpoints: restart, project file list/read with path traversal protection
- Front-end: rebuild DOM on back navigation, sandbox-aware tabs, job cards in
  chat stream, source badges, restart button for failed/interrupted jobs
- Gateway defaults to enabled, prints Web UI URL on startup
- Stale jobs marked "interrupted" on restart for visibility and restartability

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Secure in-chat auth: tokens never touch the LLM or chat history

Remove the token parameter from tool_auth so the LLM cannot pass raw
API keys. Add dedicated REST (POST /api/chat/auth-token) and WebSocket
(auth_token) endpoints that route tokens directly to ext_mgr.auth(),
completely bypassing the message pipeline, turns, history, and compaction.

Web UI shows an auth card (password input + OAuth button) when the agent
enters auth mode, submitted via the dedicated endpoint. CLI auth mode
interception is unchanged (already secure).

New StatusUpdate::AuthRequired/AuthCompleted variants propagate through
all channels (SSE, WebSocket, REPL, WASM).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Add Claude Code mode for sandbox jobs

Run Claude Code CLI inside Docker containers as an alternative to the
standard worker mode. The bridge spawns `claude -p` with stream-json
output, posts events to the orchestrator, and supports follow-up
prompts via `--resume`.

Key additions:
- `claude-bridge` CLI subcommand and ClaudeBridgeRuntime
- JobMode enum (Worker vs ClaudeCode) with per-mode container config
- Orchestrator endpoints for Claude events and prompt polling
- SSE event variants for real-time Claude Code streaming to frontend
- Claude Code sub-tab in web UI with terminal-style output and input bar
- Database migration for job_mode column and claude_code_events table
- ClaudeCodeConfig with env var support (CLAUDE_CODE_ENABLED, etc.)
- Mode parameter on run_in_sandbox tool schema

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Skip create_job tool when sandbox is enabled to prevent duplicate jobs

When sandbox mode is on, the LLM would call create_job (creating a
pending "direct" entry) then run_in_sandbox (creating a second "sandbox"
entry), producing two jobs in the list for a single user request.

Now register_job_tools() skips create_job when sandbox is enabled since
run_in_sandbox already creates tracked jobs. Also improved the
run_in_sandbox description to guide the LLM to use it directly and to
mention wait=false for async execution.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Web gateway UI quality-of-life improvements

Phase 1: Send button disabled state to prevent double-sends, copy button
on code blocks, confirm() guards on destructive actions, SSE-driven job
list auto-refresh, log filters re-applied on tab switch, jobEvents memory
leak fix (cap at 500, cleanup after 60s).

Phase 2: Toast notification system replacing chat-based system messages,
memory search highlighting with centered snippets, keyboard shortcuts
(Ctrl+1-5 tabs, Ctrl+K focus, Ctrl+N new thread, Escape close/blur),
activity tab toolbar with event type filter and auto-scroll toggle.

Phase 3: Thread sidebar with load/switch/create, thread_id passed with
messages, collapsible to hamburger. Memory inline editing with textarea,
Save/Cancel, POST to /api/memory/write.

Phase 4: Gateway status popover on hover (polls every 30s), extension
install form (name/URL/kind), markdown rendering in memory viewer for
.md files, mobile responsive layout at 768px breakpoint.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Add routines system, remove non-sandbox job mode from web UI

Routines: scheduled & reactive job system with cron and event triggers,
lightweight (single LLM call) and full-job execution modes, guardrails
(cooldown, max concurrent, dedup), and LLM-facing tools for CRUD.

Web UI: remove ContextManager-backed "direct" job mode entirely. Jobs
are now exclusively sandbox-backed (DB + container). Simplify job detail
response, drop dead types (ActionInfo, MessageInfo, MessageToolCallInfo),
fix Browse Files CSS loading (trailing-slash redirect), fix Activity tab
event rendering.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Re-enable chat input on agent completion, auto-auth on tool_activate, recover tool calls from content XML

Three fixes:

1. Chat input stays disabled after agent finishes: the "Done" status
   SSE event now calls enableChatInput() as a safety net when the
   response event is empty or lost. Same for auth_completed and
   cancelAuth().

2. tool_activate never triggers auth: when activation fails due to
   missing authentication, it now auto-initiates the auth flow
   (same pattern as the web API handler). detect_auth_awaiting()
   also matches tool_activate results now.

3. Models like GLM-4.7 emit tool calls as XML tags in content
   (<tool_call>tool_list</tool_call>) instead of using the structured
   tool_calls array. recover_tool_calls_from_content() extracts and
   validates these before falling back to plain text.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Add routines web UI tab, update docs for sandbox-jobs branch

Add full routines management to the web gateway (list, detail, trigger,
toggle, delete) with 7 new API endpoints, response types, and frontend
(HTML, JS, CSS). Update FEATURE_PARITY.md (~23 rows), CLAUDE.md (new
subsystems, config, TODOs), and README.md (architecture diagram,
features, components, fix onboard command).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Bind Telegram bot to owner account during setup

Without owner binding, anyone who discovers the bot can send it messages.
The setup wizard now prompts the user to message their bot, captures their
Telegram user ID via getUpdates, and persists it as telegram_owner_id in
settings. On startup, the owner_id is injected into the WASM channel config
so the existing owner restriction logic drops messages from non-owners.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Move settings from disk to PostgreSQL database

Settings previously lived in three JSON files on disk (settings.json,
mcp-servers.json, session.json). This made them inaccessible from the
web UI and caused redundant disk reads (Settings::load() called 8+
times during startup).

Now all settings live in a `settings` table (user_id + key -> JSONB)
with only 4 bootstrap fields remaining on disk (database_url, pool
size, secrets key source, onboard_completed) since they're needed
before the DB connection exists.

- Add V8 migration for settings table
- Add BootstrapConfig (thin disk file) and Settings DB round-trip
- Add Store CRUD methods for settings (get/set/delete/list/bulk)
- Refactor Config to load from DB (env > DB > default cascade)
- Add SessionManager DB persistence for session tokens
- Add DB-backed MCP server config load/save functions
- Add 6 settings web API endpoints (list/get/set/delete/export/import)
- Add one-time disk-to-DB migration on first boot
- Make CLI config commands async with DB access (disk fallback)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Seed workspace on boot, fix gateway duplicate logs and URL auto-auth

- Add Workspace::seed_if_empty() to create core identity files (README,
  MEMORY, IDENTITY, SOUL, AGENTS, USER, HEARTBEAT) when missing, called
  on every boot without overwriting existing user edits
- Remove duplicate gateway log lines from web/mod.rs (main.rs has the
  useful clickable ?token= URL)
- Auto-authenticate from ?token= URL parameter in the web UI and strip
  the token from the address bar after successful auth

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Harden sandbox security (path traversal + orchestrator auth)

Two vulnerabilities fixed:

1. project_dir path traversal: The create_job tool let the LLM specify
   arbitrary host paths for Docker bind mounts. Removed project_dir from
   the tool schema entirely, and added canonicalization + prefix validation
   at both resolve_project_dir() and the job_manager bind mount point.

2. Orchestrator API auth bypass: worker_auth_middleware was defined but
   never applied. Each handler manually called validate_token(), so any
   new endpoint that forgot would be publicly accessible. Applied the
   middleware as route_layer on all /worker/ routes, removed manual auth
   from all 7 handlers. Bind to 127.0.0.1 on macOS/Windows (Linux keeps
   0.0.0.0 since containers reach host via docker bridge, not loopback).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Rework gateway chat with pinned assistant, pagination, and NEAR AI response chaining

Implements the 4-phase plan for overhauling the web gateway chat:

- Phase 1: Pinned "Assistant" thread at top of sidebar, regular threads below
- Phase 2: Cursor-based history pagination with infinite scroll
- Phase 3: NEAR AI previous_response_id chaining (delta-only messages),
  with fallback to full history on chain errors, and DB persistence of
  chain state across restarts
- Phase 4: SSE thread isolation (events filtered by thread_id)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Add per-request HTTP timeout to WASM host, redact credentials in errors

Three fixes for WASM channel reliability:

1. Per-request timeout: Add optional timeout-ms parameter to http-request
   in both channel and tool WIT interfaces. Telegram long-poll now specifies
   35s (outliving the 30s server-side hold), while regular API calls use
   the 30s default. Fixes the triple-30s timeout race that caused polling
   failures.

2. Credential redaction: reqwest::Error includes the full URL (with injected
   bot tokens) in its Display output. Scrub credential values from error
   messages before logging or returning to WASM.

3. Webhook route registration: Remove tunnel URL gate so webhook routes are
   always available when webhook channels exist, not only when TUNNEL_URL
   is configured.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: Fix clippy warnings in WASM tools and channels

- slack channel: allow dead_code on signing_secret_name (forward compat field)
- gmail tool: use div_ceil() instead of manual (n+2)/3
- google-calendar tool: extract CreateEventParams/UpdateEventParams structs
  to fix too-many-arguments warnings

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Fix approval flow

* fix: Rebuild bundled telegram.wasm with updated WIT interface

The bundled WASM binary must match the host's WIT definition.
Previous binary was compiled against the old 4-arg http-request;
this rebuild includes the new timeout-ms parameter.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: Load WASM channels from disk instead of bundling in binary

Remove include_bytes! embedding of telegram.wasm. Channels are now
loaded from their build output directories (channels-src/<name>/target/)
during onboarding, then from ~/.ironclaw/channels/ at runtime.

- bundled.rs: locate_channel_artifacts() finds WASM + capabilities from
  build output; IRONCLAW_CHANNELS_SRC env var overrides the default path
- available_channel_names(): only lists channels with build artifacts
- bundled_channel_names(): lists all known channels (manifest)
- Setup wizard uses available_channel_names() to offer installable channels
- Add *.wasm to .gitignore, remove tracked telegram.wasm

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Persist gateway auth token, fix thread hydration race, polish auth screen

Three web gateway UX fixes:

1. Token persistence: Store auth token in sessionStorage so refreshing
   the page doesn't force re-authentication. Hide the auth screen
   immediately when a saved token exists to prevent flash.

2. Thread hydration: Remove the !msgs.is_empty() bail-out in
   maybe_hydrate_thread so that even brand-new (empty) assistant threads
   get hydrated with their correct DB UUID. Previously resolve_thread
   would mint a fresh UUID, causing messages to land in the wrong
   conversation and duplicate threads to appear.

3. Auth screen: Redesign as a centered card with brand, tagline, labeled
   input, and hint text.

Also adds 34 new tests covering session/thread lifecycle, thread
resolution isolation (user, channel, external ID), hydration edge cases,
serialization round-trips, approval flows, and stale mapping recovery.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: Use bindgen! for WASM tool wrapper, add dev tool loading

Three changes:

1. Rewrite src/tools/wasm/wrapper.rs to use wasmtime::component::bindgen!
   instead of manual linker.root().func_wrap(). This fixes the
   "component imports instance 'near:agent/host', but a matching
   implementation was not found in the linker" error. All 6 host functions
   (log, now-millis, workspace-read, http-request, secret-exists,
   tool-invoke) are now properly registered under the near:agent/host
   namespace. Also adds WASI support, credential injection, and leak
   detection for HTTP requests made by WASM tools.

2. Add dev tool loading to src/tools/wasm/loader.rs. During startup, the
   loader now also scans tools-src/*/target/wasm32-wasip2/release/ for
   build artifacts that are newer than installed copies. This means during
   development you just rebuild the WASM and restart the host; no manual
   copy step needed. Set IRONCLAW_TOOLS_SRC to override the source dir.

3. Wire up load_dev_tools() in main.rs alongside the existing
   load_from_dir() call.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: Wire main startup and CLI to use DB-backed settings

main.rs now reloads Config from the database after connecting,
attaches the store to the session manager for dual-write tokens,
and loads MCP servers from DB instead of disk. ExtensionManager
and MCP CLI commands use DB when available with disk fallback.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-11 08:31:25 +00:00
e6725eb6d9 feat: Improve CLI (#5)
* Start working on improved CLI

* Add tool result previews, boxed approval card, and polished help screen

REPL iteration 2: styled /help with grouped sections, box-drawing
approval card with colored params, dim separator before responses,
inline tool output previews via new StatusUpdate::ToolResult variant.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-09 05:42:41 +00:00
Illia PolosukhinandClaude Opus 4.6 bf3b8b339f Fix MCP tool calls, approval loop, shutdown, and improve web UI
- Fix MCP tool schema deserialization: rename input_schema to match
  protocol's camelCase inputSchema, so models receive actual parameter
  schemas instead of empty defaults
- Fix conversation history: add tool_calls field to ChatMessage and
  include assistant message with tool_calls before tool results, as
  required by OpenAI-compatible APIs
- Fix approval loop: pass resume_after_tool flag to run_agentic_loop
  so the "force tool use" heuristic doesn't re-trigger after approval
- Fix shutdown: add Submission::Quit, Ctrl+C signal handler, and
  graceful shutdown flow
- Fix MCP activate button: auto-attempt auth flow when activation
  fails due to missing authentication
- Add inline approval cards in chat via SSE ApprovalNeeded events
- Add markdown rendering in chat (marked.js) with proper streaming
- Add structured fields to log entries (key=value pairs from tracing)
- Collapse log entries to single line with click-to-expand

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-06 18:04:44 -08:00
Illia PolosukhinandClaude Opus 4.6 4d0fe7d37e Replace TUI (ratatui) with REPL (rustyline + termimad)
Drop the full Ratatui TUI in favor of a lighter REPL channel built on
rustyline (line editing, history, tab-completion) and termimad (inline
markdown rendering). Removes ratatui and crossterm event-stream deps,
adds rustyline and termimad. Simplifies main.rs startup to use the REPL
directly instead of the alternate-screen TUI.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-06 09:00:32 -08:00
Illia PolosukhinandClaude Opus 4.6 e0016a95e8 Add Telegram typing indicator via WIT on-status callback
Thread message metadata through Channel::send_status so WASM channels
can route status updates (like typing indicators) to the correct chat.
The WasmChannel spawns a background task that repeats on_status every
4 seconds to keep Telegram's typing bubble alive until the response
is sent.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-05 19:44:43 -08:00
Illia PolosukhinandClaude Opus 4.5 598dd43b1c Rebrand to IronClaw with security-first mission
Renamed project from "near-agent" to "ironclaw" throughout the codebase.
Updated documentation to emphasize the core philosophy:
- Your data stays yours (local, encrypted, no telemetry)
- Self-expanding capabilities (build tools on the fly)
- Defense in depth (WASM sandbox, prompt injection defense)
- Always on user's side

Key changes:
- Package name: near-agent -> ironclaw
- Config paths: ~/.near-agent/ -> ~/.ironclaw/
- Database name in docs: near_agent -> ironclaw
- CLI binary: near-agent -> ironclaw
- Log filters: RUST_LOG=near_agent -> RUST_LOG=ironclaw
- All user-facing strings (welcome messages, help text, etc.)

Preserved for compatibility:
- HKDF salt "near-agent-secrets-v1" (changing would break existing secrets)
- WIT interface names (near::agent::*)
- NEAR AI provider config (NEARAI_* env vars)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-05 01:10:40 -08:00
Illia PolosukhinandClaude Opus 4.5 7ef6362d83 Add Chat Completions API support and expand REPL debugging
- Add NearAiChatProvider using /v1/chat/completions endpoint with API key auth
- Add NEARAI_API_KEY and NEARAI_API_MODE config options
- Auto-detect API mode from presence of API key
- Keep existing Responses API (NearAiProvider) for session-based auth
- Fix response parsing to accept input_text/output_text/text content types
- Expand REPL with /help, /debug toggle, colored output
- Better tool status display (dots vs verbose based on debug mode)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-03 21:55:05 -08:00
Illia Polosukhin 2c26ba8431 Support non interactive mode and model selection 2026-02-03 14:47:26 -08:00