mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Merge branch 'nearai:staging' into staging
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Change Type
|
## Change Type
|
||||||
|
|
||||||
<!-- Check one -->
|
<!-- Check all that apply. Refactor-only PRs are for core team or maintainer-requested work. -->
|
||||||
|
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] New feature
|
- [ ] New feature
|
||||||
@@ -18,16 +18,19 @@
|
|||||||
|
|
||||||
## Linked Issue
|
## Linked Issue
|
||||||
|
|
||||||
<!-- Closes #N, or "None" -->
|
<!-- Closes #N, Fixes #N, Related #N, or "None". New feature PRs must link an approved issue. -->
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
<!-- How did you verify this works? -->
|
<!-- How did you verify this works? -->
|
||||||
|
|
||||||
- [ ] `cargo fmt`
|
- [ ] `cargo fmt --all -- --check`
|
||||||
- [ ] `cargo clippy --all --benches --tests --examples --all-features`
|
- [ ] `cargo clippy --all --benches --tests --examples --all-features -- -D warnings`
|
||||||
|
- [ ] `cargo build`
|
||||||
- [ ] Relevant tests pass: <!-- list specific tests -->
|
- [ ] Relevant tests pass: <!-- list specific tests -->
|
||||||
|
- [ ] `cargo test --features integration` if database-backed or integration behavior changed
|
||||||
- [ ] Manual testing: <!-- describe what you tested -->
|
- [ ] Manual testing: <!-- describe what you tested -->
|
||||||
|
- [ ] If a coding agent was used and supports it, `review-pr` or `pr-shepherd --fix` was run before requesting review
|
||||||
|
|
||||||
## Security Impact
|
## Security Impact
|
||||||
|
|
||||||
@@ -45,6 +48,10 @@
|
|||||||
|
|
||||||
<!-- How to revert if this causes problems? For Track C changes, this is mandatory. -->
|
<!-- How to revert if this causes problems? For Track C changes, this is mandatory. -->
|
||||||
|
|
||||||
|
## Review Follow-Through
|
||||||
|
|
||||||
|
<!-- Review conversations are author-owned. Summarize any known follow-up or areas where reviewer judgment is still needed. -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Review track**: <!-- A (docs/tests/chore) | B (feature/refactor) | C (security/runtime/DB/CI) -->
|
**Review track**: <!-- A (docs/tests/chore) | B (feature/maintainer-requested refactor) | C (security/runtime/DB/CI) -->
|
||||||
|
|||||||
+76
-1
@@ -10,6 +10,42 @@ cd optimclaw
|
|||||||
|
|
||||||
This installs the Rust toolchain, WASM targets, git hooks, and runs initial checks.
|
This installs the Rust toolchain, WASM targets, git hooks, and runs initial checks.
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
- Bug fixes, docs improvements, and focused cleanup tied to a concrete problem are welcome.
|
||||||
|
- Search existing issues and PRs before opening a new one to avoid duplicates.
|
||||||
|
- Keep changes scoped. One bug, one feature, or one documentation improvement per PR.
|
||||||
|
|
||||||
|
### Creating Issues
|
||||||
|
|
||||||
|
Open an issue when you are reporting a bug, proposing a feature, or documenting a gap in behavior.
|
||||||
|
|
||||||
|
For bug reports, include:
|
||||||
|
|
||||||
|
- What you expected to happen
|
||||||
|
- What actually happened
|
||||||
|
- Clear reproduction steps
|
||||||
|
- Relevant logs, screenshots, or error output
|
||||||
|
- Environment details when they matter (OS, database backend, feature flags, commit/branch)
|
||||||
|
|
||||||
|
For feature requests:
|
||||||
|
|
||||||
|
- Open an issue first before writing code
|
||||||
|
- Explain the problem being solved, not just the implementation idea
|
||||||
|
- Wait for maintainer feedback before investing in a large PR
|
||||||
|
|
||||||
|
We require an issue for new features so maintainers can prioritize the work and confirm it fits the roadmap before anyone spends time implementing it.
|
||||||
|
|
||||||
|
### Fixing Bugs
|
||||||
|
|
||||||
|
- Small, targeted bug-fix PRs are welcome
|
||||||
|
- If there is already an issue, link it in your PR
|
||||||
|
- If the bug is non-trivial, security-sensitive, or changes behavior across subsystems, open or confirm an issue first so the approach can be aligned before implementation
|
||||||
|
|
||||||
|
### Refactor-Only PRs
|
||||||
|
|
||||||
|
Refactor-only PRs are not accepted from contributors outside the core team. If a refactor is necessary to land a bug fix or approved feature, keep it minimal and clearly tied to that change.
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -19,6 +55,45 @@ cargo test # unit tests
|
|||||||
cargo test --features integration # + PostgreSQL tests
|
cargo test --features integration # + PostgreSQL tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
These commands are for day-to-day iteration while you are developing locally. The pre-submission checks below are intentionally stricter and use CI-style flags so you can catch formatting drift and clippy warnings before requesting review.
|
||||||
|
|
||||||
|
## Before You Open a PR
|
||||||
|
|
||||||
|
Run the local validation checks required before requesting a review. These are stricter than the commands for iterative development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
cargo clippy --all --benches --tests --examples --all-features -- -D warnings
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
Also run this when your change touches database-backed or integration behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --features integration
|
||||||
|
```
|
||||||
|
|
||||||
|
Before asking for review:
|
||||||
|
|
||||||
|
- Build and exercise the changed path locally, not just the narrowest unit test
|
||||||
|
- Keep the PR focused and avoid mixing unrelated concerns
|
||||||
|
- Fill out the PR template with a clear summary, validation notes, and impact assessment
|
||||||
|
- If your change affects tracked behavior, update `FEATURE_PARITY.md` in the same branch
|
||||||
|
- If onboarding or setup behavior changes, update the relevant setup docs in the same branch
|
||||||
|
- If you are using a coding agent and it supports them, run `review-pr` or `pr-shepherd --fix` before opening or updating the PR
|
||||||
|
- `codex review --base origin/main` is also encouraged before requesting review
|
||||||
|
|
||||||
|
## Review Follow-Through
|
||||||
|
|
||||||
|
Review conversations are author-owned.
|
||||||
|
|
||||||
|
- Address each review comment with a code change or a clear explanation
|
||||||
|
- Resolve conversations you have handled; leave them open only when reviewer judgment is still needed
|
||||||
|
- Do not leave review cleanup for maintainers when the follow-through belongs to the author
|
||||||
|
|
||||||
|
If a PR is stale for more than 48 hours after review feedback is posted, maintainers may take over the follow-up work and land the changes needed to accomplish the original PR or issue intent.
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- Zero clippy warnings policy
|
- Zero clippy warnings policy
|
||||||
@@ -46,7 +121,7 @@ All PRs follow a risk-based review process:
|
|||||||
| Track | Scope | Requirements |
|
| Track | Scope | Requirements |
|
||||||
|-------|-------|-------------|
|
|-------|-------|-------------|
|
||||||
| **A** | Docs, tests, chore, dependency bumps | 1 approval + CI green |
|
| **A** | Docs, tests, chore, dependency bumps | 1 approval + CI green |
|
||||||
| **B** | Features, refactors, new tools/channels | 1 approval + CI green + test evidence |
|
| **B** | Features, maintainer-requested refactors, new tools/channels | 1 approval + CI green + test evidence |
|
||||||
| **C** | Security (`src/safety/`, `src/secrets/`), runtime (`src/agent/`, `src/worker/`), database schema, CI workflows | 2 approvals + rollback plan documented |
|
| **C** | Security (`src/safety/`, `src/secrets/`), runtime (`src/agent/`, `src/worker/`), database schema, CI workflows | 2 approvals + rollback plan documented |
|
||||||
|
|
||||||
Select the appropriate track in the PR template based on what your changes touch.
|
Select the appropriate track in the PR template based on what your changes touch.
|
||||||
|
|||||||
Generated
+6
@@ -6845,7 +6845,11 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
|
"rustls 0.23.37",
|
||||||
|
"rustls-native-certs 0.8.3",
|
||||||
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls 0.26.4",
|
||||||
"tungstenite 0.26.2",
|
"tungstenite 0.26.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7206,6 +7210,8 @@ dependencies = [
|
|||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
|
"rustls 0.23.37",
|
||||||
|
"rustls-pki-types",
|
||||||
"sha1",
|
"sha1",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
|
|||||||
+1
-1
@@ -40,6 +40,7 @@ eula = false
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] }
|
||||||
eventsource-stream = "0.2"
|
eventsource-stream = "0.2"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
@@ -208,7 +209,6 @@ zbus = "4"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
tracing-test = "0.2"
|
tracing-test = "0.2"
|
||||||
tokio-tungstenite = "0.26"
|
|
||||||
testcontainers-modules = { version = "0.11", features = ["postgres"] }
|
testcontainers-modules = { version = "0.11", features = ["postgres"] }
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ This document tracks feature parity between OptimClaw (Rust implementation) and
|
|||||||
| WASM channels | ❌ | ✅ | - | OptimClaw innovation; host resolves owner scope vs sender identity |
|
| WASM channels | ❌ | ✅ | - | OptimClaw innovation; host resolves owner scope vs sender identity |
|
||||||
| WhatsApp | ✅ | ❌ | P1 | Baileys (Web), same-phone mode with echo detection |
|
| WhatsApp | ✅ | ❌ | P1 | Baileys (Web), same-phone mode with echo detection |
|
||||||
| Telegram | ✅ | ✅ | - | WASM channel(MTProto), DM pairing, caption, /start, bot_username, DM topics, setup-time owner auto-verification, owner-scoped persistence |
|
| Telegram | ✅ | ✅ | - | WASM channel(MTProto), DM pairing, caption, /start, bot_username, DM topics, setup-time owner auto-verification, owner-scoped persistence |
|
||||||
| Discord | ✅ | ❌ | P2 | discord.js, thread parent binding inheritance |
|
| Discord | ✅ | 🚧 | P2 | Gateway `MESSAGE_CREATE` intake restored via websocket queue + WASM poll; Gateway DMs now respect pairing; thread parent binding inheritance and reply/thread parity still incomplete |
|
||||||
| Signal | ✅ | ✅ | P2 | signal-cli daemonPC, SSE listener HTTP/JSON-R, user/group allowlists, DM pairing |
|
| Signal | ✅ | ✅ | P2 | signal-cli daemonPC, SSE listener HTTP/JSON-R, user/group allowlists, DM pairing |
|
||||||
| Slack | ✅ | ✅ | - | WASM tool |
|
| Slack | ✅ | ✅ | - | WASM tool |
|
||||||
| iMessage | ✅ | ❌ | P3 | BlueBubbles or Linq recommended |
|
| iMessage | ✅ | ❌ | P3 | BlueBubbles or Linq recommended |
|
||||||
|
|||||||
Generated
+11
-216
@@ -20,162 +20,33 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64ct"
|
|
||||||
version = "1.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.10.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-oid"
|
|
||||||
version = "0.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crypto-common"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "curve25519-dalek"
|
|
||||||
version = "4.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"curve25519-dalek-derive",
|
|
||||||
"digest",
|
|
||||||
"fiat-crypto",
|
|
||||||
"rustc_version",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "curve25519-dalek-derive"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "der"
|
|
||||||
version = "0.7.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
|
||||||
dependencies = [
|
|
||||||
"const-oid",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.10.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer",
|
|
||||||
"crypto-common",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "discord-channel"
|
name = "discord-channel"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ed25519-dalek",
|
|
||||||
"hex",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519"
|
|
||||||
version = "2.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
|
||||||
dependencies = [
|
|
||||||
"pkcs8",
|
|
||||||
"signature",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ed25519-dalek"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
|
||||||
dependencies = [
|
|
||||||
"curve25519-dalek",
|
|
||||||
"ed25519",
|
|
||||||
"serde",
|
|
||||||
"sha2",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fiat-crypto"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.14.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -197,12 +68,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "id-arena"
|
name = "id-arena"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -223,9 +88,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128"
|
name = "leb128"
|
||||||
@@ -233,12 +98,6 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.182"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -253,19 +112,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkcs8"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
|
||||||
dependencies = [
|
|
||||||
"der",
|
|
||||||
"spki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
@@ -288,22 +137,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc_version"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
|
||||||
dependencies = [
|
|
||||||
"semver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
@@ -353,23 +193,6 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2"
|
|
||||||
version = "0.10.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signature"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -385,22 +208,6 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spki"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"der",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "subtle"
|
|
||||||
version = "2.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -412,12 +219,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typenum"
|
|
||||||
version = "1.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -575,30 +376,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.39"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.39"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize"
|
|
||||||
version = "1.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "discord-channel"
|
name = "discord-channel"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Discord channel for OptimClaw"
|
description = "Discord channel for OptimClaw"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
@@ -10,8 +10,6 @@ publish = false
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wit-bindgen = "0.36"
|
wit-bindgen = "0.36"
|
||||||
ed25519-dalek = { version = "2", default-features = false, features = ["alloc", "fast", "zeroize"] }
|
|
||||||
hex = "0.4"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|||||||
@@ -86,6 +86,24 @@ If an internal error occurs (e.g., metadata serialization failure), the tool att
|
|||||||
Check the host logs for detailed error information.
|
Check the host logs for detailed error information.
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
### Gateway Mode
|
||||||
|
|
||||||
|
The Discord channel now defaults to Discord Gateway transport for inbound message intake.
|
||||||
|
The bundled identify payload requests intents `4609`, which expands to:
|
||||||
|
|
||||||
|
- `GUILDS` (`1`)
|
||||||
|
- `GUILD_MESSAGES` (`512`)
|
||||||
|
- `DIRECT_MESSAGES` (`4096`)
|
||||||
|
|
||||||
|
Gateway DMs now follow the same pairing policy as webhook DMs. Unpaired users receive a pairing
|
||||||
|
instruction reply in the DM channel before the message is allowed through to the agent. If you
|
||||||
|
want stricter access control than pairing, set `owner_id`; that lock still applies to both
|
||||||
|
webhook and Gateway traffic.
|
||||||
|
|
||||||
|
Gateway presence simply reflects a successful authenticated Gateway connection and advertises
|
||||||
|
`online`. Pairing still controls whether DMs are allowed through to the agent, but it no longer
|
||||||
|
changes the visible Discord status.
|
||||||
|
|
||||||
### Mention Polling
|
### Mention Polling
|
||||||
|
|
||||||
The Discord channel can also poll configured channels for `@bot` mentions.
|
The Discord channel can also poll configured channels for `@bot` mentions.
|
||||||
@@ -110,6 +128,7 @@ Example channel config:
|
|||||||
- `owner_id`: when set, only that Discord user can interact with the bot.
|
- `owner_id`: when set, only that Discord user can interact with the bot.
|
||||||
- `dm_policy`: `open` allows all DMs; `pairing` requires approval.
|
- `dm_policy`: `open` allows all DMs; `pairing` requires approval.
|
||||||
- `allow_from`: allowlist entries for DM pairing checks (`*`, user id, or username).
|
- `allow_from`: allowlist entries for DM pairing checks (`*`, user id, or username).
|
||||||
|
- Gateway DMs respect `dm_policy` and pairing just like webhook DMs.
|
||||||
|
|
||||||
### Embeds
|
### Embeds
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"wit_version": "0.3.0",
|
"wit_version": "0.3.0",
|
||||||
"type": "channel",
|
"type": "channel",
|
||||||
"name": "discord",
|
"name": "discord",
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"capabilities": {
|
"capabilities": {
|
||||||
"http": {
|
"http": {
|
||||||
"allowlist": [
|
"allowlist": [
|
||||||
{ "host": "discord.com", "path_prefix": "/api/v10" }
|
{ "host": "discord.com", "path_prefix": "/api/v10" },
|
||||||
|
{ "host": "gateway.discord.gg", "path_prefix": "/", "methods": ["GET"] }
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"discord_bot_token": {
|
"discord_bot_token": {
|
||||||
@@ -36,6 +37,20 @@
|
|||||||
"requests_per_hour": 3600
|
"requests_per_hour": 3600
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"websocket": {
|
||||||
|
"url": "wss://gateway.discord.gg/?v=10&encoding=json",
|
||||||
|
"connect_on_start": true,
|
||||||
|
"identify_secret_name": "discord_bot_token",
|
||||||
|
"identify": {
|
||||||
|
"_intents_doc": "GUILDS(1) + GUILD_MESSAGES(512) + DIRECT_MESSAGES(4096)",
|
||||||
|
"intents": 4609,
|
||||||
|
"properties": {
|
||||||
|
"os": "linux",
|
||||||
|
"browser": "ironclaw",
|
||||||
|
"device": "ironclaw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"allowed_names": ["discord_bot_token", "discord_*"]
|
"allowed_names": ["discord_bot_token", "discord_*"]
|
||||||
},
|
},
|
||||||
|
|||||||
+1568
-1005
File diff suppressed because it is too large
Load Diff
@@ -539,6 +539,65 @@ impl ChannelWorkspaceStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append a text frame to a JSON queue stored at `path`.
|
||||||
|
///
|
||||||
|
/// The queue is stored as a JSON array of strings and bounded to the most
|
||||||
|
/// recent `max_items` entries so websocket runtimes cannot grow it without
|
||||||
|
/// limit.
|
||||||
|
pub fn append_json_text_queue(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
text: &str,
|
||||||
|
max_items: usize,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut data = self
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.map_err(|_| "workspace store lock poisoned".to_string())?;
|
||||||
|
|
||||||
|
let mut queue: Vec<String> = data
|
||||||
|
.get(path)
|
||||||
|
.and_then(|raw| serde_json::from_str(raw).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
queue.push(text.to_string());
|
||||||
|
|
||||||
|
if queue.len() > max_items {
|
||||||
|
let overflow = queue.len() - max_items;
|
||||||
|
queue.drain(0..overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&queue)
|
||||||
|
.map_err(|error| format!("failed to serialize websocket queue: {error}"))?;
|
||||||
|
|
||||||
|
data.insert(path.to_string(), serialized);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomically move a queued JSON array of text frames from `source_path` to `dest_path`.
|
||||||
|
pub fn move_json_text_queue(&self, source_path: &str, dest_path: &str) -> Result<bool, String> {
|
||||||
|
let mut data = self
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.map_err(|_| "workspace store lock poisoned".to_string())?;
|
||||||
|
|
||||||
|
let Some(raw_queue) = data.remove(source_path) else {
|
||||||
|
data.remove(dest_path);
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
let queue: Vec<String> = serde_json::from_str(&raw_queue)
|
||||||
|
.map_err(|error| format!("failed to deserialize websocket queue: {error}"))?;
|
||||||
|
|
||||||
|
if queue.is_empty() {
|
||||||
|
data.remove(dest_path);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.insert(dest_path.to_string(), raw_queue);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::tools::wasm::WorkspaceReader for ChannelWorkspaceStore {
|
impl crate::tools::wasm::WorkspaceReader for ChannelWorkspaceStore {
|
||||||
@@ -818,6 +877,51 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_channel_workspace_store_append_json_text_queue_is_bounded() {
|
||||||
|
use crate::channels::wasm::host::ChannelWorkspaceStore;
|
||||||
|
use crate::tools::wasm::WorkspaceReader;
|
||||||
|
|
||||||
|
let store = ChannelWorkspaceStore::new();
|
||||||
|
let path = "channels/discord/state/gateway_event_queue";
|
||||||
|
|
||||||
|
store.append_json_text_queue(path, "frame-1", 2).unwrap();
|
||||||
|
store.append_json_text_queue(path, "frame-2", 2).unwrap();
|
||||||
|
store.append_json_text_queue(path, "frame-3", 2).unwrap();
|
||||||
|
|
||||||
|
let queue: Vec<String> = serde_json::from_str(&store.read(path).unwrap()).unwrap();
|
||||||
|
assert_eq!(queue, vec!["frame-2".to_string(), "frame-3".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_channel_workspace_store_move_json_text_queue_is_atomic() {
|
||||||
|
use crate::channels::wasm::host::ChannelWorkspaceStore;
|
||||||
|
use crate::tools::wasm::WorkspaceReader;
|
||||||
|
|
||||||
|
let store = ChannelWorkspaceStore::new();
|
||||||
|
let live_path = "channels/discord/state/gateway_event_queue";
|
||||||
|
let drain_path = "channels/discord/state/gateway_event_queue_processing";
|
||||||
|
|
||||||
|
store
|
||||||
|
.append_json_text_queue(live_path, "frame-1", 4)
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.append_json_text_queue(live_path, "frame-2", 4)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(store.move_json_text_queue(live_path, drain_path).unwrap());
|
||||||
|
assert_eq!(store.read(live_path), None);
|
||||||
|
|
||||||
|
let drained: Vec<String> = serde_json::from_str(&store.read(drain_path).unwrap()).unwrap();
|
||||||
|
assert_eq!(drained, vec!["frame-1".to_string(), "frame-2".to_string()]);
|
||||||
|
|
||||||
|
store
|
||||||
|
.append_json_text_queue(live_path, "frame-3", 4)
|
||||||
|
.unwrap();
|
||||||
|
let live: Vec<String> = serde_json::from_str(&store.read(live_path).unwrap()).unwrap();
|
||||||
|
assert_eq!(live, vec!["frame-3".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
// === QA Plan P2 - 2.3: WASM channel lifecycle tests ===
|
// === QA Plan P2 - 2.3: WASM channel lifecycle tests ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+1106
-3
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,37 @@ use crate::channels::web::auth::AuthenticatedUser;
|
|||||||
use crate::channels::web::server::GatewayState;
|
use crate::channels::web::server::GatewayState;
|
||||||
use crate::channels::web::types::*;
|
use crate::channels::web::types::*;
|
||||||
|
|
||||||
|
pub(crate) fn derive_activation_status(
|
||||||
|
ext: &crate::extensions::InstalledExtension,
|
||||||
|
pairing_store: &crate::pairing::PairingStore,
|
||||||
|
has_owner_binding: bool,
|
||||||
|
) -> Option<ExtensionActivationStatus> {
|
||||||
|
if ext.kind == crate::extensions::ExtensionKind::WasmChannel {
|
||||||
|
let allowlist_exists = pairing_store
|
||||||
|
.has_allow_from_file(&ext.name)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_paired = pairing_store
|
||||||
|
.read_allow_from(&ext.name)
|
||||||
|
.map(|list| !list.is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
classify_wasm_channel_activation(
|
||||||
|
ext,
|
||||||
|
has_paired,
|
||||||
|
has_owner_binding || (ext.active && !allowlist_exists),
|
||||||
|
)
|
||||||
|
} else if ext.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
||||||
|
Some(if ext.active {
|
||||||
|
ExtensionActivationStatus::Active
|
||||||
|
} else if ext.authenticated {
|
||||||
|
ExtensionActivationStatus::Configured
|
||||||
|
} else {
|
||||||
|
ExtensionActivationStatus::Installed
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn extensions_list_handler(
|
pub async fn extensions_list_handler(
|
||||||
State(state): State<Arc<GatewayState>>,
|
State(state): State<Arc<GatewayState>>,
|
||||||
AuthenticatedUser(user): AuthenticatedUser,
|
AuthenticatedUser(user): AuthenticatedUser,
|
||||||
@@ -38,27 +69,11 @@ pub async fn extensions_list_handler(
|
|||||||
let extensions = installed
|
let extensions = installed
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ext| {
|
.map(|ext| {
|
||||||
let activation_status = if ext.kind == crate::extensions::ExtensionKind::WasmChannel {
|
let activation_status = derive_activation_status(
|
||||||
let has_paired = pairing_store
|
&ext,
|
||||||
.read_allow_from(&ext.name)
|
&pairing_store,
|
||||||
.map(|list| !list.is_empty())
|
owner_bound_channels.contains(&ext.name),
|
||||||
.unwrap_or(false);
|
);
|
||||||
crate::channels::web::types::classify_wasm_channel_activation(
|
|
||||||
&ext,
|
|
||||||
has_paired,
|
|
||||||
owner_bound_channels.contains(&ext.name),
|
|
||||||
)
|
|
||||||
} else if ext.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
|
||||||
Some(if ext.active {
|
|
||||||
crate::channels::web::types::ExtensionActivationStatus::Active
|
|
||||||
} else if ext.authenticated {
|
|
||||||
crate::channels::web::types::ExtensionActivationStatus::Configured
|
|
||||||
} else {
|
|
||||||
crate::channels::web::types::ExtensionActivationStatus::Installed
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
ExtensionInfo {
|
ExtensionInfo {
|
||||||
name: ext.name,
|
name: ext.name,
|
||||||
display_name: ext.display_name,
|
display_name: ext.display_name,
|
||||||
@@ -143,3 +158,63 @@ pub async fn extensions_remove_handler(
|
|||||||
Err(e) => Ok(Json(ActionResponse::fail(e.to_string()))),
|
Err(e) => Ok(Json(ActionResponse::fail(e.to_string()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::derive_activation_status;
|
||||||
|
use crate::channels::web::types::ExtensionActivationStatus;
|
||||||
|
use crate::extensions::{ExtensionKind, InstalledExtension};
|
||||||
|
use crate::pairing::PairingStore;
|
||||||
|
|
||||||
|
fn active_authenticated_wasm_channel(name: &str) -> InstalledExtension {
|
||||||
|
InstalledExtension {
|
||||||
|
name: name.to_string(),
|
||||||
|
kind: ExtensionKind::WasmChannel,
|
||||||
|
display_name: None,
|
||||||
|
description: None,
|
||||||
|
url: None,
|
||||||
|
authenticated: true,
|
||||||
|
active: true,
|
||||||
|
tools: Vec::new(),
|
||||||
|
needs_setup: false,
|
||||||
|
has_auth: false,
|
||||||
|
installed: true,
|
||||||
|
activation_error: None,
|
||||||
|
version: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_authenticated_wasm_channel_without_allowlist_file_is_active() {
|
||||||
|
let temp_dir = TempDir::new().expect("temp dir");
|
||||||
|
let pairing_store = PairingStore::with_base_dir(temp_dir.path().to_path_buf());
|
||||||
|
let ext = active_authenticated_wasm_channel("discord");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
derive_activation_status(&ext, &pairing_store, false),
|
||||||
|
Some(ExtensionActivationStatus::Active)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_authenticated_wasm_channel_with_empty_allowlist_file_is_pairing() {
|
||||||
|
let temp_dir = TempDir::new().expect("temp dir");
|
||||||
|
let pairing_store = PairingStore::with_base_dir(temp_dir.path().to_path_buf());
|
||||||
|
let ext = active_authenticated_wasm_channel("discord");
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
temp_dir.path().join("discord-allowFrom.json"),
|
||||||
|
r#"{"version":1,"allowFrom":[]}"#,
|
||||||
|
)
|
||||||
|
.expect("write empty allowlist");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
derive_activation_status(&ext, &pairing_store, false),
|
||||||
|
Some(ExtensionActivationStatus::Pairing)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2159,27 +2159,12 @@ async fn extensions_list_handler(
|
|||||||
let extensions = installed
|
let extensions = installed
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ext| {
|
.map(|ext| {
|
||||||
let activation_status = if ext.kind == crate::extensions::ExtensionKind::WasmChannel {
|
let activation_status =
|
||||||
let has_paired = pairing_store
|
crate::channels::web::handlers::extensions::derive_activation_status(
|
||||||
.read_allow_from(&ext.name)
|
|
||||||
.map(|list| !list.is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
crate::channels::web::types::classify_wasm_channel_activation(
|
|
||||||
&ext,
|
&ext,
|
||||||
has_paired,
|
&pairing_store,
|
||||||
owner_bound_channels.contains(&ext.name),
|
owner_bound_channels.contains(&ext.name),
|
||||||
)
|
);
|
||||||
} else if ext.kind == crate::extensions::ExtensionKind::ChannelRelay {
|
|
||||||
Some(if ext.active {
|
|
||||||
ExtensionActivationStatus::Active
|
|
||||||
} else if ext.authenticated {
|
|
||||||
ExtensionActivationStatus::Configured
|
|
||||||
} else {
|
|
||||||
ExtensionActivationStatus::Installed
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
ExtensionInfo {
|
ExtensionInfo {
|
||||||
name: ext.name,
|
name: ext.name,
|
||||||
display_name: ext.display_name,
|
display_name: ext.display_name,
|
||||||
|
|||||||
@@ -420,6 +420,12 @@ impl PairingStore {
|
|||||||
Ok(Some(entry))
|
Ok(Some(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the allowFrom list for a channel.
|
||||||
|
pub fn has_allow_from_file(&self, channel: &str) -> Result<bool, PairingStoreError> {
|
||||||
|
let path = allow_from_path(&self.base_dir, channel)?;
|
||||||
|
Ok(path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the allowFrom list for a channel.
|
/// Read the allowFrom list for a channel.
|
||||||
pub fn read_allow_from(&self, channel: &str) -> Result<Vec<String>, PairingStoreError> {
|
pub fn read_allow_from(&self, channel: &str) -> Result<Vec<String>, PairingStoreError> {
|
||||||
let path = allow_from_path(&self.base_dir, channel)?;
|
let path = allow_from_path(&self.base_dir, channel)?;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ pub struct Capabilities {
|
|||||||
pub secrets: Option<SecretsCapability>,
|
pub secrets: Option<SecretsCapability>,
|
||||||
/// Webhook authentication and signature verification.
|
/// Webhook authentication and signature verification.
|
||||||
pub webhook: Option<WebhookCapability>,
|
pub webhook: Option<WebhookCapability>,
|
||||||
|
/// Arbitrary websocket configuration preserved from capabilities JSON.
|
||||||
|
pub websocket: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Capabilities {
|
impl Capabilities {
|
||||||
@@ -341,6 +343,7 @@ mod tests {
|
|||||||
assert!(caps.tool_invoke.is_none());
|
assert!(caps.tool_invoke.is_none());
|
||||||
assert!(caps.secrets.is_none());
|
assert!(caps.secrets.is_none());
|
||||||
assert!(caps.webhook.is_none());
|
assert!(caps.webhook.is_none());
|
||||||
|
assert!(caps.websocket.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ pub struct CapabilitiesFile {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub webhook: Option<WebhookCapabilitySchema>,
|
pub webhook: Option<WebhookCapabilitySchema>,
|
||||||
|
|
||||||
|
/// Arbitrary websocket configuration preserved for runtime consumers.
|
||||||
|
#[serde(default)]
|
||||||
|
pub websocket: Option<serde_json::Value>,
|
||||||
|
|
||||||
/// Authentication setup instructions.
|
/// Authentication setup instructions.
|
||||||
/// Used by `optimclaw config` to guide users through auth setup.
|
/// Used by `optimclaw config` to guide users through auth setup.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -155,6 +159,7 @@ impl CapabilitiesFile {
|
|||||||
self.tool_invoke = self.tool_invoke.or(inner.tool_invoke);
|
self.tool_invoke = self.tool_invoke.or(inner.tool_invoke);
|
||||||
self.workspace = self.workspace.or(inner.workspace);
|
self.workspace = self.workspace.or(inner.workspace);
|
||||||
self.webhook = self.webhook.or(inner.webhook);
|
self.webhook = self.webhook.or(inner.webhook);
|
||||||
|
self.websocket = self.websocket.or(inner.websocket);
|
||||||
self.auth = self.auth.or(inner.auth);
|
self.auth = self.auth.or(inner.auth);
|
||||||
self.setup = self.setup.or(inner.setup);
|
self.setup = self.setup.or(inner.setup);
|
||||||
}
|
}
|
||||||
@@ -250,6 +255,8 @@ impl CapabilitiesFile {
|
|||||||
caps.webhook = Some(webhook.to_webhook_capability());
|
caps.webhook = Some(webhook.to_webhook_capability());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
caps.websocket = self.websocket.clone();
|
||||||
|
|
||||||
caps
|
caps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,6 +752,8 @@ fn default_tool_setup_field_input_type() -> ToolSetupFieldInputType {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::tools::wasm::capabilities_schema::{CapabilitiesFile, CredentialLocationSchema};
|
use crate::tools::wasm::capabilities_schema::{CapabilitiesFile, CredentialLocationSchema};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1402,6 +1411,48 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_discord_websocket_config_preserved_in_runtime_capabilities() {
|
||||||
|
let json = r#"{
|
||||||
|
"capabilities": {
|
||||||
|
"http": {
|
||||||
|
"allowlist": [{ "host": "discord.com", "path_prefix": "/api/v10" }]
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"url": "wss://gateway.discord.gg/?v=10&encoding=json",
|
||||||
|
"connect_on_start": true,
|
||||||
|
"identify": {
|
||||||
|
"intents": 513,
|
||||||
|
"properties": {
|
||||||
|
"os": "linux",
|
||||||
|
"browser": "ironclaw",
|
||||||
|
"device": "ironclaw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let file = CapabilitiesFile::from_json(json).unwrap();
|
||||||
|
let caps = file.to_capabilities();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
caps.websocket,
|
||||||
|
Some(json!({
|
||||||
|
"url": "wss://gateway.discord.gg/?v=10&encoding=json",
|
||||||
|
"connect_on_start": true,
|
||||||
|
"identify": {
|
||||||
|
"intents": 513,
|
||||||
|
"properties": {
|
||||||
|
"os": "linux",
|
||||||
|
"browser": "ironclaw",
|
||||||
|
"device": "ironclaw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tool description ────────────────────────────────────────────────
|
// ── Tool description ────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user