Compare commits

...
Author SHA1 Message Date
Henry ParkandGitHub ab67f02886 fix: publish ironclaw_safety 0.2.0 (#1659) 2026-03-25 18:21:17 -07:00
Henry ParkandGitHub f02345fd1f fix: allow publishing ironclaw_common (#1657) 2026-03-25 17:58:51 -07:00
ironclaw-ci[bot]GitHubironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
0b4e7c761b chore: release v0.22.0 (#1601)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
2026-03-25 16:44:53 -07:00
Henry ParkandGitHub cdc625566f Merge pull request #1451 from nearai/staging-promote/455f543b-23329172268
chore: promote staging to staging-promote/89203225-23327092672 (2026-03-20 04:32 UTC)
2026-03-25 15:58:40 -07:00
Henry ParkandGitHub bb24952622 Merge branch 'main' into staging-promote/455f543b-23329172268 2026-03-25 15:58:19 -07:00
Henry ParkandGitHub ef37d705a1 Merge pull request #1655 from nearai/codex/fix-staging-promotion-1451-version-bumps
fix: bump registry versions for staging promotion 1451
2026-03-25 15:56:49 -07:00
Henry ParkandGitHub b400c2a711 Merge pull request #1499 from nearai/staging-promote/9603fefd-23364438978
chore: promote staging to staging-promote/d3b69e7b-23359661011 (2026-03-20 22:04 UTC)
2026-03-25 15:17:48 -07:00
Henry ParkandGitHub ea24d79ace Merge pull request #1508 from nearai/staging-promote/6d847c60-23366109539
chore: promote staging to staging-promote/9603fefd-23364438978 (2026-03-20 23:06 UTC)
2026-03-25 15:17:38 -07:00
Henry ParkandGitHub 8d632872fd Merge pull request #1514 from nearai/staging-promote/e6277a39-23371263100
chore: promote staging to staging-promote/6d847c60-23366109539 (2026-03-21 03:42 UTC)
2026-03-25 15:17:29 -07:00
Henry ParkandGitHub 4c5d961102 Merge pull request #1515 from nearai/staging-promote/0d1a5c21-23372030005
chore: promote staging to staging-promote/e6277a39-23371263100 (2026-03-21 04:30 UTC)
2026-03-25 15:17:21 -07:00
Henry ParkandGitHub 2b4e881a72 Merge pull request #1517 from nearai/staging-promote/9964d5da-23372765633
chore: promote staging to staging-promote/0d1a5c21-23372030005 (2026-03-21 05:17 UTC)
2026-03-25 15:17:11 -07:00
Henry ParkandGitHub c0f33c37f7 Merge pull request #1522 from nearai/staging-promote/62326090-23374571867
chore: promote staging to staging-promote/9964d5da-23372765633 (2026-03-21 07:13 UTC)
2026-03-25 15:17:04 -07:00
Henry ParkandGitHub 5d714be354 Merge pull request #1548 from nearai/staging-promote/8ad7d78a-23387609319
chore: promote staging to staging-promote/62326090-23374571867 (2026-03-21 20:02 UTC)
2026-03-25 15:16:55 -07:00
Henry ParkandGitHub 3d43917cd0 Merge pull request #1551 from nearai/staging-promote/9d538136-23389762470
chore: promote staging to staging-promote/8ad7d78a-23387609319 (2026-03-21 22:03 UTC)
2026-03-25 15:16:46 -07:00
Henry ParkandGitHub f9dfb74800 Merge pull request #1552 from nearai/staging-promote/b97d82db-23390775365
chore: promote staging to staging-promote/9d538136-23389762470 (2026-03-21 23:04 UTC)
2026-03-25 15:16:39 -07:00
Henry ParkandGitHub cb01800f73 Merge pull request #1553 from nearai/staging-promote/89394ebd-23395764012
chore: promote staging to staging-promote/b97d82db-23390775365 (2026-03-22 04:36 UTC)
2026-03-25 15:16:30 -07:00
Henry ParkandGitHub 16aaea8d74 Merge pull request #1555 from nearai/staging-promote/b58b4215-23396456254
chore: promote staging to staging-promote/89394ebd-23395764012 (2026-03-22 05:25 UTC)
2026-03-25 15:16:22 -07:00
Henry ParkandGitHub a19deb6812 Merge pull request #1556 from nearai/staging-promote/86388958-23397163010
chore: promote staging to staging-promote/b58b4215-23396456254 (2026-03-22 06:14 UTC)
2026-03-25 15:16:11 -07:00
Henry ParkandGitHub 2f80b7b0b8 Merge pull request #1560 from nearai/staging-promote/1a62febe-23398066063
chore: promote staging to staging-promote/86388958-23397163010 (2026-03-22 07:15 UTC)
2026-03-25 15:16:00 -07:00
Henry ParkandGitHub 2f47c611d4 Merge pull request #1561 from nearai/staging-promote/fbce9a5f-23403885064
chore: promote staging to staging-promote/1a62febe-23398066063 (2026-03-22 13:21 UTC)
2026-03-25 15:15:52 -07:00
Henry ParkandGitHub 1f8d901cf6 Merge pull request #1576 from nearai/staging-promote/abba0831-23415935143
chore: promote staging to staging-promote/fbce9a5f-23403885064 (2026-03-23 01:32 UTC)
2026-03-25 15:15:43 -07:00
Henry ParkandGitHub ad20a5ab4f Merge pull request #1583 from nearai/staging-promote/d9358b0f-23426138451
chore: promote staging to staging-promote/abba0831-23415935143 (2026-03-23 07:37 UTC)
2026-03-25 15:15:33 -07:00
Henry ParkandGitHub e15c50ea2d Merge pull request #1593 from nearai/staging-promote/485d1568-23439773006
chore: promote staging to staging-promote/d9358b0f-23426138451 (2026-03-23 13:43 UTC)
2026-03-25 15:15:20 -07:00
Henry ParkandGitHub d4e18020e2 Merge pull request #1604 from nearai/staging-promote/dea789cc-23455694329
chore: promote staging to staging-promote/485d1568-23439773006 (2026-03-23 19:48 UTC)
2026-03-25 15:15:09 -07:00
Henry ParkandGitHub a23d87fc00 Merge pull request #1606 from nearai/staging-promote/fa51b9f5-23468747429
chore: promote staging to staging-promote/dea789cc-23455694329 (2026-03-24 01:54 UTC)
2026-03-25 15:14:59 -07:00
Henry ParkandGitHub c737fb0855 Merge pull request #1616 from nearai/staging-promote/fb354895-23477842664
chore: promote staging to staging-promote/fa51b9f5-23468747429 (2026-03-24 07:59 UTC)
2026-03-25 15:14:10 -07:00
Henry ParkandGitHub 0145672f36 Merge pull request #1620 from nearai/staging-promote/d3d517fd-23491969691
chore: promote staging to staging-promote/fb354895-23477842664 (2026-03-24 14:04 UTC)
2026-03-25 15:14:01 -07:00
Henry ParkandGitHub 9fd5537a01 Merge pull request #1624 from nearai/staging-promote/59014516-23505370929
chore: promote staging to staging-promote/d3d517fd-23491969691 (2026-03-24 18:16 UTC)
2026-03-25 15:13:51 -07:00
Henry ParkandGitHub 492d9d22c9 Merge pull request #1627 from nearai/staging-promote/82822d7b-23516534944
chore: promote staging to staging-promote/59014516-23505370929 (2026-03-24 23:13 UTC)
2026-03-25 15:13:44 -07:00
Henry ParkandGitHub b8b88ab84e Merge pull request #1642 from nearai/staging-promote/6daa2f15-23538193544
chore: promote staging to staging-promote/82822d7b-23516534944 (2026-03-25 12:01 UTC)
2026-03-25 15:13:36 -07:00
Henry ParkandGitHub c98ec3fb18 Merge pull request #1645 from nearai/staging-promote/0341fcc9-23558273569
chore: promote staging to staging-promote/6daa2f15-23538193544 (2026-03-25 18:47 UTC)
2026-03-25 15:13:23 -07:00
Henry ParkandGitHub 189fa35e64 Merge pull request #1647 from nearai/staging-promote/c949521d-23562109203
chore: promote staging to staging-promote/0341fcc9-23558273569 (2026-03-25 20:19 UTC)
2026-03-25 15:13:16 -07:00
Henry ParkandGitHub c5dce279e2 Merge pull request #1649 from nearai/staging-promote/ab0ad948-23563320113
chore: promote staging to staging-promote/c949521d-23562109203 (2026-03-25 20:47 UTC)
2026-03-25 15:13:08 -07:00
Henry ParkandGitHub 5a5ffe8d08 Merge pull request #1654 from nearai/staging-promote/86d11430-23565413131
chore: promote staging to staging-promote/ab0ad948-23563320113 (2026-03-25 21:37 UTC)
2026-03-25 15:12:34 -07:00
Henry ParkandGitHub 86d1143064 Fix libsql prompt scope regressions (#1651) 2026-03-25 14:36:53 -07:00
Henry ParkandGitHub ab0ad948f3 Normalize cron schedules on routine create (#1648)
* Fix REPL single-message hang and cap CI test duration

* Fix Clippy nested-if lint in REPL startup

* Fix single-message approval flow

* Handle empty single-message REPL exits

* Wait for one-shot event routines before exit

* Fix MCP lifecycle trace user scope

* Normalize cron schedules on routine create
2026-03-25 13:47:12 -07:00
Henry ParkandGitHub c949521d8d Fix MCP lifecycle trace user scope (#1646)
* Fix REPL single-message hang and cap CI test duration

* Fix Clippy nested-if lint in REPL startup

* Fix single-message approval flow

* Handle empty single-message REPL exits

* Wait for one-shot event routines before exit

* Fix MCP lifecycle trace user scope
2026-03-25 13:17:32 -07:00
Henry ParkandGitHub 0341fcc940 Fix REPL single-message hang and cap CI test duration (#1643)
* Fix REPL single-message hang and cap CI test duration

* Fix Clippy nested-if lint in REPL startup

* Fix single-message approval flow

* Handle empty single-message REPL exits

* Wait for one-shot event routines before exit
2026-03-25 11:45:29 -07:00
serrrfirat 67a025e2fa fix(deps): unblock promotion PR #1451 cargo-deny 2026-03-25 13:59:50 +03:00
Henry ParkandGitHub 424b470c59 Merge pull request #1483 from nearai/staging-promote/d3b69e7b-23359661011
chore: promote staging to staging-promote/ee6f5cd6-23354122351 (2026-03-20 19:41 UTC)
2026-03-24 11:34:10 -07:00
Henry ParkandGitHub ae370d7e2b Merge pull request #1467 from nearai/staging-promote/ee6f5cd6-23354122351
chore: promote staging to staging-promote/3da9810e-23351687636 (2026-03-20 17:14 UTC)
2026-03-23 20:27:41 -07:00
Henry ParkandGitHub 98418b3ef0 Merge pull request #1452 from nearai/staging-promote/806d4028-23330265305
chore: promote staging to staging-promote/455f543b-23329172268 (2026-03-20 05:23 UTC)
2026-03-23 12:01:16 -07:00
Henry ParkandGitHub 74b2b4129e Merge pull request #1456 from nearai/staging-promote/b952d229-23331469361
chore: promote staging to staging-promote/806d4028-23330265305 (2026-03-20 06:16 UTC)
2026-03-23 12:00:56 -07:00
Henry ParkandGitHub bb57e36e6d Merge pull request #1459 from nearai/staging-promote/c1762616-23332963145
chore: promote staging to staging-promote/b952d229-23331469361 (2026-03-20 07:18 UTC)
2026-03-23 12:00:45 -07:00
Henry ParkandGitHub 0194275792 Merge pull request #1462 from nearai/staging-promote/cba1bc37-23334371795
chore: promote staging to staging-promote/c1762616-23332963145 (2026-03-20 08:09 UTC)
2026-03-23 12:00:38 -07:00
Henry ParkandGitHub ddf64e8485 Merge pull request #1466 from nearai/staging-promote/3da9810e-23351687636
chore: promote staging to staging-promote/cba1bc37-23334371795 (2026-03-20 16:12 UTC)
2026-03-23 12:00:31 -07:00
Henry ParkandGitHub bd6977e6a8 Merge pull request #1447 from nearai/staging-promote/89203225-23327092672
chore: promote staging to staging-promote/c4ab3825-23321164063 (2026-03-20 02:56 UTC)
2026-03-23 11:59:41 -07:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
d47b4b0346 chore: update WASM artifact SHA256 checksums [skip ci] (#1481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-20 12:24:47 -07:00
ironclaw-ci[bot]GitHubironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
91a241a3c7 chore: release v0.21.0 (#1472)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
2026-03-20 11:23:39 -07:00
Henry ParkandGitHub d1d74d665a Merge pull request #1420 from nearai/staging-promote/71f9012d-23307625134
chore: promote staging to staging-promote/ec04354c-23271447493 (2026-03-19 17:20 UTC)
2026-03-20 10:51:43 -07:00
Henry Park e077e1277d fix: bump Feishu channel version for promotion 2026-03-20 10:33:57 -07:00
Henry ParkandGitHub 6fc8cc2f39 Merge pull request #1422 from nearai/staging-promote/71f41dd1-23309993684
chore: promote staging to staging-promote/71f9012d-23307625134 (2026-03-19 18:14 UTC)
2026-03-20 10:11:43 -07:00
Henry ParkandGitHub e031d8246b Merge pull request #1425 from nearai/staging-promote/52ca9d65-23312673755
chore: promote staging to staging-promote/71f41dd1-23309993684 (2026-03-19 19:18 UTC)
2026-03-20 10:11:32 -07:00
Henry ParkandGitHub 23263029f9 Merge pull request #1428 from nearai/staging-promote/65062f3c-23317058602
chore: promote staging to staging-promote/52ca9d65-23312673755 (2026-03-19 21:10 UTC)
2026-03-20 10:11:15 -07:00
Henry ParkandGitHub d5e08b95f9 Merge pull request #1439 from nearai/staging-promote/c4ab3825-23321164063
chore: promote staging to staging-promote/65062f3c-23317058602 (2026-03-19 23:06 UTC)
2026-03-20 10:10:45 -07:00
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e4d3200d80 chore: update WASM artifact SHA256 checksums [skip ci] (#1424)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-19 13:04:07 -07:00
ironclaw-ci[bot]GitHubironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
7dc3c6d067 chore: release v0.20.0 (#1310)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
2026-03-19 11:20:16 -07:00
Henry ParkandGitHub e1774e9ec0 Merge pull request #1387 from nearai/staging-promote/ec04354c-23271447493
chore: promote staging to main (2026-03-18 23:07 UTC)
2026-03-19 10:35:49 -07:00
Henry ParkandGitHub e1d9827b21 Merge pull request #1411 from nearai/staging-promote/38dafb96-23306226661
chore: promote staging to staging-promote/ec04354c-23271447493 (2026-03-19 16:48 UTC)
2026-03-19 09:54:37 -07:00
Henry ParkandGitHub e582166781 Merge pull request #1396 from nearai/staging-promote/3dcccc1e-23280048384
chore: promote staging to staging-promote/ec04354c-23271447493 (2026-03-19 04:37 UTC)
2026-03-19 08:58:29 -07:00
Henry ParkandGitHub 656d1f3e86 Merge pull request #1402 from nearai/staging-promote/b9e5acf6-23283208580
chore: promote staging to staging-promote/3dcccc1e-23280048384 (2026-03-19 06:44 UTC)
2026-03-19 08:58:09 -07:00
Henry ParkandGitHub 0e3aa4f806 Merge pull request #1409 from nearai/staging-promote/07c6ca72-23302016242
chore: promote staging to staging-promote/b9e5acf6-23283208580 (2026-03-19 15:15 UTC)
2026-03-19 08:57:54 -07:00
Henry ParkandGitHub 44d16732a7 Merge pull request #1390 from nearai/staging-promote/94e4d9d3-23273403042
chore: promote staging to staging-promote/ec04354c-23271447493 (2026-03-19 00:12 UTC)
2026-03-18 17:30:59 -07:00
32 changed files with 443 additions and 75 deletions
+19 -5
View File
@@ -12,6 +12,7 @@ jobs:
tests:
name: Tests (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
@@ -40,11 +41,14 @@ jobs:
- name: Build WASM channels (for integration tests)
run: ./scripts/build-wasm-extensions.sh --channels
- name: Run Tests
run: cargo test ${{ matrix.flags }} -- --nocapture
run: |
timeout --signal=INT --kill-after=30s 40m \
cargo test ${{ matrix.flags }} -- --nocapture
heavy-integration-tests:
name: Heavy Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -58,9 +62,13 @@ jobs:
- name: Build Telegram WASM channel
run: cargo build --manifest-path channels-src/telegram/Cargo.toml --target wasm32-wasip2 --release
- name: Run thread scheduling integration tests
run: cargo test --no-default-features --features libsql,integration --test e2e_thread_scheduling -- --nocapture
run: |
timeout --signal=INT --kill-after=30s 15m \
cargo test --no-default-features --features libsql,integration --test e2e_thread_scheduling -- --nocapture
- name: Run Telegram thread-scope regression test
run: cargo test --features integration --test telegram_auth_integration test_private_messages_use_chat_id_as_thread_scope -- --exact
run: |
timeout --signal=INT --kill-after=30s 10m \
cargo test --features integration --test telegram_auth_integration test_private_messages_use_chat_id_as_thread_scope -- --exact
telegram-tests:
name: Telegram Channel Tests
@@ -68,6 +76,7 @@ jobs:
github.event_name != 'pull_request' ||
github.base_ref != 'staging'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -75,7 +84,9 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run Telegram Channel Tests
run: cargo test --manifest-path channels-src/telegram/Cargo.toml -- --nocapture
run: |
timeout --signal=INT --kill-after=30s 10m \
cargo test --manifest-path channels-src/telegram/Cargo.toml -- --nocapture
windows-build:
name: Windows Build (${{ matrix.name }})
@@ -110,6 +121,7 @@ jobs:
github.event_name != 'pull_request' ||
github.base_ref != 'staging'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -125,7 +137,9 @@ jobs:
- name: Build all WASM extensions against current WIT
run: ./scripts/build-wasm-extensions.sh
- name: Instantiation test (host linker compatibility)
run: cargo test --all-features wit_compat -- --nocapture
run: |
timeout --signal=INT --kill-after=30s 20m \
cargo test --all-features wit_compat -- --nocapture
bench-compile:
name: Benchmark Compilation
+132
View File
@@ -7,6 +7,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.22.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.21.0...ironclaw-v0.22.0) - 2026-03-25
### Added
- *(agent)* thread per-tool reasoning through provider, session, and all surfaces ([#1513](https://github.com/nearai/ironclaw/pull/1513))
- *(cli)* show credential auth status in tool info ([#1572](https://github.com/nearai/ironclaw/pull/1572))
- multi-tenant auth with per-user workspace isolation ([#1118](https://github.com/nearai/ironclaw/pull/1118))
- *(cli)* add ironclaw models subcommands (list/status/set/set-provider) ([#1043](https://github.com/nearai/ironclaw/pull/1043))
- *(workspace)* multi-scope workspace reads ([#1117](https://github.com/nearai/ironclaw/pull/1117))
- *(ux)* complete UX overhaul — design system, onboarding, web polish ([#1277](https://github.com/nearai/ironclaw/pull/1277))
- *(gemini_oauth)* full Gemini CLI OAuth integration with Cloud Code API ([#1356](https://github.com/nearai/ironclaw/pull/1356))
- *(shell)* add Low/Medium/High risk levels for graduated command approval (closes #172) ([#368](https://github.com/nearai/ironclaw/pull/368))
- *(agent)* queue and merge messages during active turns ([#1412](https://github.com/nearai/ironclaw/pull/1412))
- *(cli)* add `ironclaw hooks list` subcommand ([#1023](https://github.com/nearai/ironclaw/pull/1023))
- *(extensions)* support text setup fields in web configure modal ([#496](https://github.com/nearai/ironclaw/pull/496))
- *(llm)* add GitHub Copilot as LLM provider ([#1512](https://github.com/nearai/ironclaw/pull/1512))
- *(workspace)* layered memory with sensitivity-based privacy redirect ([#1112](https://github.com/nearai/ironclaw/pull/1112))
- *(webhooks)* add public webhook trigger endpoint for routines ([#736](https://github.com/nearai/ironclaw/pull/736))
- *(llm)* Add OpenAI Codex (ChatGPT subscription) as LLM provider ([#1461](https://github.com/nearai/ironclaw/pull/1461))
- *(web)* add light theme with dark/light/system toggle ([#1457](https://github.com/nearai/ironclaw/pull/1457))
- *(agent)* activate stuck_threshold for time-based stuck job detection ([#1234](https://github.com/nearai/ironclaw/pull/1234))
- chat onboarding and routine advisor ([#927](https://github.com/nearai/ironclaw/pull/927))
### Fixed
- ensure LLM calls always end with user message (closes #763) ([#1259](https://github.com/nearai/ironclaw/pull/1259))
- restore owner-scoped gateway startup ([#1625](https://github.com/nearai/ironclaw/pull/1625))
- remove stale stream_token gate from channel-relay activation ([#1623](https://github.com/nearai/ironclaw/pull/1623))
- *(agent)* case-insensitive channel match and user_id filter for event triggers ([#1211](https://github.com/nearai/ironclaw/pull/1211))
- *(routines)* normalize status display across web and CLI ([#1469](https://github.com/nearai/ironclaw/pull/1469))
- *(tunnel)* managed tunnels target wrong port and die from SIGPIPE ([#1093](https://github.com/nearai/ironclaw/pull/1093))
- *(agent)* persist /model selection to .env, TOML, and DB ([#1581](https://github.com/nearai/ironclaw/pull/1581))
- post-merge review sweep — 8 fixes across security, perf, and correctness ([#1550](https://github.com/nearai/ironclaw/pull/1550))
- generate Mistral-compatible 9-char alphanumeric tool call IDs ([#1242](https://github.com/nearai/ironclaw/pull/1242))
- *(mcp)* handle empty 202 notification acknowledgements ([#1539](https://github.com/nearai/ironclaw/pull/1539))
- *(tests)* eliminate env mutex poison cascade ([#1558](https://github.com/nearai/ironclaw/pull/1558))
- *(safety)* escape tool output XML content and remove misleading sanitized attr ([#1067](https://github.com/nearai/ironclaw/pull/1067))
- *(oauth)* reject malformed ic2.* states in decode_hosted_oauth_state ([#1441](https://github.com/nearai/ironclaw/pull/1441)) ([#1454](https://github.com/nearai/ironclaw/pull/1454))
- parameter coercion and validation for oneOf/anyOf/allOf schemas ([#1397](https://github.com/nearai/ironclaw/pull/1397))
- persist startup-loaded MCP clients in ExtensionManager ([#1509](https://github.com/nearai/ironclaw/pull/1509))
- *(deps)* patch rustls-webpki vulnerability (RUSTSEC-2026-0049)
- *(routines)* add missing extension_manager field in trigger_manual EngineContext
- *(ci)* serialize env-mutating OAuth wildcard tests with ENV_MUTEX ([#1280](https://github.com/nearai/ironclaw/pull/1280)) ([#1468](https://github.com/nearai/ironclaw/pull/1468))
- *(setup)* remove redundant LLM config and API keys from bootstrap .env ([#1448](https://github.com/nearai/ironclaw/pull/1448))
- resolve wasm broadcast merge conflicts with staging ([#395](https://github.com/nearai/ironclaw/pull/395)) ([#1460](https://github.com/nearai/ironclaw/pull/1460))
- skip credential validation for Bedrock backend ([#1011](https://github.com/nearai/ironclaw/pull/1011))
- register sandbox jobs in ContextManager for query tool visibility ([#1426](https://github.com/nearai/ironclaw/pull/1426))
- prefer execution-local message routing metadata ([#1449](https://github.com/nearai/ironclaw/pull/1449))
- *(security)* validate embedding base URLs to prevent SSRF ([#1221](https://github.com/nearai/ironclaw/pull/1221))
- f32→f64 precision artifact in temperature causes provider 400 errors ([#1450](https://github.com/nearai/ironclaw/pull/1450))
- *(routines)* surface errors when sandbox unavailable for full_job routines ([#769](https://github.com/nearai/ironclaw/pull/769))
- restore libSQL vector search with dynamic dimensions ([#1393](https://github.com/nearai/ironclaw/pull/1393))
- staging CI triage — consolidate retry parsing, fix flaky tests, add docs ([#1427](https://github.com/nearai/ironclaw/pull/1427))
### Other
- Merge branch 'main' into staging-promote/455f543b-23329172268
- Merge pull request #1655 from nearai/codex/fix-staging-promotion-1451-version-bumps
- Merge pull request #1499 from nearai/staging-promote/9603fefd-23364438978
- Fix libsql prompt scope regressions ([#1651](https://github.com/nearai/ironclaw/pull/1651))
- Normalize cron schedules on routine create ([#1648](https://github.com/nearai/ironclaw/pull/1648))
- Fix MCP lifecycle trace user scope ([#1646](https://github.com/nearai/ironclaw/pull/1646))
- Fix REPL single-message hang and cap CI test duration ([#1643](https://github.com/nearai/ironclaw/pull/1643))
- extract AppEvent to crates/ironclaw_common ([#1615](https://github.com/nearai/ironclaw/pull/1615))
- Fix hosted OAuth refresh via proxy ([#1602](https://github.com/nearai/ironclaw/pull/1602))
- *(agent)* optimize approval thread resolution (UUID parsing + lock contention) ([#1592](https://github.com/nearai/ironclaw/pull/1592))
- *(tools)* auto-compact WASM tool schemas, add descriptions, improve credential prompts ([#1525](https://github.com/nearai/ironclaw/pull/1525))
- Default new lightweight routines to tools-enabled ([#1573](https://github.com/nearai/ironclaw/pull/1573))
- Google OAuth URL broken when initiated from Telegram channel ([#1165](https://github.com/nearai/ironclaw/pull/1165))
- add gitcgr code graph badge ([#1563](https://github.com/nearai/ironclaw/pull/1563))
- Fix owner-scoped message routing fallbacks ([#1574](https://github.com/nearai/ironclaw/pull/1574))
- *(tools)* remove unconditional params clone in shared execution (fix #893) ([#926](https://github.com/nearai/ironclaw/pull/926))
- *(llm)* move transcription module into src/llm/ ([#1559](https://github.com/nearai/ironclaw/pull/1559))
- *(agent)* avoid preview allocations for non-truncated strings (fix #894) ([#924](https://github.com/nearai/ironclaw/pull/924))
- Expand AGENTS.md with coding agents guidance ([#1392](https://github.com/nearai/ironclaw/pull/1392))
- Fix CI approval flows and stale fixtures ([#1478](https://github.com/nearai/ironclaw/pull/1478))
- Use live owner tool scope for autonomous routines and jobs ([#1453](https://github.com/nearai/ironclaw/pull/1453))
- use Arc in embedding cache to avoid clones on miss path ([#1438](https://github.com/nearai/ironclaw/pull/1438))
- Add owner-scoped permissions for full-job routines ([#1440](https://github.com/nearai/ironclaw/pull/1440))
## [0.21.0](https://github.com/nearai/ironclaw/compare/v0.20.0...v0.21.0) - 2026-03-20
### Added
- structured fallback deliverables for failed/stuck jobs ([#236](https://github.com/nearai/ironclaw/pull/236))
- LRU embedding cache for workspace search ([#1423](https://github.com/nearai/ironclaw/pull/1423))
- receive relay events via webhook callbacks ([#1254](https://github.com/nearai/ironclaw/pull/1254))
### Fixed
- bump Feishu channel version for promotion
- *(approval)* make "always" auto-approve work for credentialed HTTP requests ([#1257](https://github.com/nearai/ironclaw/pull/1257))
- skip NEAR AI session check when backend is not nearai ([#1413](https://github.com/nearai/ironclaw/pull/1413))
### Other
- Make hosted OAuth and MCP auth generic ([#1375](https://github.com/nearai/ironclaw/pull/1375))
## [0.20.0](https://github.com/nearai/ironclaw/compare/v0.19.0...v0.20.0) - 2026-03-19
### Added
- *(self-repair)* wire stuck_threshold, store, and builder ([#712](https://github.com/nearai/ironclaw/pull/712))
- *(testing)* add FaultInjector framework for StubLlm ([#1233](https://github.com/nearai/ironclaw/pull/1233))
- *(gateway)* unified settings page with subtabs ([#1191](https://github.com/nearai/ironclaw/pull/1191))
- upgrade MiniMax default model to M2.7 ([#1357](https://github.com/nearai/ironclaw/pull/1357))
### Fixed
- navigate telegram E2E tests to channels subtab ([#1408](https://github.com/nearai/ironclaw/pull/1408))
- add missing `builder` field and update E2E extensions tab navigation ([#1400](https://github.com/nearai/ironclaw/pull/1400))
- remove debug_assert guards that panic on valid error paths ([#1385](https://github.com/nearai/ironclaw/pull/1385))
- address valid review comments from PR #1359 ([#1380](https://github.com/nearai/ironclaw/pull/1380))
- full_job routine runs stay running until linked job completion ([#1374](https://github.com/nearai/ironclaw/pull/1374))
- full_job routine concurrency tracks linked job lifetime ([#1372](https://github.com/nearai/ironclaw/pull/1372))
- remove -x from coverage pytest to prevent suite-blocking failures ([#1360](https://github.com/nearai/ironclaw/pull/1360))
- add debug_assert invariant guards to critical code paths ([#1312](https://github.com/nearai/ironclaw/pull/1312))
- *(mcp)* retry after missing session id errors ([#1355](https://github.com/nearai/ironclaw/pull/1355))
- *(telegram)* preserve polling after secret-blocked updates ([#1353](https://github.com/nearai/ironclaw/pull/1353))
- *(llm)* cap retry-after delays ([#1351](https://github.com/nearai/ironclaw/pull/1351))
- *(setup)* remove nonexistent webhook secret command hint ([#1349](https://github.com/nearai/ironclaw/pull/1349))
- Rate limiter returns retry after None instead of a duration ([#1269](https://github.com/nearai/ironclaw/pull/1269))
### Other
- bump telegram channel version to 0.2.5 ([#1410](https://github.com/nearai/ironclaw/pull/1410))
- *(ci)* enforce test requirement for state machine and resilience changes ([#1230](https://github.com/nearai/ironclaw/pull/1230)) ([#1304](https://github.com/nearai/ironclaw/pull/1304))
- Fix duplicate LLM responses for matched event routines ([#1275](https://github.com/nearai/ironclaw/pull/1275))
- add Japanese README ([#1306](https://github.com/nearai/ironclaw/pull/1306))
- *(ci)* add coverage gates via codecov.yml ([#1228](https://github.com/nearai/ironclaw/pull/1228)) ([#1291](https://github.com/nearai/ironclaw/pull/1291))
- Redesign routine create requests for LLMs ([#1147](https://github.com/nearai/ironclaw/pull/1147))
## [0.19.0](https://github.com/nearai/ironclaw/compare/v0.18.0...v0.19.0) - 2026-03-17
### Added
Generated
+5 -5
View File
@@ -2323,7 +2323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3390,7 +3390,7 @@ dependencies = [
[[package]]
name = "ironclaw"
version = "0.19.0"
version = "0.22.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -3496,7 +3496,7 @@ dependencies = [
[[package]]
name = "ironclaw_safety"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"aho-corasick",
"regex",
@@ -5481,7 +5481,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6388,7 +6388,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
+2 -2
View File
@@ -20,7 +20,7 @@ exclude = [
[package]
name = "ironclaw"
version = "0.19.0"
version = "0.22.0"
edition = "2024"
rust-version = "1.92"
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
@@ -104,7 +104,7 @@ cron = "0.13"
ironclaw_common = { path = "crates/ironclaw_common", version = "0.1.0" }
# Safety/sanitization
ironclaw_safety = { path = "crates/ironclaw_safety", version = "0.1.0" }
ironclaw_safety = { path = "crates/ironclaw_safety", version = "0.2.0" }
regex = "1"
aho-corasick = "1"
-1
View File
@@ -8,7 +8,6 @@ authors = ["NEAR AI <[email protected]>"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/nearai/ironclaw"
repository = "https://github.com/nearai/ironclaw"
publish = false
[package.metadata.dist]
dist = false
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "ironclaw_safety"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
rust-version = "1.92"
description = "Prompt injection defense, input validation, secret leak detection, and safety policy enforcement"
@@ -8,7 +8,6 @@ authors = ["NEAR AI <[email protected]>"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/nearai/ironclaw"
repository = "https://github.com/nearai/ironclaw"
publish = false
[package.metadata.dist]
dist = false
+3 -3
View File
@@ -2,7 +2,7 @@
"name": "feishu",
"display_name": "Feishu / Lark Channel",
"kind": "channel",
"version": "0.1.1",
"version": "0.1.3",
"wit_version": "0.3.0",
"description": "Talk to your agent through a Feishu or Lark bot",
"keywords": [
@@ -19,8 +19,8 @@
},
"artifacts": {
"wasm32-wasip2": {
"sha256": "5fca74022264d1c8e78a0853766276f7ffa3cf0d8065b2f51ca10985acad4714",
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-feishu-0.1.1-wasm32-wasip2.tar.gz"
"sha256": "52def36121a93cf0b06dd6d594dc9528745fae56ded7d632998700cc595be762",
"url": "https://github.com/nearai/ironclaw/releases/download/v0.21.0/channel-feishu-0.1.2-wasm32-wasip2.tar.gz"
}
},
"auth_summary": {
+2 -2
View File
@@ -18,8 +18,8 @@
},
"artifacts": {
"wasm32-wasip2": {
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-telegram-0.2.4-wasm32-wasip2.tar.gz",
"sha256": "a7cb300ec1c946831cfceaa95c1dc8f30d0f42a3924f3cb5de8098821573f4b8"
"url": "https://github.com/nearai/ironclaw/releases/download/v0.20.0/channel-telegram-0.2.5-wasm32-wasip2.tar.gz",
"sha256": "1ef20a538f55b379e049356e4d6758006251846bc3365ceaa1c87eba8379a329"
}
},
"auth_summary": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "github",
"display_name": "GitHub",
"kind": "tool",
"version": "0.2.1",
"version": "0.2.2",
"wit_version": "0.3.0",
"description": "GitHub integration for issues, PRs, repos, and code search",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gmail",
"display_name": "Gmail",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Read, send, and manage Gmail messages and threads",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "google-calendar",
"display_name": "Google Calendar",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Create, read, update, and delete Google Calendar events",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "google-docs",
"display_name": "Google Docs",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Create and edit Google Docs documents",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "google-drive",
"display_name": "Google Drive",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Upload, download, search, and manage Google Drive files and folders",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "google-sheets",
"display_name": "Google Sheets",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Read and write Google Sheets spreadsheet data",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "google-slides",
"display_name": "Google Slides",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Create and edit Google Slides presentations",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "llm-context",
"display_name": "LLM Context",
"kind": "tool",
"version": "0.1.0",
"version": "0.1.1",
"wit_version": "0.3.0",
"description": "Fetch pre-extracted web content from Brave Search for grounding LLM answers (RAG, fact-checking)",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "slack-tool",
"display_name": "Slack Tool",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Your agent uses Slack to post and read messages in your workspace",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "telegram-mtproto",
"display_name": "Telegram Tool",
"kind": "tool",
"version": "0.2.0",
"version": "0.2.1",
"wit_version": "0.3.0",
"description": "Your agent uses your Telegram account to read and send messages",
"keywords": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "web-search",
"display_name": "Web Search",
"kind": "tool",
"version": "0.2.1",
"version": "0.2.2",
"wit_version": "0.3.0",
"description": "Search the web using Brave Search API",
"keywords": [
-5
View File
@@ -1,7 +1,2 @@
[workspace]
git_release_enable = false
[[package]]
name = "ironclaw_safety"
publish = false
release = false
+64 -5
View File
@@ -16,6 +16,7 @@ use crate::agent::context_monitor::ContextMonitor;
use crate::agent::heartbeat::spawn_heartbeat;
use crate::agent::routine_engine::{RoutineEngine, spawn_cron_ticker};
use crate::agent::self_repair::{DefaultSelfRepair, RepairResult, SelfRepair};
use crate::agent::session::ThreadState;
use crate::agent::session_manager::SessionManager;
use crate::agent::submission::{Submission, SubmissionParser, SubmissionResult};
use crate::agent::{HeartbeatConfig as AgentHeartbeatConfig, Router, Scheduler, SchedulerDeps};
@@ -84,6 +85,15 @@ fn resolve_owner_scope_notification_user(
trimmed_option(explicit_user).or_else(|| trimmed_option(owner_fallback))
}
fn is_single_message_repl(message: &IncomingMessage) -> bool {
message.channel == "repl"
&& message
.metadata
.get("single_message_mode")
.and_then(|value| value.as_bool())
.unwrap_or(false)
}
async fn resolve_channel_notification_user(
extension_manager: Option<&Arc<ExtensionManager>>,
channel: Option<&str>,
@@ -1140,9 +1150,14 @@ impl Agent {
&& let Submission::UserInput { ref content } = submission
&& let Some(engine) = self.routine_engine().await
{
let single_message_repl = is_single_message_repl(message);
// Use post-hook content so that BeforeInbound hooks that rewrite
// input are respected by event trigger matching.
let fired = engine.check_event_triggers(message, content).await;
let fired = if single_message_repl {
engine.check_event_triggers_and_wait(message, content).await
} else {
engine.check_event_triggers(message, content).await
};
if fired > 0 {
tracing::debug!(
channel = %message.channel,
@@ -1150,10 +1165,16 @@ impl Agent {
fired,
"Consumed inbound user message with matching event-triggered routine(s)"
);
return Ok(Some(String::new()));
return if single_message_repl {
Ok(None)
} else {
Ok(Some(String::new()))
};
}
}
let session_for_empty_exit = Arc::clone(&session);
// Process based on submission type
let result = match submission {
Submission::UserInput { content } => {
@@ -1263,7 +1284,13 @@ impl Agent {
SubmissionResult::Error { message } => {
Ok(Some(format!("Error: {}", message)))
}
_ => Ok(Some(String::new())),
_ => {
if is_single_message_repl(message) {
Ok(None)
} else {
Ok(Some(String::new()))
}
}
};
}
// Authorization checks (including restart channel check) are enforced in handle_system_command
@@ -1325,7 +1352,26 @@ impl Agent {
Ok(Some(content))
}
}
SubmissionResult::Ok { message } => Ok(message),
SubmissionResult::Ok {
message: output_message,
} => {
let should_exit =
if output_message.as_deref() == Some("") && is_single_message_repl(message) {
let sess = session_for_empty_exit.lock().await;
sess.threads
.get(&thread_id)
.map(|thread| thread.state != ThreadState::AwaitingApproval)
.unwrap_or(true)
} else {
false
};
if should_exit {
Ok(None)
} else {
Ok(output_message)
}
}
SubmissionResult::Error { message } => Ok(Some(format!("Error: {}", message))),
SubmissionResult::Interrupted => Ok(Some("Interrupted.".into())),
SubmissionResult::NeedApproval { .. } => {
@@ -1341,7 +1387,7 @@ impl Agent {
#[cfg(test)]
mod tests {
use super::{
chat_tool_execution_metadata, resolve_routine_notification_user,
chat_tool_execution_metadata, is_single_message_repl, resolve_routine_notification_user,
should_fallback_routine_notification, truncate_for_preview,
};
use crate::channels::IncomingMessage;
@@ -1503,4 +1549,17 @@ mod tests {
assert!(should_fallback_routine_notification(&error)); // safety: test-only assertion
}
#[test]
fn single_message_repl_detection_requires_repl_channel_and_metadata_flag() {
let repl = IncomingMessage::new("repl", "owner-scope", "hello")
.with_metadata(serde_json::json!({ "single_message_mode": true }));
let gateway = IncomingMessage::new("gateway", "owner-scope", "hello")
.with_metadata(serde_json::json!({ "single_message_mode": true }));
let plain_repl = IncomingMessage::new("repl", "owner-scope", "hello");
assert!(is_single_message_repl(&repl)); // safety: test-only assertion
assert!(!is_single_message_repl(&gateway)); // safety: test-only assertion
assert!(!is_single_message_repl(&plain_repl)); // safety: test-only assertion
}
}
+6 -1
View File
@@ -63,7 +63,12 @@ impl Agent {
);
let system_prompt = if let Some(ws) = self.workspace() {
match ws
let scoped_workspace = if ws.user_id() == message.user_id {
Arc::clone(ws)
} else {
Arc::new(ws.scoped_to_user(&message.user_id))
};
match scoped_workspace
.system_prompt_for_context_tz(is_group_chat, user_tz)
.await
{
+60 -10
View File
@@ -18,6 +18,7 @@ use std::time::Duration;
use chrono::Utc;
use regex::Regex;
use tokio::sync::{RwLock, mpsc};
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::agent::Scheduler;
@@ -45,6 +46,11 @@ enum EventMatcher {
System { routine: Routine },
}
struct TriggeredRoutine {
routine: Routine,
detail: String,
}
/// Distinguishes why sandbox is unavailable so error messages are accurate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxReadiness {
@@ -202,6 +208,44 @@ impl RoutineEngine {
/// Check incoming message against event triggers. Returns number of routines fired.
pub async fn check_event_triggers(&self, message: &IncomingMessage, content: &str) -> usize {
let triggered = self.matching_event_triggers(message, content).await;
let fired = triggered.len();
for triggered in triggered {
std::mem::drop(self.spawn_fire(triggered.routine, "event", Some(triggered.detail)));
}
fired
}
/// Fire matching event-triggered routines and wait for them to complete.
///
/// Used by single-message REPL mode so the process does not exit before
/// background event-triggered routines finish.
pub async fn check_event_triggers_and_wait(
&self,
message: &IncomingMessage,
content: &str,
) -> usize {
let triggered = self.matching_event_triggers(message, content).await;
let fired = triggered.len();
let handles: Vec<JoinHandle<()>> = triggered
.into_iter()
.map(|triggered| self.spawn_fire(triggered.routine, "event", Some(triggered.detail)))
.collect();
for handle in handles {
if let Err(e) = handle.await {
tracing::warn!(error = %e, "Event-triggered routine task failed");
}
}
fired
}
async fn matching_event_triggers(
&self,
message: &IncomingMessage,
content: &str,
) -> Vec<TriggeredRoutine> {
let cache = self.event_cache.read().await;
// Early return if there are no message matchers at all.
@@ -209,10 +253,9 @@ impl RoutineEngine {
.iter()
.any(|m| matches!(m, EventMatcher::Message { .. }))
{
return 0;
return Vec::new();
}
let mut fired = 0;
let mut triggered = Vec::new();
// Collect routine IDs for batch query
let routine_ids: Vec<Uuid> = cache
@@ -224,13 +267,13 @@ impl RoutineEngine {
.collect();
if routine_ids.is_empty() {
return 0;
return Vec::new();
}
// Single batch query instead of N queries
let concurrent_counts = match self.batch_concurrent_counts(&routine_ids).await {
Some(counts) => counts,
None => return 0,
None => return Vec::new(),
};
for matcher in cache.iter() {
@@ -285,11 +328,13 @@ impl RoutineEngine {
}
let detail = truncate(content, 200);
self.spawn_fire(routine.clone(), "event", Some(detail));
fired += 1;
triggered.push(TriggeredRoutine {
routine: routine.clone(),
detail,
});
}
fired
triggered
}
/// Emit a structured event to system-event routines.
@@ -845,7 +890,12 @@ impl RoutineEngine {
}
/// Spawn a fire in a background task.
fn spawn_fire(&self, routine: Routine, trigger_type: &str, trigger_detail: Option<String>) {
fn spawn_fire(
&self,
routine: Routine,
trigger_type: &str,
trigger_detail: Option<String>,
) -> JoinHandle<()> {
let run = RoutineRun {
id: Uuid::new_v4(),
routine_id: routine.id,
@@ -882,7 +932,7 @@ impl RoutineEngine {
return;
}
execute_routine(engine, routine, run).await;
});
})
}
fn check_cooldown(&self, routine: &Routine) -> bool {
+51 -9
View File
@@ -431,6 +431,18 @@ impl ReplChannel {
let _ = execute!(stderr, terminal::Clear(terminal::ClearType::FromCursorDown));
}
}
async fn finish_single_message_turn(&self) {
if self.single_message.is_none() {
return;
}
let tx = self.msg_tx.lock().ok().and_then(|mut guard| guard.take());
if let Some(tx) = tx {
let msg = IncomingMessage::new("repl", &self.user_id, "/quit");
let _ = tx.send(msg).await;
}
}
}
impl Default for ReplChannel {
@@ -480,7 +492,9 @@ impl Channel for ReplChannel {
async fn start(&self) -> Result<MessageStream, ChannelError> {
let (tx, rx) = mpsc::channel(32);
// Store tx so send_status can inject approval responses directly
// Approval prompts inject responses back through this sender.
// In single-message mode we keep it until the turn finishes, then
// drop it after enqueuing /quit so the receiver stream can close.
if let Ok(mut guard) = self.msg_tx.lock() {
*guard = Some(tx.clone());
}
@@ -496,11 +510,10 @@ impl Channel for ReplChannel {
// Single message mode: send it and return
if let Some(msg) = single_message {
let incoming = IncomingMessage::new("repl", &user_id, &msg).with_timezone(&sys_tz);
let incoming = IncomingMessage::new("repl", &user_id, &msg)
.with_metadata(serde_json::json!({ "single_message_mode": true }))
.with_timezone(&sys_tz);
let _ = tx.blocking_send(incoming);
// Ensure the agent exits after handling exactly one turn in -m mode,
// even when other channels (gateway/http) are enabled.
let _ = tx.blocking_send(IncomingMessage::new("repl", &user_id, "/quit"));
return;
}
@@ -663,6 +676,7 @@ impl Channel for ReplChannel {
println!();
println!();
self.stdin_locked.store(false, Ordering::Relaxed);
self.finish_single_message_turn().await;
return Ok(());
}
@@ -681,6 +695,7 @@ impl Channel for ReplChannel {
println!();
// Unlock stdin so readline can resume
self.stdin_locked.store(false, Ordering::Relaxed);
self.finish_single_message_turn().await;
Ok(())
}
@@ -780,6 +795,7 @@ impl Channel for ReplChannel {
let msg_tx = Arc::clone(&self.msg_tx);
let user_id = self.user_id.clone();
let lock_flag = Arc::clone(&self.stdin_locked);
let single_message_mode = self.single_message.is_some();
tokio::task::spawn_blocking(move || {
let action = run_approval_selector(allow_always).unwrap_or("n");
// Unlock stdin so readline can resume after approval
@@ -788,7 +804,12 @@ impl Channel for ReplChannel {
return;
};
if let Some(tx) = guard.as_ref() {
let msg = IncomingMessage::new("repl", &user_id, action);
let msg = if single_message_mode {
IncomingMessage::new("repl", &user_id, action)
.with_metadata(serde_json::json!({ "single_message_mode": true }))
} else {
IncomingMessage::new("repl", &user_id, action)
};
let _ = tx.blocking_send(msg);
}
});
@@ -889,6 +910,7 @@ impl Channel for ReplChannel {
#[cfg(test)]
mod tests {
use futures::StreamExt;
use tokio::time::{Duration, timeout};
use super::*;
@@ -897,16 +919,36 @@ mod tests {
let repl = ReplChannel::with_message("hi".to_string());
let mut stream = repl.start().await.expect("repl start should succeed");
let first = stream.next().await.expect("first message missing");
let first = timeout(Duration::from_secs(1), stream.next())
.await
.expect("timed out waiting for first message")
.expect("first message missing");
assert_eq!(first.channel, "repl");
assert_eq!(first.content, "hi");
let second = stream.next().await.expect("quit message missing");
assert!(
timeout(Duration::from_millis(100), stream.next())
.await
.is_err(),
"single-message mode should wait for the turn to finish before quitting"
);
repl.respond(&first, OutgoingResponse::text("done"))
.await
.expect("respond should succeed");
let second = timeout(Duration::from_secs(1), stream.next())
.await
.expect("timed out waiting for quit message")
.expect("quit message missing");
assert_eq!(second.channel, "repl");
assert_eq!(second.content, "/quit");
assert!(
stream.next().await.is_none(),
timeout(Duration::from_secs(1), stream.next())
.await
.expect("timed out waiting for stream to close")
.is_none(),
"stream should end after /quit"
);
}
+15 -1
View File
@@ -915,7 +915,7 @@ fn parse_routine_create_request(
fn build_routine_trigger(trigger: &NormalizedTriggerRequest) -> Trigger {
match trigger {
NormalizedTriggerRequest::Cron { schedule, timezone } => Trigger::Cron {
schedule: schedule.clone(),
schedule: normalize_cron_expression(schedule),
timezone: timezone.clone(),
},
NormalizedTriggerRequest::Manual => Trigger::Manual,
@@ -1836,6 +1836,20 @@ mod tests {
assert_eq!(parsed.cooldown_secs, 30);
}
#[test]
fn build_routine_trigger_normalizes_cron_schedule() {
let trigger = build_routine_trigger(&NormalizedTriggerRequest::Cron {
schedule: "0 0 9 * * MON-FRI".to_string(),
timezone: Some("UTC".to_string()),
});
assert!(matches!(
trigger,
Trigger::Cron { schedule, timezone }
if schedule == "0 0 9 * * MON-FRI *" && timezone.as_deref() == Some("UTC")
));
}
#[test]
fn parses_grouped_message_event_with_tools() {
let params = serde_json::json!({
+55
View File
@@ -149,6 +149,7 @@ fn reject_if_injected(path: &str, content: &str) -> Result<(), WorkspaceError> {
///
/// Allows Workspace to work with either a PostgreSQL `Repository` (the original
/// path) or any `Database` trait implementation (e.g. libSQL backend).
#[derive(Clone)]
enum WorkspaceStorage {
/// PostgreSQL-backed repository (uses connection pool directly).
#[cfg(feature = "postgres")]
@@ -576,6 +577,60 @@ impl Workspace {
self
}
/// Clone the workspace configuration for a different primary user scope.
///
/// This preserves search config, embeddings, shared read scopes, memory
/// layers, and privacy classifier while switching the primary read/write
/// scope to `user_id`.
pub fn scoped_to_user(&self, user_id: impl Into<String>) -> Self {
let user_id = user_id.into();
let mut memory_layers = self.memory_layers.clone();
for layer in &mut memory_layers {
if layer.sensitivity == crate::workspace::layer::LayerSensitivity::Private
&& layer.scope == self.user_id
{
layer.scope = user_id.clone();
}
}
let mut read_user_ids = vec![user_id.clone()];
for scope in &self.read_user_ids {
if scope != &self.user_id && !read_user_ids.contains(scope) {
read_user_ids.push(scope.clone());
}
}
for scope in crate::workspace::layer::MemoryLayer::read_scopes(&memory_layers) {
if !read_user_ids.contains(&scope) {
read_user_ids.push(scope);
}
}
let preserve_flags = user_id == self.user_id;
Self {
user_id,
read_user_ids,
agent_id: self.agent_id,
storage: self.storage.clone(),
embeddings: self.embeddings.clone(),
bootstrap_pending: std::sync::atomic::AtomicBool::new(if preserve_flags {
self.bootstrap_pending
.load(std::sync::atomic::Ordering::Acquire)
} else {
false
}),
bootstrap_completed: std::sync::atomic::AtomicBool::new(if preserve_flags {
self.bootstrap_completed
.load(std::sync::atomic::Ordering::Acquire)
} else {
false
}),
search_defaults: self.search_defaults.clone(),
memory_layers,
privacy_classifier: self.privacy_classifier.clone(),
}
}
/// Get the user ID (primary scope for writes).
pub fn user_id(&self) -> &str {
&self.user_id
+1
View File
@@ -15,6 +15,7 @@ use crate::workspace::document::{MemoryChunk, MemoryDocument, WorkspaceEntry};
use crate::workspace::search::{RankedResult, SearchConfig, SearchResult, fuse_results};
/// Database repository for workspace operations.
#[derive(Clone)]
pub struct Repository {
pool: Pool,
}
@@ -253,6 +253,6 @@ async def test_telegram_hot_activation_transitions_installed_to_active(page):
assert await card.locator(SEL["ext_pairing_label"]).count() == 0
assert captured_setup_payloads == [
{"secrets": {"telegram_bot_token": "123456789:ABCdefGhI"}},
{"secrets": {}},
{"secrets": {"telegram_bot_token": "123456789:ABCdefGhI"}, "fields": {}},
{"secrets": {}, "fields": {}},
]
+3 -2
View File
@@ -587,6 +587,7 @@ mod advanced {
async fn mcp_extension_lifecycle() {
use crate::support::mock_mcp_server::{MockToolResponse, start_mock_mcp_server};
use ironclaw::extensions::{AuthHint, ExtensionKind, ExtensionSource, RegistryEntry};
const TEST_USER_ID: &str = "test-user";
// 1. Start mock MCP server with pre-configured tool responses.
let mock_server = start_mock_mcp_server(vec![
@@ -654,14 +655,14 @@ mod advanced {
ext_mgr
.secrets()
.create(
"default",
TEST_USER_ID,
ironclaw::secrets::CreateSecretParams::new(secret_name, "mock-access-token")
.with_provider("mcp:mock-notion".to_string()),
)
.await
.expect("failed to inject test token");
let activate_result = ext_mgr.activate("mock-notion", "default").await;
let activate_result = ext_mgr.activate("mock-notion", TEST_USER_ID).await;
assert!(
activate_result.is_ok(),
"activation failed: {:?}",
+1 -1
View File
@@ -439,7 +439,7 @@ mod tests {
match &routine.trigger {
Trigger::Cron { schedule, timezone } => {
assert_eq!(schedule, "0 0 9 * * MON-FRI");
assert_eq!(schedule, "0 0 9 * * MON-FRI *");
assert_eq!(timezone.as_deref(), Some("UTC"));
}
other => panic!("expected cron trigger, got {other:?}"),
+3 -1
View File
@@ -12,6 +12,7 @@ mod tests {
use crate::support::test_rig::TestRigBuilder;
use crate::support::trace_llm::LlmTrace;
use ironclaw::workspace::Workspace;
// -----------------------------------------------------------------------
// Test 1: write_chunk_search
@@ -268,6 +269,7 @@ mod tests {
#[tokio::test]
async fn identity_in_system_prompt() {
const TEST_USER_ID: &str = "test-user";
let trace = LlmTrace::from_file(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/llm_traces/workspace/identity_prompt.json"
@@ -280,7 +282,7 @@ mod tests {
.await;
// Seed an IDENTITY.md so the system prompt has real content to inject.
let ws = rig.workspace().expect("workspace must be available");
let ws = Workspace::new_with_db(TEST_USER_ID, rig.database().clone());
ws.write(
"IDENTITY.md",
"I am TestBot, a helpful testing assistant created for E2E verification.",
+7 -7
View File
@@ -1,10 +1,10 @@
//! Tests proving that multi-tenant system prompts are broken.
//! Regression tests for multi-tenant system prompts.
//!
//! Bug: In multi-tenant mode, the agent loop uses `self.workspace()` which
//! returns a single shared workspace (user_id="default"). Identity files
//! (IDENTITY.md, SOUL.md, USER.md) seeded under per-user IDs ("alice",
//! "bob") are invisible to this workspace, so the system prompt is
//! empty/wrong.
//! The agent must build the conversational system prompt from a workspace
//! scoped to the incoming message's user, not from the shared owner-scope
//! workspace created at startup. Otherwise per-user identity files
//! (IDENTITY.md, SOUL.md, USER.md) become invisible and different users can
//! see the same owner-scoped prompt.
//!
//! These tests:
//! 1. Seed identity files for two users (alice, bob) in the database
@@ -13,7 +13,7 @@
//! correct user's identity
//! 4. Verify user A's identity doesn't leak into user B's prompt
//!
//! All tests are expected to FAIL until the bug is fixed.
//! These tests ensure each user's identity is isolated correctly.
#[cfg(feature = "libsql")]
mod support;