@adapt-toolkit/mufl 0.10.4 → 0.10.6
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.
|
@@ -33,7 +33,16 @@ library address_document_types loads library key_utils
|
|
|
33
33
|
$key_list->t_publickey(,), // list of keys active with this container
|
|
34
34
|
$default_keys->t_function->>t_key_id, // exactly two keys, one for signing, one for encryption, that are the default keys for this container
|
|
35
35
|
$container_id->t_container_id, // id of the container
|
|
36
|
-
|
|
36
|
+
// AD-v2 E2E double-ratchet prekey bundle (NIL for v1). Typed `any+`, NOT the nested
|
|
37
|
+
// t_e2e_bundle metadef, so a meta _value_id / SAFE-cast of t_address_document does NOT
|
|
38
|
+
// recursively reduce t_e2e_bundle at EVERY AD op (pin_registrar, verify_identity_bundle,
|
|
39
|
+
// peer_ads, cluster AD handling — dozens of sites). That nested reduction, summed across a
|
|
40
|
+
// full daemon packet + the migration core, blew the per-unit meta-fuel ceiling
|
|
41
|
+
// (meta.mm:2604 via actor.mu pin_registrar). Every read already casts it — e2e.mm
|
|
42
|
+
// `(identity $e2e_bundle) SAFE(t_e2e_bundle)`, a2a_messaging `(...as any) $e2e_bundle safe
|
|
43
|
+
// t_e2e_bundle` — so validation is unchanged and the wire/_value_id bytes are identical
|
|
44
|
+
// (value-hash, type-agnostic). Same pattern as transaction e2e_signed_message $e2e_envelope.
|
|
45
|
+
$e2e_bundle->any+
|
|
37
46
|
/* $code_id->hash_code */
|
|
38
47
|
),
|
|
39
48
|
$authorizations -> crypto_signature(,)
|
|
@@ -227,12 +227,44 @@ loads libraries key_storage, address_document_types
|
|
|
227
227
|
m_account IS bin+ = NIL. // this packet's Olm account (lazy first-use)
|
|
228
228
|
m_sessions IS (global_id ->> bin) = (,). // per-peer session pickle, keyed by contact id
|
|
229
229
|
m_wire_pv = 8. // wire dialect stamp ($pv) for v1 (matches wire8)
|
|
230
|
+
// core 0.9.0 migration (spec §5.5): ONE staged rotation per peer — a FRESH session
|
|
231
|
+
// established for the bilateral migration commit, held aside so the LIVE session
|
|
232
|
+
// (m_sessions[cid]) keeps decrypting legacy e2e traffic until commit_rotation promotes
|
|
233
|
+
// it. Packet-state, NEVER exported (INV-4) — same secrecy class as m_sessions.
|
|
234
|
+
m_staged IS (global_id ->> bin) = (,).
|
|
235
|
+
// Decode-seam migration hook (spec §5.5): "is a migration FSM entry pending for this
|
|
236
|
+
// cid?" Core (a2a_messaging) registers the real predicate via set_mig_pending_hook at
|
|
237
|
+
// init; the DEFAULT is FALSE, so with no core wiring the decode seam behaves EXACTLY
|
|
238
|
+
// as 0.8.0 (an inbound pre-key on a live session self-heals by replacing — the correct
|
|
239
|
+
// behavior when there is no migration to protect). When TRUE, an inbound pre-key on a
|
|
240
|
+
// live session STAGES instead of replacing, so the live session survives until the
|
|
241
|
+
// bilateral commit promotes the staged one. This is the load-bearing distinction that
|
|
242
|
+
// closes the unilateral-live-session-reset path without breaking pre-key self-heal.
|
|
243
|
+
m_mig_pending IS (global_id -> bool) = fn (_: global_id) -> bool { return FALSE. }.
|
|
230
244
|
|
|
231
245
|
fn account (_) -> bin
|
|
232
246
|
{
|
|
233
247
|
if m_account == NIL { m_account -> create_account(). }
|
|
234
248
|
return m_account as bin.
|
|
235
249
|
}
|
|
250
|
+
|
|
251
|
+
// The idempotent pre-key re-delivery result (a live- or staged-session replay). No
|
|
252
|
+
// plaintext, no state change — the session lives; the wrapper drops it (or, for a
|
|
253
|
+
// staged-session match, routes it to the core replay-commit hook).
|
|
254
|
+
fn replayed (_) -> e2e_result
|
|
255
|
+
{
|
|
256
|
+
return ( $ok -> FALSE, $plaintext -> NIL, $session -> NIL, $account -> NIL,
|
|
257
|
+
$error -> ( $code -> "replayed_handshake",
|
|
258
|
+
$detail -> "pre-key re-delivers an already-established session" ) ).
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Core (a2a_messaging) registers the migration-pending predicate here at init so the decode
|
|
263
|
+
// seam can distinguish a MIGRATION rotation (stage the fresh session) from a self-heal pre-key
|
|
264
|
+
// (replace, 0.8.0). Idempotent; the default predicate is FALSE (pure 0.8.0 decode behavior).
|
|
265
|
+
fn set_mig_pending_hook (hook: (global_id -> bool)) -> nil
|
|
266
|
+
{
|
|
267
|
+
m_mig_pending -> hook.
|
|
236
268
|
}
|
|
237
269
|
|
|
238
270
|
// Our own published AD-v2 prekey bundle (from our lazily-created account) — embedded in
|
|
@@ -273,26 +305,138 @@ loads libraries key_storage, address_document_types
|
|
|
273
305
|
// against this to reject an unknown/downgraded dialect (S2). Pure read.
|
|
274
306
|
fn wire_pv (_) -> int { return m_wire_pv. }
|
|
275
307
|
|
|
308
|
+
// =====================================================================================
|
|
309
|
+
// STAGED ROTATION (core 0.9.0 migration, spec §5.5) — additive. The facade above cannot
|
|
310
|
+
// express a bilateral key rotation: encrypt_to reuses m_sessions[cid] unconditionally and
|
|
311
|
+
// an inbound pre-key unilaterally replaces the live session. These APIs add a STAGED slot
|
|
312
|
+
// so a live session is NEVER clobbered without an explicit commit. Exactly-once by
|
|
313
|
+
// construction: commit_rotation is the SOLE promotion point; a lost staged secret is
|
|
314
|
+
// abandoned + superseded by a fresh rotation, never re-derived (INV-4 / plan invariant 4).
|
|
315
|
+
// =====================================================================================
|
|
316
|
+
|
|
317
|
+
// MIGRATION SEND (initiator): create a FRESH outbound session to `peer` EVEN WHEN a live
|
|
318
|
+
// session already exists (deliberately NOT encrypt_to's lazy-establish-if-absent path) and
|
|
319
|
+
// hold it STAGED. m_sessions[cid] is untouched — it keeps decrypting legacy traffic until
|
|
320
|
+
// commit_rotation. Returns the fresh session's stable id (the epoch binds to THIS id). The
|
|
321
|
+
// account may rotate (prekey consumption); that is peer-independent bookkeeping, committed
|
|
322
|
+
// immediately (validated under empty-OTK v1, plan B gate 2).
|
|
323
|
+
fn stage_outbound_rotation (cid: global_id, peer: t_e2e_bundle) -> bin
|
|
324
|
+
{
|
|
325
|
+
est = create_outbound_session (account()) peer.
|
|
326
|
+
m_account -> est $account.
|
|
327
|
+
m_staged cid -> est $session.
|
|
328
|
+
return session_id (est $session).
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Encrypt on the STAGED session (PRE_KEY until the peer replies on it); the ratchet advance
|
|
332
|
+
// commits to the STAGED slot ONLY — the live session is never advanced here. Same S1
|
|
333
|
+
// sender/recipient emsig binding + envelope structure as encrypt_to, so the far-side decode
|
|
334
|
+
// hook verifies identically; the envelope carries the STAGED session_id.
|
|
335
|
+
fn encrypt_staged (cid: global_id, plaintext: bin) -> e2e_signed_message
|
|
336
|
+
{
|
|
337
|
+
e = encrypt ((m_staged cid) as bin) plaintext.
|
|
338
|
+
m_staged cid -> e $session. // commit the ratchet advance on the STAGED slot
|
|
339
|
+
env = ( $session_id -> (session_id (e $session)), $olm_type -> (e $olm_type),
|
|
340
|
+
$ciphertext -> (e $ciphertext), $pv -> m_wire_pv ).
|
|
341
|
+
sig_context = ( $from -> _get_container_id(), $to -> cid, $envelope -> env ).
|
|
342
|
+
return ( $e2e_envelope -> env,
|
|
343
|
+
$emsignature -> (key_storage::default_sign (_value_id sig_context)) ).
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// MIGRATION inbound (responder): establish an inbound session from a pre-key INTO the STAGED
|
|
347
|
+
// slot, leaving m_sessions[cid] untouched. Returns the typed e2e_result (its $plaintext is
|
|
348
|
+
// the inner commit tx for the caller to dispatch). On the establish path the account rotates;
|
|
349
|
+
// committed immediately (peer-independent). A typed failure ($ok=FALSE) stages nothing.
|
|
350
|
+
fn stage_inbound_rotation (from_cid: global_id, sender_ik: bin, ciphertext: bin) -> e2e_result
|
|
351
|
+
{
|
|
352
|
+
r = decrypt (account()) NIL sender_ik 0 ciphertext. // olm_type 0 = establish; NIL session = fresh
|
|
353
|
+
if r $ok
|
|
354
|
+
{
|
|
355
|
+
m_staged from_cid -> (r $session) as bin.
|
|
356
|
+
if (r $account) != NIL { m_account -> (r $account) as bin. }
|
|
357
|
+
}
|
|
358
|
+
return r.
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Promote the STAGED session to ACTIVE atomically: it replaces the live session and the old
|
|
362
|
+
// one is DROPPED (the owner's fresh-key requirement). Returns the new session's id, or NIL
|
|
363
|
+
// if nothing is staged (idempotent — a duplicate/lost-confirm commit is a safe no-op). The
|
|
364
|
+
// account is already committed. This is the SOLE session-promotion point (plan invariant 4).
|
|
365
|
+
fn commit_rotation (cid: global_id) -> bin+
|
|
366
|
+
{
|
|
367
|
+
st IS bin+ = m_staged cid.
|
|
368
|
+
if st == NIL { return NIL. }
|
|
369
|
+
m_sessions cid -> st as bin.
|
|
370
|
+
delete m_staged cid.
|
|
371
|
+
return session_id (st as bin).
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Drop a staged rotation WITHOUT promoting (a rejected/aborted/superseded migration).
|
|
375
|
+
// Idempotent. The live session is untouched — legacy transport continues.
|
|
376
|
+
fn discard_rotation (cid: global_id) -> nil
|
|
377
|
+
{
|
|
378
|
+
if (m_staged cid) != NIL { delete m_staged cid. }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Pure reads — the byte-comparable session-id evidence the core FSM gates on (staged for the
|
|
382
|
+
// commit/confirm equality checks; active as the replayed-commit authentication evidence,
|
|
383
|
+
// spec §5.5 lost-confirm path). NIL when the respective slot is empty.
|
|
384
|
+
fn staged_session_id (cid: global_id) -> bin+
|
|
385
|
+
{
|
|
386
|
+
s IS bin+ = m_staged cid.
|
|
387
|
+
if s == NIL { return NIL. }
|
|
388
|
+
return session_id (s as bin).
|
|
389
|
+
}
|
|
390
|
+
fn active_session_id (cid: global_id) -> bin+
|
|
391
|
+
{
|
|
392
|
+
s IS bin+ = m_sessions cid.
|
|
393
|
+
if s == NIL { return NIL. }
|
|
394
|
+
return session_id (s as bin).
|
|
395
|
+
}
|
|
396
|
+
|
|
276
397
|
// RECEIVE: decrypt an inbound envelope for a known sender and COMMIT the advanced session
|
|
277
398
|
// (and, on the establish path only, the rotated account). Returns the typed e2e_result.
|
|
278
399
|
// The caller (the __t_wrapper decode hook) authenticates $emsignature FIRST and supplies
|
|
279
400
|
// the sender's e2e identity key (read from the same AD identity it origin-binds against).
|
|
280
401
|
fn decrypt_and_commit (from_cid: global_id, sender_ik: bin, olm_type: int, ciphertext: bin) -> e2e_result
|
|
281
402
|
{
|
|
282
|
-
// Pre-key (establish) REPLAY guard. v1 publishes a MULTI-USE fallback key (empty OTK
|
|
283
|
-
// list), so a replayed olm_type=0 pre-key would re-establish successfully — re-executing
|
|
284
|
-
// the inner transaction AND overwriting the live (possibly advanced) session. If we
|
|
285
|
-
// already hold a session for this sender and THIS pre-key is the same handshake that
|
|
286
|
-
// established it, it is an idempotent re-delivery: drop it recoverably (no re-establish,
|
|
287
|
-
// no re-deliver, session untouched). session_matches is a pure read on the existing
|
|
288
|
-
// session; a genuine NEW handshake (different ephemeral) does not match and re-establishes.
|
|
289
403
|
existing IS bin+ = m_sessions from_cid.
|
|
290
|
-
|
|
404
|
+
staged IS bin+ = m_staged from_cid.
|
|
405
|
+
|
|
406
|
+
if olm_type == 0
|
|
291
407
|
{
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
408
|
+
// Pre-key (establish) REPLAY guard. v1 publishes a MULTI-USE fallback key (empty OTK
|
|
409
|
+
// list), so a replayed olm_type=0 pre-key would re-establish successfully — re-running
|
|
410
|
+
// the inner tx AND overwriting the (possibly advanced) session. session_matches is a
|
|
411
|
+
// pure read; a genuine NEW handshake (different ephemeral) does not match.
|
|
412
|
+
// (a) matches the LIVE session — idempotent re-delivery, drop recoverably.
|
|
413
|
+
if existing != NIL && (session_matches (existing as bin) ciphertext)
|
|
414
|
+
{ return replayed(). }
|
|
415
|
+
// (b) matches the STAGED session — a redelivered migration COMMIT after we already
|
|
416
|
+
// staged it (the lost-confirm case, spec §5.5). The wrapper turns this typed
|
|
417
|
+
// error into a re-confirm via core mig_handle_replayed_commit; NO inner
|
|
418
|
+
// re-dispatch, NO re-establish. active_session_id is the authenticated evidence.
|
|
419
|
+
if staged != NIL && (session_matches (staged as bin) ciphertext)
|
|
420
|
+
{ return replayed(). }
|
|
421
|
+
// DECODE-SEAM (§5.5, the load-bearing security change): a genuinely NEW pre-key on a
|
|
422
|
+
// cid that ALREADY holds a live session, WHILE a migration is pending, STAGES the
|
|
423
|
+
// fresh session instead of unilaterally replacing the live one. m_sessions[cid] is
|
|
424
|
+
// untouched until commit_rotation promotes the staged session. With NO pending
|
|
425
|
+
// migration the hook is FALSE and we fall through to the 0.8.0 path (a live-session
|
|
426
|
+
// pre-key self-heals by replacing — correct when there is no migration to protect).
|
|
427
|
+
if existing != NIL && (m_mig_pending from_cid)
|
|
428
|
+
{ return stage_inbound_rotation from_cid sender_ik ciphertext. }
|
|
295
429
|
}
|
|
430
|
+
// Normal ratchet (olm_type=1) that decrypts on the STAGED session: the initiator reading
|
|
431
|
+
// the peer's confirm on the fresh session BEFORE it promotes (decode-seam). Try staged
|
|
432
|
+
// first; a typed failure (the message was for the live session) falls through below.
|
|
433
|
+
if olm_type == 1 && staged != NIL
|
|
434
|
+
{
|
|
435
|
+
rs = decrypt (account()) staged sender_ik 1 ciphertext.
|
|
436
|
+
if rs $ok { m_staged from_cid -> (rs $session) as bin. return rs. }
|
|
437
|
+
}
|
|
438
|
+
// Default 0.8.0 path: establish (first-contact / self-heal) or decrypt on the LIVE session,
|
|
439
|
+
// committing the advance (and, on establish, the rotated account).
|
|
296
440
|
r = decrypt (account()) (m_sessions from_cid) sender_ik olm_type ciphertext.
|
|
297
441
|
if r $ok
|
|
298
442
|
{
|
|
@@ -301,4 +445,64 @@ loads libraries key_storage, address_document_types
|
|
|
301
445
|
}
|
|
302
446
|
return r.
|
|
303
447
|
}
|
|
448
|
+
|
|
449
|
+
// ⚠ MUST STAY BYTE-IDENTICAL to __t_wrapper.mm's inline e2e decode (S2 wire-pv + S1 emsig
|
|
450
|
+
// over ($from,$to,$envelope) + decrypt_and_commit, currently ~lines 96-104). Plan-F folds the
|
|
451
|
+
// wrapper's inline copy into THIS fn (single call) when the SDK bundle is regenerated (A); until
|
|
452
|
+
// then the two are a REVIEWED-IDENTICAL pair guarded by the mig-gate decode negative matrix
|
|
453
|
+
// (bad wire_pv / tampered emsig / wrong $to → abort; replay → !ok). Do NOT edit one without the
|
|
454
|
+
// other. See MigrationReview point-1 ruling (2026-07-15).
|
|
455
|
+
// The single AUTHENTICATED inbound-e2e entry for the core-migration boxed-envelope path
|
|
456
|
+
// (a2a_messaging handle_e2e_migrate_commit/_confirm). At plan-F the __t_wrapper decode hook
|
|
457
|
+
// folds its inline S1/S2 into THIS fn too (one audited implementation); until then the two are
|
|
458
|
+
// the reviewed-identical pair the ⚠ note above governs. S2 (wire dialect)
|
|
459
|
+
// then S1 (emsig over the SENDER + RECIPIENT + envelope context, so a captured envelope cannot
|
|
460
|
+
// be reflected to a third party or re-targeted) — both FORGERY aborts — then decrypt_and_commit
|
|
461
|
+
// (which owns the decode-seam staging + pre-key replay guards). The caller supplies the
|
|
462
|
+
// origin-bound sender SIGN key and e2e identity key (read from the SAME AD it origin-binds).
|
|
463
|
+
fn recv_authenticated_envelope (from_cid: global_id, to_cid: global_id, origin_sign_key: publickey_sign, sender_ik: bin, env: t_e2e_envelope, emsig: crypto_signature) -> e2e_result
|
|
464
|
+
{
|
|
465
|
+
// S2: reject an unknown/downgraded wire dialect before any decrypt/commit.
|
|
466
|
+
abort "E2E envelope wire-version mismatch" WHEN ((env $pv) as int) != wire_pv().
|
|
467
|
+
// S1: recompute the signed context from the ACTUAL $from/$to + envelope.
|
|
468
|
+
sig_context = ( $from -> from_cid, $to -> to_cid, $envelope -> env ).
|
|
469
|
+
abort "E2E envelope signature verification failed"
|
|
470
|
+
WHEN (_crypto_verify_hash_signature (_value_id sig_context |) emsig origin_sign_key) != TRUE.
|
|
471
|
+
return decrypt_and_commit from_cid sender_ik ((env $olm_type) as int) ((env $ciphertext) as bin).
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Core-migration boxed-envelope decode (a2a_messaging C.3, spec §5.5). The SDK wire schema has
|
|
475
|
+
// no e2e_signed_message transaction variant yet (a coordinated plan-F SDK-bundle regen), so the
|
|
476
|
+
// migration commit/confirm carry the staged-session Olm ciphertext as $targ INSIDE the legacy
|
|
477
|
+
// box; the handler drives the e2e decode here instead of the __t_wrapper decode-seam. SECURITY:
|
|
478
|
+
// identical S1/S2 auth to the wrapper (via recv_authenticated_envelope). The caller MUST pass
|
|
479
|
+
// from_cid = the box's AUTHENTICATED sender_id and peer_ad = THAT cid's verified AD; we re-bind
|
|
480
|
+
// here — (a) AD.container_id == from_cid, (b) the origin SIGN key's address == from_cid, (c) S1
|
|
481
|
+
// $from == from_cid — so a relay CANNOT box a captured envelope (its signed $from != the relay's
|
|
482
|
+
// box $from → S1 fails). to_cid = my container id (S1 $to binding: a commit addressed to a
|
|
483
|
+
// different recipient cannot be replayed into my session).
|
|
484
|
+
fn decode_migration_envelope (from_cid: global_id, to_cid: global_id, peer_ad: t_address_document, env: t_e2e_envelope, emsig: crypto_signature) -> e2e_result
|
|
485
|
+
{
|
|
486
|
+
identity = peer_ad $identity.
|
|
487
|
+
abort "e2e migration: peer AD container id does not match the box sender" WHEN (identity $container_id) != from_cid.
|
|
488
|
+
// origin-bound sender SIGN key — the SAME derivation as __t_wrapper check_origin_binding.
|
|
489
|
+
sign_key_id = identity $default_keys $SIGN.
|
|
490
|
+
sign_pub IS publickey_sign+ = NIL.
|
|
491
|
+
sc identity $key_list -- (k->) {
|
|
492
|
+
if _crypto_get_key_id k == sign_key_id {
|
|
493
|
+
sign_pub -> k SAFE(publickey_sign).
|
|
494
|
+
break.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
abort "e2e migration: SIGN key not found in sender identity key_list" WHEN sign_pub == NIL.
|
|
498
|
+
// DEFENSE-IN-DEPTH (do NOT delete as dead code): the sign-key-origin and bundle-presence
|
|
499
|
+
// aborts below are already guaranteed for a peer_ads[from_cid] AD by the offer/ack
|
|
500
|
+
// verify_identity_bundle (origin-binding via PoP+cid-bind) + GATE3 bundle-presence, which a
|
|
501
|
+
// peer MUST clear before it can reach commit. They become load-bearing if that upstream
|
|
502
|
+
// verify is ever weakened. (Not driven by the mig-gate guard — would need a forged AD.)
|
|
503
|
+
abort "e2e migration: sender origin address does not commit to signing key" WHEN (address_of_key sign_pub?) != from_cid.
|
|
504
|
+
bundle = (identity $e2e_bundle) SAFE(t_e2e_bundle).
|
|
505
|
+
abort "e2e migration: sender published no e2e bundle" WHEN bundle == NIL.
|
|
506
|
+
return recv_authenticated_envelope from_cid to_cid sign_pub? (bundle? $ik_curve) env emsig.
|
|
507
|
+
}
|
|
304
508
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adapt-toolkit/mufl",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"description": "The MUFL toolchain: prebuilt `mufl` (REPL) and `mufl-compile` (compiler) engine binaries plus the MUFL stdlib, meta, and transactions trees they need at run time. Linux x64 only; the binaries are proprietary (AEFL) — see LICENSE and NOTICE.",
|
package/prebuilds/linux-x64/mufl
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -176,7 +176,11 @@ library __t_wrapper loads libraries transaction, key_storage, transaction_queue,
|
|
|
176
176
|
if (envelope $to) { to_cid -> (envelope $to) as global_id. }
|
|
177
177
|
// AD-v2 peers publish an $e2e_bundle in their identity; a v1 peer has none (and
|
|
178
178
|
// cannot send E2E). Read the sender's identity key for the E2E establish path.
|
|
179
|
-
if (ob_identity $e2e_bundle) {
|
|
179
|
+
if (ob_identity $e2e_bundle) {
|
|
180
|
+
ob_bundle = (ob_identity $e2e_bundle) SAFE(address_document_types::t_e2e_bundle)
|
|
181
|
+
abort "Invalid E2E bundle in sender identity" WHEN IS NIL.
|
|
182
|
+
sender_ik -> ob_bundle? $ik_curve.
|
|
183
|
+
}
|
|
180
184
|
message -> envelope $body.
|
|
181
185
|
has_envelope -> TRUE.
|
|
182
186
|
} else {
|