forge-openclaw-plugin 0.2.60 → 0.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +121 -51
  2. package/dist/assets/{board-B1V3M__K.js → board-DUwMfZvN.js} +1 -1
  3. package/dist/assets/index-B9VOpR7r.css +1 -0
  4. package/dist/assets/index-DoHjjze2.js +90 -0
  5. package/dist/assets/{motion-CltSTItx.js → motion-Crg3QyXD.js} +1 -1
  6. package/dist/assets/{table-B-VrSFx8.js → table-CTlDeYRs.js} +1 -1
  7. package/dist/assets/{ui-DUqM4jkt.js → ui-CJPaElbj.js} +1 -1
  8. package/dist/assets/{vendor-C0otBhgu.js → vendor-BdrT2htV.js} +217 -207
  9. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  11. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  12. package/dist/companion-iroh-src/Cargo.lock +4559 -0
  13. package/dist/companion-iroh-src/Cargo.toml +37 -0
  14. package/dist/companion-iroh-src/src/lib.rs +279 -0
  15. package/dist/companion-iroh-src/src/main.rs +478 -0
  16. package/dist/companion-iroh-src/src/protocol.rs +129 -0
  17. package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
  18. package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
  19. package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
  20. package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
  21. package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
  22. package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
  23. package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
  24. package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
  25. package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
  26. package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
  27. package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
  28. package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
  29. package/dist/index.html +7 -7
  30. package/dist/openclaw/parity.js +27 -0
  31. package/dist/openclaw/plugin-entry-shared.js +2 -2
  32. package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
  33. package/dist/openclaw/routes.d.ts +4 -0
  34. package/dist/openclaw/routes.js +112 -3
  35. package/dist/openclaw/tools.js +32 -4
  36. package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
  37. package/dist/server/server/src/app.js +288 -61
  38. package/dist/server/server/src/data-management-types.js +2 -0
  39. package/dist/server/server/src/discovery-advertiser.js +13 -0
  40. package/dist/server/server/src/health.js +58 -3
  41. package/dist/server/server/src/movement.js +16 -1
  42. package/dist/server/server/src/openapi.js +410 -9
  43. package/dist/server/server/src/repositories/rewards.js +60 -0
  44. package/dist/server/server/src/services/companion-iroh.js +425 -0
  45. package/dist/server/server/src/services/data-management.js +32 -2
  46. package/dist/server/server/src/services/doctor.js +762 -0
  47. package/dist/server/server/src/services/gamification.js +75 -3
  48. package/dist/server/server/src/services/life-force.js +166 -25
  49. package/dist/server/server/src/web.js +88 -12
  50. package/dist/server/src/lib/api.js +9 -0
  51. package/dist/server/src/lib/gamification-catalog.js +1 -1
  52. package/openclaw.plugin.json +85 -3
  53. package/package.json +10 -6
  54. package/server/migrations/059_data_backup_retention.sql +2 -0
  55. package/skills/forge-openclaw/SKILL.md +80 -19
  56. package/skills/forge-openclaw/entity_conversation_playbooks.md +283 -25
  57. package/skills/forge-openclaw/psyche_entity_playbooks.md +82 -0
  58. package/dist/assets/index-BwKAPo98.css +0 -1
  59. package/dist/assets/index-Dy7c-dRY.js +0 -90
@@ -0,0 +1,37 @@
1
+ [package]
2
+ name = "forge-companion-iroh"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ license = "Apache-2.0"
6
+ publish = false
7
+
8
+ [lib]
9
+ name = "forge_companion_iroh"
10
+ path = "src/lib.rs"
11
+ crate-type = ["rlib", "staticlib"]
12
+
13
+ [[bin]]
14
+ name = "forge-companion-iroh"
15
+ path = "src/main.rs"
16
+
17
+ [dependencies]
18
+ anyhow = "1.0"
19
+ base64 = "0.22"
20
+ clap = { version = "4.5", features = ["derive"] }
21
+ hex = "0.4"
22
+ hostname = "0.4"
23
+ iroh = "=0.98.1"
24
+ ed25519 = "=3.0.0-rc.4"
25
+ der = "=0.8.0-rc.10"
26
+ pkcs8 = "=0.11.0-rc.10"
27
+ signature = "=3.0.0-rc.10"
28
+ spki = "=0.8.0-rc.4"
29
+ reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
30
+ serde = { version = "1.0", features = ["derive"] }
31
+ serde_json = "1.0"
32
+ tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "fs", "io-util", "signal", "time"] }
33
+ tracing = "0.1"
34
+ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
35
+
36
+ [dev-dependencies]
37
+ tempfile = "3.23"
@@ -0,0 +1,279 @@
1
+ use std::ffi::{CStr, CString, c_char};
2
+ use std::time::Duration;
3
+
4
+ use iroh::{Endpoint, SecretKey};
5
+ use protocol::{
6
+ BridgeRequest, BridgeResponse, COMPANION_ALPN, FORGE_AGENT_NAME, ForgeHttpRequest,
7
+ ForgeHttpResponse, HeaderPair, PROTOCOL_VERSION, PairPayload,
8
+ };
9
+ use serde::{Deserialize, Serialize};
10
+ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11
+
12
+ pub mod protocol;
13
+
14
+ const MAX_FRAME_BYTES: usize = 50 * 1024 * 1024;
15
+
16
+ #[derive(Debug, Deserialize)]
17
+ #[serde(rename_all = "camelCase")]
18
+ struct FfiHttpRequest {
19
+ pair_payload: PairPayload,
20
+ method: String,
21
+ path: String,
22
+ #[serde(default)]
23
+ headers: Vec<HeaderPair>,
24
+ #[serde(default)]
25
+ body_base64: Option<String>,
26
+ }
27
+
28
+ #[derive(Debug, Serialize)]
29
+ #[serde(rename_all = "camelCase")]
30
+ struct FfiHttpResponse {
31
+ ok: bool,
32
+ #[serde(skip_serializing_if = "Option::is_none")]
33
+ status: Option<u16>,
34
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
35
+ headers: Vec<HeaderPair>,
36
+ #[serde(skip_serializing_if = "Option::is_none")]
37
+ body_base64: Option<String>,
38
+ #[serde(skip_serializing_if = "Option::is_none")]
39
+ error: Option<String>,
40
+ }
41
+
42
+ #[unsafe(no_mangle)]
43
+ pub extern "C" fn forge_iroh_http_request_json(input_json: *const c_char) -> *mut c_char {
44
+ let response = match read_c_string(input_json)
45
+ .and_then(|input| serde_json::from_str::<FfiHttpRequest>(&input).map_err(|e| e.to_string()))
46
+ .and_then(run_ffi_http_request)
47
+ {
48
+ Ok(response) => response,
49
+ Err(error) => FfiHttpResponse {
50
+ ok: false,
51
+ status: None,
52
+ headers: vec![],
53
+ body_base64: None,
54
+ error: Some(error),
55
+ },
56
+ };
57
+ into_c_string(serde_json::to_string(&response).unwrap_or_else(|error| {
58
+ format!(r#"{{"ok":false,"error":"failed to encode Forge Iroh response: {error}"}}"#)
59
+ }))
60
+ }
61
+
62
+ #[unsafe(no_mangle)]
63
+ pub extern "C" fn forge_iroh_free_string(value: *mut c_char) {
64
+ if value.is_null() {
65
+ return;
66
+ }
67
+ unsafe {
68
+ let _ = CString::from_raw(value);
69
+ }
70
+ }
71
+
72
+ fn read_c_string(input_json: *const c_char) -> Result<String, String> {
73
+ if input_json.is_null() {
74
+ return Err("input JSON pointer was null".to_string());
75
+ }
76
+ unsafe { CStr::from_ptr(input_json) }
77
+ .to_str()
78
+ .map(|value| value.to_string())
79
+ .map_err(|error| format!("input JSON was not valid UTF-8: {error}"))
80
+ }
81
+
82
+ fn into_c_string(value: String) -> *mut c_char {
83
+ let sanitized = value.replace('\0', "\\u0000");
84
+ CString::new(sanitized)
85
+ .expect("sanitized Forge Iroh response should not contain NUL bytes")
86
+ .into_raw()
87
+ }
88
+
89
+ fn run_ffi_http_request(request: FfiHttpRequest) -> Result<FfiHttpResponse, String> {
90
+ let runtime = tokio::runtime::Builder::new_current_thread()
91
+ .enable_all()
92
+ .build()
93
+ .map_err(|error| format!("building Forge Iroh runtime: {error}"))?;
94
+ runtime.block_on(async move {
95
+ let response = send_http_request_over_iroh(request).await?;
96
+ Ok(FfiHttpResponse {
97
+ ok: true,
98
+ status: Some(response.status),
99
+ headers: response.headers,
100
+ body_base64: response.body_base64,
101
+ error: None,
102
+ })
103
+ })
104
+ }
105
+
106
+ async fn send_http_request_over_iroh(request: FfiHttpRequest) -> Result<ForgeHttpResponse, String> {
107
+ validate_pair_payload(&request.pair_payload)?;
108
+ validate_proxy_path(&request.path)?;
109
+
110
+ let endpoint = Endpoint::builder(iroh::endpoint::presets::N0)
111
+ .secret_key(SecretKey::generate())
112
+ .bind()
113
+ .await
114
+ .map_err(|error| format!("binding Iroh endpoint: {error}"))?;
115
+ let result = async {
116
+ let node_id = request
117
+ .pair_payload
118
+ .node_id
119
+ .parse()
120
+ .map_err(|error| format!("parsing Iroh node id: {error}"))?;
121
+ let mut addr = iroh::EndpointAddr::new(node_id);
122
+ if let Some(relay) = request.pair_payload.relay.as_deref() {
123
+ addr = addr.with_relay_url(
124
+ relay
125
+ .parse()
126
+ .map_err(|error| format!("parsing Iroh relay URL: {error}"))?,
127
+ );
128
+ }
129
+ let conn = endpoint
130
+ .connect(addr, COMPANION_ALPN)
131
+ .await
132
+ .map_err(|error| format!("connecting over Forge Iroh bridge: {error}"))?;
133
+ let (mut send, mut recv) = conn
134
+ .open_bi()
135
+ .await
136
+ .map_err(|error| format!("opening Iroh stream: {error}"))?;
137
+ write_json_frame(
138
+ &mut send,
139
+ &BridgeRequest::Connect {
140
+ v: PROTOCOL_VERSION,
141
+ token: request.pair_payload.token.clone(),
142
+ agent: FORGE_AGENT_NAME.to_string(),
143
+ },
144
+ )
145
+ .await?;
146
+ let ack: BridgeResponse = read_json_frame(&mut recv).await?;
147
+ validate_bridge_response(&ack)?;
148
+ write_json_frame(
149
+ &mut send,
150
+ &ForgeHttpRequest {
151
+ v: PROTOCOL_VERSION,
152
+ method: request.method,
153
+ path: request.path,
154
+ headers: request.headers,
155
+ body_base64: request.body_base64,
156
+ },
157
+ )
158
+ .await?;
159
+ let response = tokio::time::timeout(
160
+ Duration::from_secs(60),
161
+ read_json_frame::<ForgeHttpResponse, _>(&mut recv),
162
+ )
163
+ .await
164
+ .map_err(|_| "timed out waiting for Forge Iroh response".to_string())??;
165
+ conn.close(
166
+ iroh::endpoint::VarInt::from_u32(0),
167
+ b"forge request complete",
168
+ );
169
+ Ok(response)
170
+ }
171
+ .await;
172
+ endpoint.close().await;
173
+ result
174
+ }
175
+
176
+ fn validate_pair_payload(payload: &PairPayload) -> Result<(), String> {
177
+ if payload.v != PROTOCOL_VERSION {
178
+ return Err(format!(
179
+ "Iroh bridge protocol mismatch: payload={} client={}",
180
+ payload.v, PROTOCOL_VERSION
181
+ ));
182
+ }
183
+ if payload.node_id.trim().is_empty() {
184
+ return Err("Iroh node id was empty".to_string());
185
+ }
186
+ if payload.token.trim().is_empty() {
187
+ return Err("Iroh pairing token was empty".to_string());
188
+ }
189
+ Ok(())
190
+ }
191
+
192
+ fn validate_proxy_path(path: &str) -> Result<(), String> {
193
+ if path.starts_with("http://") || path.starts_with("https://") {
194
+ return Err("absolute proxy paths are not allowed".to_string());
195
+ }
196
+ if !path.starts_with('/') {
197
+ return Err("proxy path must start with /".to_string());
198
+ }
199
+ Ok(())
200
+ }
201
+
202
+ fn validate_bridge_response(response: &BridgeResponse) -> Result<(), String> {
203
+ if response.v != PROTOCOL_VERSION {
204
+ return Err(format!(
205
+ "Iroh bridge response protocol mismatch: host={} client={}",
206
+ response.v, PROTOCOL_VERSION
207
+ ));
208
+ }
209
+ if !response.ok {
210
+ return Err(response
211
+ .error
212
+ .clone()
213
+ .unwrap_or_else(|| "Forge Iroh host rejected the request".to_string()));
214
+ }
215
+ Ok(())
216
+ }
217
+
218
+ async fn read_json_frame<T, R>(reader: &mut R) -> Result<T, String>
219
+ where
220
+ T: serde::de::DeserializeOwned,
221
+ R: AsyncRead + Unpin,
222
+ {
223
+ let len = reader
224
+ .read_u32()
225
+ .await
226
+ .map_err(|error| format!("reading frame length: {error}"))? as usize;
227
+ if len > MAX_FRAME_BYTES {
228
+ return Err(format!("frame too large: {len} bytes"));
229
+ }
230
+ let mut buf = vec![0u8; len];
231
+ reader
232
+ .read_exact(&mut buf)
233
+ .await
234
+ .map_err(|error| format!("reading frame body: {error}"))?;
235
+ serde_json::from_slice(&buf).map_err(|error| format!("decoding JSON frame: {error}"))
236
+ }
237
+
238
+ async fn write_json_frame<T, W>(writer: &mut W, value: &T) -> Result<(), String>
239
+ where
240
+ T: serde::Serialize,
241
+ W: AsyncWrite + Unpin,
242
+ {
243
+ let buf = serde_json::to_vec(value).map_err(|error| format!("encoding JSON frame: {error}"))?;
244
+ if buf.len() > MAX_FRAME_BYTES {
245
+ return Err(format!("frame too large: {} bytes", buf.len()));
246
+ }
247
+ writer
248
+ .write_u32(buf.len() as u32)
249
+ .await
250
+ .map_err(|error| format!("writing frame length: {error}"))?;
251
+ writer
252
+ .write_all(&buf)
253
+ .await
254
+ .map_err(|error| format!("writing frame body: {error}"))?;
255
+ writer
256
+ .flush()
257
+ .await
258
+ .map_err(|error| format!("flushing frame: {error}"))?;
259
+ Ok(())
260
+ }
261
+
262
+ #[cfg(test)]
263
+ mod tests {
264
+ use super::*;
265
+
266
+ #[test]
267
+ fn ffi_rejects_null_input() {
268
+ let ptr = forge_iroh_http_request_json(std::ptr::null());
269
+ assert!(!ptr.is_null());
270
+ let response = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_string();
271
+ forge_iroh_free_string(ptr);
272
+ assert!(response.contains("input JSON pointer was null"));
273
+ }
274
+
275
+ #[test]
276
+ fn proxy_path_rejects_absolute_urls() {
277
+ assert!(validate_proxy_path("https://example.com/api/v1/health").is_err());
278
+ }
279
+ }