Skip to content

chore: promote staging to staging-promote/79c1b0fd-24108317021 (2026-04-08 00:16 UTC) - #2131

Merged
henrypark133 merged 31 commits into
staging-promote/79c1b0fd-24108317021from
staging-promote/288fe49a-24110798843
Apr 10, 2026
Merged

henrypark133 merged 31 commits into
staging-promote/79c1b0fd-24108317021from
staging-promote/288fe49a-24110798843

Conversation

@ironclaw-ci

@ironclaw-ci ironclaw-ci Bot commented Apr 8, 2026 •

Copy link
Copy Markdown
Contributor

Auto-promotion from staging CI

Batch range: a55aff980a4e235590c3af57ded2542512e2f9f6..288fe49aada48aacb206a0a341ada180522be044
Promotion branch: staging-promote/288fe49a-24110798843
Base: staging-promote/79c1b0fd-24108317021
Triggered by: Staging CI batch at 2026-04-08 00:16 UTC

Commits in this batch (51):

Current commits in this promotion (0)

Current base: staging-promote/79c1b0fd-24108317021
Current head: staging-promote/288fe49a-24110798843
Current range: origin/staging-promote/79c1b0fd-24108317021..origin/staging-promote/288fe49a-24110798843

  • (no non-merge commits in range)

Auto-updated by staging promotion metadata workflow

Waiting for gates:

  • Tests: pending
  • E2E: pending
  • Claude Code review: pending (will post comments on this PR)

Auto-created by staging-ci workflow

henrypark133 and others added 5 commits April 7, 2026 16:37
* fix(web): intercept approval text input ("yes"/"no"/"always") in chat

When a tool requires approval in the web UI, typing "yes", "no", or
"always" in the chat input now resolves the approval card directly
instead of sending a regular message. This prevents duplicate approval
prompts and "No pending approval" errors that occurred when text went
through the backend message pipeline.

The frontend intercepts approval keywords in sendMessage() and routes
them through sendApprovalAction() — the same code path as clicking
the Approve/Deny/Always buttons on the card.

[skip-regression-check]

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

* fix: find most recent unresolved approval card for text interception

Address review feedback: instead of checking the last card and then
separately checking if it's resolved, find the most recent unresolved
card directly. Handles the edge case where the last card is resolved
(during 1.5s removal animation) but an earlier one isn't.

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

* test: add E2E test for skip-resolved-card behavior

Addresses review feedback: adds a test where two approval cards are
visible, the newer one is resolved via button click, then typing "yes"
correctly targets the older unresolved card instead of falling through.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n visibility bug (#2126)

Missions created via the agent were invisible in the gateway UI for
non-owner users because list_engine_missions/list_engine_threads fell
back to the engine owner's default_project_id instead of resolving
the authenticated user's per-user project.

Broader change: replace three inconsistent ownership patterns
(can_act_on, engine is_owned_by, raw string comparisons) with a
single Owned trait providing uniform is_owned_by(user_id) checks
across jobs, routines, and all internal code paths.

- Fix project_id resolution in list_engine_missions/list_engine_threads
- Add Owned trait to src/ownership with impls on AgentJobRecord,
  SandboxJobRecord, Routine, and JobContext
- Migrate all can_act_on calls in web handlers (jobs.rs, routines.rs)
- Migrate raw user_id comparisons in tenant.rs, commands.rs,
  routine_engine.rs, context/manager.rs, tools/builtin/job.rs
- Add missing ownership check in routines_trigger_handler
- Migrate active routines_runs_handler and verify_project_ownership
  in server.rs
- Remove dead can_act_on function and ownership_identity helper
- Add regression tests for concrete Owned impls on real types

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…2129)

* fix(e2e): canonicalize extension names in configure/setup + update tests

1. Bug fix: `configure()` and `get_setup_schema()` in ExtensionManager
   called `validate_extension_name()` which discards the canonical form.
   Hyphenated names from URL paths (e.g., `web-search`) were used raw
   for capabilities file lookups, causing "Capabilities file not found"
   errors. Now both methods canonicalize with `canonicalize_extension_name()`
   so `web-search` → `web_search` before any file I/O.

2. Bug fix: `extensions_setup_handler` in server.rs compared raw URL
   path param against canonical stored names in the kind lookup.

3. Test fix: `test_wasm_lifecycle.py` — update all `web-search` refs
   to `web_search` (canonical form returned by registry/API).

4. Test fix: `test_mcp_auth_flow.py` — update `mock-mcp` / `mock-mcp-400`
   to `mock_mcp` / `mock_mcp_400`.

5. Test fix: `test_chat.py` — `test_send_message_and_receive_response`
   now counts assistant messages before sending to avoid picking up the
   pre-existing onboarding greeting as the LLM response.

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

* fix: use centralized SEL selector in wait_for_function JS call

Address review feedback: pass the selector from SEL dict into the
JS function instead of hardcoding '#chat-messages .message.assistant'.

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

* fix: use send_chat_and_wait_for_terminal_message helper for robustness

Replace manual count+wait_for_function with the existing helper that
correctly handles streaming chunks and waits for the terminal message.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…2130)

* fix(engine): repair mission ACL regression and 4 stale engine tests

The mission access control in pause_mission/resume_mission had a
logic error that allowed any user to manage shared/system missions.
The `&& !mission.owner_id().is_shared()` condition short-circuited
the entire check for shared missions. Replaced with proper two-branch
logic using is_shared_owner().

Also fixed 4 test assertions that checked thread.messages instead of
thread.internal_messages (orchestrator stores working messages in the
internal transcript), and made the trace test resilient to event
ordering by filtering by EventKind.

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

* fix(engine): extend ACL fix to update_mission, error on missing missions

Address review feedback:
- Fix same shared-mission ACL bug in update_mission (line 130)
- pause_mission/resume_mission now error on missing missions instead
  of silently proceeding, matching update_mission's pattern
- Update doc comments to reflect that the engine enforces shared
  ownership checks directly

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): remove silent cross-tenant credential fallback in WASM wrappers (#2069, #2070)

WASM tool credential resolution silently fell back to looking up secrets
under the hardcoded "default" scope when the calling user had no credential
configured, leaking the instance owner's API keys to other users without
error or audit trail.

- Remove "default" fallback in resolve_host_credentials(); return
  Err(ToolError::NotAuthorized) with actionable message instead of
  silently skipping missing credentials
- Fix resolve_websocket_identify_message() to accept owner_scope_id
  parameter instead of hardcoding "default"
- Document legacy broadcast metadata fallback with removal tracking
- Document setup.rs boot-time owner_id lookups as intentional
  instance-level resource ownership
- Add regression tests proving cross-tenant credentials do not leak

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(review): differentiate expired vs missing credential errors, exclude UrlPath from store check

Address PR review feedback:
- Filter out UrlPath credentials in the no-store check so tools with
  only UrlPath mappings don't incorrectly get NotAuthorized
- Match SecretError::Expired separately to produce "has expired" message
  instead of misleading "not found"
- Add tests for both: UrlPath-only no-store (Ok), expired credential
  (specific error message)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(review): map backend SecretErrors to ExecutionFailed, not NotAuthorized

Address @serrrfirat review: Database, DecryptionFailed, KeychainError,
and other backend errors were incorrectly mapped to "not found". Now:
- NotFound → ToolError::NotAuthorized ("not found, configure via secrets set")
- Expired → ToolError::NotAuthorized ("has expired, refresh or re-set")
- All others → ToolError::ExecutionFailed (preserves real cause)

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

* fix(review): AccessDenied → NotAuthorized, fix issue refs, reduce visibility

- Map SecretError::AccessDenied to ToolError::NotAuthorized (not
  ExecutionFailed) since it's an authorization failure
- Update legacy fallback comments to reference #2100 (the tracking
  issue) instead of #2069
- Revert resolve_websocket_identify_message to private — test uses
  super:: import instead of pub(crate) path

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

* fix(review): redact secrets in Debug impl, document all-or-nothing invariant

- Replace #[derive(Debug)] on ResolvedHostCredential with custom impl
  that redacts secret_value and auth headers to prevent latent leakage
- Add comment documenting that all declared non-UrlPath credentials are
  required — tool execution fails on first missing credential rather
  than running with partial auth

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

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions github-actions Bot added scope: agent Agent core (agent loop, router, scheduler) scope: channel/web Web gateway channel scope: channel/wasm WASM channel runtime scope: tool/builtin Built-in tools scope: tool/wasm WASM tool sandbox scope: extensions Extension management size: XL 500+ changed lines risk: high Safety, secrets, auth, or critical infrastructure contributor: core 20+ merged PRs labels Apr 8, 2026
henrypark133 and others added 6 commits April 7, 2026 20:05
* chore(engine): rename ENGINE_V2_TRACE to IRONCLAW_RECORD_TRACE

Aligns the trace-recording env var with the project-wide IRONCLAW_*
naming convention so it's discoverable alongside other ironclaw flags.

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

* unify engine v2 trace recording with v1 RecordingLlm

Address PR review feedback. Instead of having two separate trace
systems both named IRONCLAW_RECORD_TRACE (the v1 RecordingLlm in
src/llm/recording.rs and the engine v2 executor/trace.rs JSON dumper),
collapse them to one.

Engine v2's LlmBackend is wired to the host's full LLM provider chain,
which already includes RecordingLlm when IRONCLAW_RECORD_TRACE=1.
That means engine v2 LLM interactions are already captured by the
unified trace_*.json fixture file -- no engine-side env var, no second
JSON output, no risk of one flag enabling two divergent recorders.

Removed:
- is_trace_enabled() and write_trace() from executor/trace.rs
- the engine_trace_*.json write site in runtime/manager.rs

Kept (still useful, runs unconditionally for the self-improvement
mission):
- build_trace() / analyze_trace() / log_trace_summary()

Docs updated to point to RecordingLlm as the single trace mechanism.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ner (#2042)

* test: add Slack E2E tests, Rust integration tests, and smoke runner

Replicate the Telegram test infrastructure for the Slack WASM channel:
- Add Slack URL rewriting in wrapper.rs for test API redirection
- Create fake_slack_api.py mock server for E2E tests
- Add 12 Python E2E tests covering setup, DM, mentions, auth, threads, files
- Add 12 Rust integration tests for WASM channel behavior
- Add conftest.py fixtures for isolated Slack test instances
- Add local smoke test runner for pre-release validation with real Slack

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

* fix: wrap env::set_var/remove_var in unsafe blocks for Rust 1.83+

CI uses Rust 1.94 which requires unsafe blocks for std::env::set_var
and std::env::remove_var. Wrap the test-only calls in unsafe blocks
with safety comments.

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

* fix: address PR review feedback

- Replace fragile time.time()-1 fallback with explicit SmokeError in
  run_smoke.py attachment case (reviewer finding #1)
- Add OnceLock<Mutex> guard around env var mutation in wrapper.rs unit
  test to prevent parallel test races (reviewer finding #2)
- Extract duplicated git-worktree discovery into find_project_file()
  helper in slack_auth_integration.rs (reviewer finding #3)

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

* test(channels): generalize WASM HTTP test rewrites

* fix(channels): gate Slack test URL rewrites from release builds

* fix(ci): update wrapper test pairing store ctor

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* chore(ci): add Dependabot and pin GitHub Actions by SHA

Add automated dependency vulnerability scanning via Dependabot for both
Cargo crates (weekly) and GitHub Actions (weekly). Pin all 101 external
action references across 14 workflow files to full commit SHAs to prevent
supply-chain attacks via compromised tags.

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

* fix(ci): harden workflows — persist-credentials, permissions, template injection

Address zizmor security audit findings:
- Add persist-credentials: false to all checkout steps (artipacked)
- Add explicit minimal permissions to all workflows (excessive-permissions)
- Move workflow-level write permissions to job level where possible
- Fix template injection in regression-test-check.yml by using env vars

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

* fix(ci): address PR review comments

- Group Dependabot updates by ecosystem to reduce PR noise (gemini)
- Add persist-credentials: false to docker.yml checkout (Copilot)
- Move inputs.tag and other expansions to env vars in docker.yml to
  eliminate template injection from workflow_dispatch user input

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

* fix(ci): restore git push auth and harden git fetch

Address PR #2043 review comments:
- staging-ci create-promotion-pr: generate App token before checkout
  and pass it to checkout so 'git push origin "$BRANCH"' works
- staging-ci update-tag: re-enable credential persistence so the
  'staging-tested' tag force-push succeeds (job is internal-only)
- release update-registry-checksums: re-enable credential persistence
  so the checksum-update branch push succeeds
- regression-test-check: add '--' to git fetch to prevent refs that
  start with '-' from being interpreted as options

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

* fix(ci): address serrrfirat PR review comments

- release.yml: move github.ref_name, needs.plan.outputs.tag, and
  needs.plan.outputs.tag-flag to env vars across plan, build-local-artifacts,
  build-global-artifacts, and host jobs. The tag pattern
  '[0-9]+.[0-9]+.[0-9]+*' has a trailing glob, so a tag like
  '1.2.3\$(curl evil)' could match and be shell-expanded.
- dependabot.yml: split Cargo groups into tokio-ecosystem, serialization,
  wasm, and everything-else to make regression bisection easier when
  CI fails on a Dependabot PR.

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

* fix(ci): add missing job-level permissions for gh CLI calls

- resolve-promotion-base: add pull-requests: read for 'gh pr list'
- gate: add checks: read for 'gh api .../commits/{sha}/check-runs'

Both were dropped when workflow-level permissions moved to job level.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Zaki Manian <zaki@iqlusion.io>
serrrfirat and others added 7 commits April 8, 2026 23:23
* feat: port ratatui tui onto staging

* Add TUI model picker for /model

* Fix TUI CI lint failures

* Format /tools output as vertical list

* Restore TUI approval modal on thread switch

* Re-emit pending approval events on follow-up messages

* Improve TUI thread handling and activity UI

* Sort TUI resume conversations by activity

* fix(tui): address PR review feedback

* Add TUI thread detail modal for activity sidebar

* feat(tui): improve conversation scrolling UX

- Mouse wheel: 1-line increments (was 3-line jumps)
- PageUp/PageDown: full-page scroll based on viewport height (was 5 lines)
- Add scrollbar widget on conversation right edge (track │, thumb ┃)
- Add "↓ N more ↓ End to return" indicator when scrolled up
- Add auto-follow (pinned_to_bottom) that disengages on scroll-up
  and re-engages when reaching bottom or pressing End
- Clamp scroll offset to valid range (can't scroll past content)
- Add End key binding to jump to bottom

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

* fix(tui): use engine context pressure data for status bar

The context bar was using cumulative session tokens (total_input +
total_output) which grow unboundedly across turns, making the bar
always show 100% after a few exchanges. Now uses the actual context
window usage from ContextPressure events when available, falling back
to cumulative tokens only before the first engine update arrives.

Also syncs context_window from the engine's max_tokens so the limit
reflects the real model capability instead of name-based heuristics.

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

* fix(tui): render markdown in thread detail modal

The thread detail modal was displaying raw markdown text (plain
line splitting). Now uses render_markdown() for proper formatting
of headers, lists, bold, code blocks, etc.

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

* feat(tui): hydrate sidebar with engine threads and routines at startup

The TUI sidebar was empty until the first user message because
EngineThreadList and RoutineUpdate events were only sent after
processing a message. Now sends initial data right before the
message loop so the activity panel shows existing threads and
routines immediately on startup.

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

* fix(tui): use owner_id for engine thread hydration at startup

list_engine_threads filters by user_id, so passing "" matched no
threads. Now uses self.owner_id() which matches the TUI channel's
user_id, so threads are visible in the sidebar immediately.

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

* fix(tui): fix CI — type errors and formatting in TUI tests

Wrap `started_at` and `updated_at` in `Some(...)` to match
`Option<DateTime<Utc>>` after upstream struct change, and run
`cargo fmt` on files with formatting drift.

[skip-regression-check]

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

* fix(ci): resolve clippy warnings — collapsible ifs and needless borrow

Collapse three nested `if` blocks into `if && let` chains and remove
a needless `&` on the `process_list_threads` call, all in agent_loop.rs.

[skip-regression-check]

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

* fix(ci): add live_harness.rs with updated StatusUpdate patterns

The live_harness.rs file was added to staging after this branch diverged.
When CI merges the PR into staging, the file uses old StatusUpdate patterns
that don't account for the new `detail` and `call_id` fields added by this
branch. Add the file with `..` rest patterns to fix the merge-time compile
errors.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(engine): add skill repair learning loop

* fix(engine): guard skill repair mission updates

* fix(engine): persist skill repair provenance

* fix(engine): address skill-repair PR review feedback

- Fix hex formatting: iterate GenericArray bytes individually instead of
  relying on Display impl which produces debug-like output
- Always recompute content hash from actual doc.content when archiving a
  revision to prevent drift from out-of-band writes
- Prune repair history on rollback to remove records for versions newer
  than the one being restored
- Combine collect_error_messages + collect_observed_actions into a single
  pass (collect_errors_and_actions) to avoid redundant event iteration
- Document bounded revision eviction policy (cap at 10)
- Add comment clarifying concurrent skill-repair / error-diagnosis triggers

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

* fix(engine): constrain skill repair updates

* fix(engine): keep insights on completed threads

* style(engine): satisfy fmt and clippy on mission.rs

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
* allow private local llm endpoints

* Fix private endpoint review issues

* Fix link-local clippy warning

* Tighten base URL validation follow-ups

* fix(config): non-blocking DNS validation and admin-only LLM key filtering

Address PR #1955 review feedback:

- Wrap to_socket_addrs() in tokio::task::block_in_place when called from
  a multi-threaded async runtime so the LLM utility handlers
  (/api/llm/test_connection, /api/llm/list_models) can no longer stall a
  worker thread on slow DNS. Synchronous callers (env config, CLI) are
  unaffected.

- Add ADMIN_ONLY_LLM_SETTING_KEYS + strip_admin_only_llm_keys helper as
  defense-in-depth: Config::from_db_with_toml and re_resolve_llm_with_secrets
  now take an is_operator flag and strip admin-only base-URL-bearing keys
  (llm_builtin_overrides, llm_custom_providers, ollama_base_url,
  openai_compatible_base_url) from the DB merge for non-operator users.
  This guards future per-user resolve paths and any pre-existing legacy
  rows from reactivating a private/loopback endpoint via the operator
  validation policy. Existing call sites pass true (owner_id is the
  operator scope).

Adds regression tests covering:
  * strip_admin_only_llm_keys removes all four keys, leaves others
  * validate_base_url is callable from a multi-thread tokio runtime
    without panicking on the strict short-circuit path
  * validate_operator_base_url remains callable from async handlers
  * re_resolve_llm filters admin-only keys when is_operator=false and
    keeps them when is_operator=true

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

* refactor(settings): de-dup admin-only LLM key list (#1955 review)

`handlers/settings.rs::is_admin_only_setting_key` now delegates to
`crate::config::helpers::ADMIN_ONLY_LLM_SETTING_KEYS` so the write-side
gate cannot drift from the read-side `strip_admin_only_llm_keys` filter.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(workspace): admin system prompt shared with all users (#2088)

Introduce SYSTEM.md in a well-known __admin__ scope so admins can set a
system prompt that all tenants receive. Gated behind multi-tenant mode
(WorkspacePool sets admin_prompt_enabled on each workspace; owner
workspace in app.rs also gets the flag when has_any_users() is true).

New endpoints:
- GET  /api/admin/system-prompt — read admin system prompt
- PUT  /api/admin/system-prompt — set admin system prompt (64 KB limit)

Safety:
- SYSTEM.md added to injection scan list
- is_reserved_scope() guard on user creation (defense-in-depth)
- Multi-tenancy gate on both API and prompt assembly layers

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

* chore: remove review audit file from tracked files

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

* fix: add 64 KB size limit to admin system prompt PUT handler

Addresses PR review feedback:
- Enforce 64 KB limit on system prompt content to prevent token budget
  exhaustion (the content is injected into every user's system prompt)
- Add regression tests for the size limit (413 for oversized, not-413
  for at-limit)
- Document that is_multi_tenant is evaluated once at startup and the
  owner workspace requires a restart after the first user is created

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

* fix: address remaining review feedback on admin system prompt

- Restore rustdoc comments stripped from document.rs (DocumentMetadata,
  HygieneMetadata, DocumentVersion, VersionSummary, PatchResult, etc.)
  to keep the diff focused on feature additions only
- Replace silent error swallowing (if let Ok) with discriminated match
  in admin prompt read — only DocumentNotFound is silent, other errors
  logged at debug! level
- Cache admin system prompt on WorkspacePool to avoid an extra DB read
  on every turn; invalidated on PUT via invalidate_admin_prompt()
- Add cache invalidation integration test

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

* fix(workspace): tighten reserved-scope check and admin-prompt body limit

- is_reserved_scope: case-insensitive, whitespace-tolerant, and reserves
  the entire `__*__` namespace so future system scopes (alongside
  `__admin__`) cannot be impersonated by hand-crafted user IDs
- admin system-prompt route: layer-level DefaultBodyLimit of 128 KB
  rejects oversized payloads before JSON parse, complementing the
  in-handler 64 KB content cap
- system_prompt put_handler: clarify that the in-handler size check is
  a clearer-error fallback for the layer cap
- users_create_handler: drop the dead is_reserved_scope check on a
  freshly-minted UUID; the guard belongs at a code path that actually
  accepts user-supplied IDs
- expand is_reserved_scope tests for case, whitespace, and the wider
  `__*__` namespace

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
* Fix skill installs for invalid catalog names

* Fix clippy test module ordering

* fix: address PR review feedback

* fix: use PairingStore::new_noop() in SSRF test after merge with staging

The staging branch introduced a new test (test_http_request_rejects_private_ip_targets)
that calls PairingStore::new(), but this branch changed the signature to require
db and cache arguments. Use new_noop() since this is a test context.

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

* fix: address PR #2040 review — remove expect() and dead strip_prefix

- Restructure download_key flow in skills_install_handler to use the
  value directly instead of round-tripping through Option + expect(),
  satisfying the no-expect-in-production-code rule.
- Remove dead strip_prefix("---\n") in render_skill_md — serde_yml does
  not emit a leading document marker for structs.

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

* fix(skills): preserve unknown frontmatter and tighten install matching

- rewrite install-recovery to mutate the `name` field via raw YAML
  Value rather than re-serializing the typed SkillManifest, so unknown
  frontmatter keys (vendor extensions, future fields) survive the
  install rewrite
- catalog_entry_is_installed: case-insensitive comparison for the
  display-name and normalized-slug branches, matching the slug branch
- normalize_skill_identifier: document non-ASCII handling
- normalizing-invalid-name log: warn -> debug (REPL/TUI rule)
- add round-trip test asserting unknown top-level keys, nested
  mappings, and sequences survive install recovery

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
The test used a hyphenated channel name ("test-failing-channel") but
canonicalize_extension_name() converts hyphens to underscores. This
caused configure() to look for "test_failing_channel.capabilities.json"
which didn't exist, returning an early Err before reaching the
activation code path the test was designed to exercise.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1770

chore: promote staging to staging-promote/bb2c3e1d-24154330911 (2026-04-08 21:41 UTC)
…0911

chore: promote staging to staging-promote/315c4cf8-24151502580 (2026-04-08 19:29 UTC)
…2580

chore: promote staging to staging-promote/8aa09412-24146405548 (2026-04-08 18:23 UTC)
…5548

chore: promote staging to staging-promote/482ee57c-24140982267 (2026-04-08 16:25 UTC)
…2267

chore: promote staging to staging-promote/3df1bf38-24132614443 (2026-04-08 14:32 UTC)
…4443

chore: promote staging to staging-promote/10d970d4-24125728989 (2026-04-08 11:19 UTC)
…8989

chore: promote staging to staging-promote/fdb093fb-24118285581 (2026-04-08 08:28 UTC)
…5581

chore: promote staging to staging-promote/288fe49a-24110798843 (2026-04-08 04:48 UTC)
@github-actions github-actions Bot added scope: channel Channel infrastructure scope: tool Tool infrastructure scope: tool/builder Dynamic tool builder scope: workspace Persistent memory / workspace scope: ci CI/CD workflows scope: docs Documentation scope: dependencies Dependency updates labels Apr 9, 2026
henrypark133 and others added 6 commits April 9, 2026 11:52
* fix(ci): target wasm32-wasip2 in WASM build script

cargo-component defaults to wasm32-wasip1 in CI, placing the binary at
the wrong path. All slack_auth_integration tests panic because they
look for the module at the wasm32-wasip2 target directory.

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

* test: add regression test for wasm32-wasip2 build target

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Zaki Manian <zaki@iqlusion.io>
…on (#1328) (#2101)

* fix(db): repair V6 migration checksum and guard against re-modification (#1328)

PR #1151 modified the already-released migrations/V6__routines.sql in
place, causing refinery's checksum validation to abort startup on every
existing PostgreSQL deployment upgrading to v0.19.0.

Revert V6 to its v0.18.0 content (V13 already applies the schema change
incrementally and is idempotent for fresh installs that received the
modified V6). Add a runtime checksum realignment step that rewrites
refinery_schema_history rows whose stored checksum disagrees with the
embedded SQL — this handles both populations of databases in the wild
(pre-#1151 originals and post-#1151 fresh installs).

Add migrations/checksums.lock pinning every migration's SipHasher13
checksum and a `released_migrations_are_immutable` cargo test that
fails if any migration is modified or added without a matching lockfile
entry. A second hard-coded sentinel test pins V6's literal v0.18.0
checksum so the guard cannot be defeated by editing both the migration
and the lockfile in the same commit.

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

* fix(db): address review feedback on migration_fixup (#2101)

- Drop hard-coded `public.` schema qualifier from the existence probe
  so PostgreSQL resolves `refinery_schema_history` via the active
  search_path, matching how refinery itself locates the table and how
  the subsequent UPDATE statement is written. Without this, deployments
  using a non-default schema would silently skip the realignment.
- Use `IS DISTINCT FROM` instead of `<>` so a corrupted row with a NULL
  checksum is repaired rather than silently skipped.
- Add `explanation` field to `KnownDivergence` and use it in the
  realignment warning so future entries are not coupled to the V6/#1328
  wording.

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

* fix(db): narrowly whitelist V6 known-bad checksum (#2101)

Per @serrrfirat's review, the previous IS DISTINCT FROM-based realignment
would silently rewrite *any* non-canonical V6 checksum, masking unrelated
corruption or manual tampering instead of narrowly exempting the one
historical released mismatch.

Add a `known_bad_checksums: &'static [u64]` field to `KnownDivergence`
listing the exact historical bad value(s), and rewrite only rows whose
stored checksum is in that whitelist via `WHERE checksum = ANY($4)`.
Anything else is left alone so refinery still aborts startup loudly.

The single known-bad V6 value (`11230857244097235596`) is the SipHasher13
of `git show 878a67c:migrations/V6__routines.sql` (the post-#1151
content) and is pinned by a new sentinel test
`v6_known_bad_checksum_matches_post_1151_content` so the whitelist
cannot drift or be silently widened.

Also adds an ignored bootstrap helper `compute_checksum_for_external_file`
for computing checksums of external SQL files when adding future entries.

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

* fix(db): assert in release + add postgres integration test (#2101)

Address two follow-up review comments from @serrrfirat:

1. The defensive `debug_assert!` guarding against the canonical
   checksum being listed in `known_bad_checksums` is stripped in
   release builds, so the safety net was absent in production.
   `KNOWN_DIVERGENCES` has at most a handful of entries — switch to
   `assert!` so the guard runs in release too. Cost is one constant-
   time slice lookup per startup.

2. The `realign_diverged_checksums` SQL path was never exercised
   against a real database. Refactor into a thin pub wrapper plus an
   injectable `realign_diverged_checksums_with` inner helper, and add
   a `#[cfg(feature = "integration")]` test that:
   - skips gracefully if no DATABASE_URL is reachable
   - creates `refinery_schema_history` if missing
   - seeds a synthetic V99999 row with a deliberately-wrong checksum
   - calls the realignment with a custom divergence list (no collision
     with real V6 rows in shared CI databases)
   - asserts the row now holds the canonical checksum
   - re-runs the realignment and asserts a no-op

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

* fix(db): replace assert! with returned error to satisfy no-panics check (#2101)

The previous commit changed `debug_assert!` → `assert!` to keep the
canonical-in-known-bad-list guard active in release builds, but this
trips the project's "No panics in production code" CI check (the
regex matches `assert!` outside test attributes).

Replace with an early `return Err(DatabaseError::Migration(...))` so
the guard still runs in release builds — startup refuses to proceed
with a misconfigured `KNOWN_DIVERGENCES` table — without using a
panicking macro. This is also more idiomatic for a function that
already returns `Result`.

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

* fix(db): address review follow-ups on migration_fixup (#2101)

- parse_lockfile() now panics on duplicate migration keys instead of
  silently overwriting earlier entries — a stray duplicate could mask
  the actual pinned checksum and weaken the immutability guard
  (Copilot review).
- Add `rejects_canonical_in_known_bad_checksums` integration test
  exercising the defensive Err path that refuses startup when a
  KnownDivergence has its canonical checksum listed in its own
  known_bad_checksums list (serrrfirat review).
- Document why `tracing::warn!` is intentional in the realignment
  fix-up despite CLAUDE.md's warning about info!/warn! corrupting the
  TUI: this code runs at startup before any channel/REPL/TUI is
  initialized, so terminal-rendering interference is impossible. If
  the call site ever moves later in startup, downgrade to debug! or
  pre-buffer (illblackdragon review).
- Cross-reference comments in src/history/store.rs and
  src/setup/wizard.rs pointing each other out so future changes to
  the migration fix-up call site stay in sync (illblackdragon review).
- Sort migrations/checksums.lock by parsed migration version (V1, V2,
  ..., V10, V11, ...) instead of lex order (V10 before V2). The
  resulting file reads in numeric order which makes review diffs
  easier to scan (illblackdragon review).

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

* fix(db): consolidate migration entry points + advisory lock + no leaks (#2101)

Address three Medium-severity findings from @serrrfirat's review:

1. **Duplicate call sites** — extract
   `run_postgres_migrations_with_fixup(client)` in
   `crate::db::migration_fixup` that bundles fix-up + refinery into a
   single function. Both `Store::run_migrations` and
   `SetupWizard::run_migrations_postgres` now call it. Eliminates the
   class of bug where a future entry point could forget the fix-up.
   The previous comment-based coupling was an interim measure.

2. **Concurrent startup race** — the new helper acquires
   `pg_advisory_lock(1328)` (issue number, easy to grep in `pg_locks`)
   before realignment and releases it after refinery returns, on every
   exit path including errors. Serializes concurrent migration runs
   across replicas — also hardens the pre-existing refinery race that
   has always existed for multi-replica starts. Uses session-level
   advisory lock (not `pg_advisory_xact_lock`) because refinery's
   `run_async` opens its own internal transactions.

3. **`Box::leak` in tests** — refactor `KnownDivergence` to be
   lifetime-generic (`KnownDivergence<'a>`). Production
   `KNOWN_DIVERGENCES` is `&[KnownDivergence<'static>]` — no external
   API change. Both integration tests now use stack-allocated
   `&[u64]` slices, no `Box::leak`. Removes the leak-sanitizer false
   positive.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…6101

chore: promote staging to staging-promote/6895cdad-24185214226 (2026-04-09 13:31 UTC)
…4226

chore: promote staging to staging-promote/63a48e4e-24182836482 (2026-04-09 10:24 UTC)
…6482

chore: promote staging to staging-promote/288fe49a-24110798843 (2026-04-09 09:26 UTC)
@henrypark133
henrypark133 merged commit d3f5210 into staging-promote/79c1b0fd-24108317021 Apr 10, 2026
12 of 13 checks passed
@henrypark133
henrypark133 deleted the staging-promote/288fe49a-24110798843 branch April 10, 2026 22:46
@github-actions github-actions Bot added scope: db Database trait / abstraction scope: db/postgres PostgreSQL backend scope: setup Onboarding / setup labels Apr 10, 2026
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
…4110798843

chore: promote staging to staging-promote/38f5ed10-24108317021 (2026-04-08 00:16 UTC)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: high Safety, secrets, auth, or critical infrastructure scope: agent Agent core (agent loop, router, scheduler) scope: channel/wasm WASM channel runtime scope: channel/web Web gateway channel scope: channel Channel infrastructure scope: ci CI/CD workflows scope: db/postgres PostgreSQL backend scope: db Database trait / abstraction scope: dependencies Dependency updates scope: docs Documentation scope: extensions Extension management scope: setup Onboarding / setup scope: tool/builder Dynamic tool builder scope: tool/builtin Built-in tools scope: tool/wasm WASM tool sandbox scope: tool Tool infrastructure scope: workspace Persistent memory / workspace size: XL 500+ changed lines staging-promotion

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants