Repository navigation
Expand file tree
/
Copy pathcipherclerk.rs
More file actions
983 lines (912 loc) · 44.3 KB
/
Copy pathcipherclerk.rs
File metadata and controls
983 lines (912 loc) · 44.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
//! App-framework cipherclerk handle.
//!
//! Apps are *userspace*: they should not reach past the SDK into
//! `dregg_turn::builder::ActionBuilder` or hand-encode `[0u8; 64]`
//! placeholder signatures. Instead, the framework hands them a narrow,
//! cipherclerk-bound action-construction surface backed by the SDK's
//! [`dregg_sdk::AgentCipherclerk`].
//!
//! See the crate root docs for the lineage of the name "cipherclerk"
//! (Greg Egan's *Polis*); the SDK's [`AgentCipherclerk`] is the broad
//! 100+-method surface and [`AppCipherclerk`] is the narrow ~6-method
//! handle apps actually see.
//!
//! ## What this gives apps
//!
//! - [`AppCipherclerk::cell_id`] — the agent's canonical CellId in its default
//! federation domain (no string-threading every call).
//! - [`AppCipherclerk::public_key`] — the cipherclerk's identity (32-byte public key).
//! - [`AppCipherclerk::make_action`] — build a single-method action with
//! multiple effects, signed for the framework's federation_id binding.
//! - [`AppCipherclerk::make_turn`] — wrap a signed action in a Turn with
//! sane defaults (nonce/forest hash filled by the executor path).
//! - [`AppCipherclerk::sign_action`] — re-sign a pre-built action.
//! - [`AppCipherclerk::sign_turn`] — sign a fully assembled Turn for a remote
//! executor.
//!
//! ## What apps cannot do through this handle
//!
//! - Extract the underlying signing key (only the framework holds the SDK
//! cipherclerk; apps see [`AppCipherclerk`] which deliberately exposes no
//! key-export methods).
//! - Mutate the cipherclerk's receipt chain or wallet.
//! - Reach into `AgentCipherclerk`'s 107-method surface — that's an SDK
//! concern, not an app concern.
//!
//! ## Why a wrapper and not `&AgentCipherclerk`?
//!
//! Exposing `AgentCipherclerk` directly to apps couples the userspace surface
//! to every method we add to the SDK. The framework cipherclerk handle is the
//! intentional narrow waist — when an app needs a new primitive, it's
//! either a *new framework method* (small, reviewed) or a *missing SDK
//! method* (we add it once, the framework method delegates).
//!
//! ## Federation binding
//!
//! Action signatures carry a 32-byte `federation_id` to prevent
//! cross-federation replay (see `dregg_turn::executor::TurnExecutor::compute_signing_message`).
//! The framework holds *one* federation_id per process — set at
//! [`AppCipherclerk::new`] — and threads it into every `make_action` /
//! `sign_action` call. Apps never see it.
use std::sync::{Arc, Mutex, RwLock};
use dregg_sdk::{AgentCipherclerk, AgentRuntime, SignedTurn};
use dregg_turn::action::{Action, Effect};
use dregg_turn::{Turn, TurnReceipt};
use dregg_types::{CellId, PublicKey};
/// A cipherclerk handle suitable for app-level userspace.
///
/// Wraps an [`AgentCipherclerk`] and a `federation_id`, exposing only the
/// methods apps need to build signed actions and turns. Cheap to clone
/// (internally `Arc<RwLock<AgentCipherclerk>>` — same shared cell as the
/// embedded executor's runtime, so signing the cipherclerk sees the same
/// receipt chain head as turn submission).
#[derive(Clone)]
pub struct AppCipherclerk {
inner: Arc<RwLock<AgentCipherclerk>>,
federation_id: [u8; 32],
domain: String,
}
impl AppCipherclerk {
/// Construct an app cipherclerk from an SDK cipherclerk and the federation
/// identifier this app operates in.
///
/// The default domain is `"default"` — matches `AgentCipherclerk::cell_id("default")`.
/// Override with [`Self::with_domain`].
pub fn new(cipherclerk: AgentCipherclerk, federation_id: [u8; 32]) -> Self {
Self {
inner: Arc::new(RwLock::new(cipherclerk)),
federation_id,
domain: "default".to_string(),
}
}
/// Construct an app cipherclerk from an already-shared SDK cipherclerk handle.
///
/// Use this when the cipherclerk is *also* owned by an
/// [`EmbeddedExecutor`]'s runtime — both this handle and the
/// runtime's lock guard share the same underlying agent identity
/// and receipt chain. The framework constructs the shared handle
/// itself in [`EmbeddedExecutor::app_cipherclerk`]; apps rarely need to
/// call this directly.
pub fn from_shared(
cipherclerk: Arc<RwLock<AgentCipherclerk>>,
federation_id: [u8; 32],
) -> Self {
Self {
inner: cipherclerk,
federation_id,
domain: "default".to_string(),
}
}
/// Set the default domain used by [`Self::cell_id`] and
/// [`Self::make_turn`].
pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
self.domain = domain.into();
self
}
/// This cipherclerk's public key (the agent identity).
pub fn public_key(&self) -> PublicKey {
self.read().public_key()
}
/// This cipherclerk's CellId in the framework's default domain.
pub fn cell_id(&self) -> CellId {
self.read().cell_id(&self.domain)
}
/// This cipherclerk's CellId in an explicit domain (rarely needed; prefer
/// [`Self::cell_id`]).
pub fn cell_id_for(&self, domain: &str) -> CellId {
self.read().cell_id(domain)
}
/// The federation_id this cipherclerk signs against.
pub fn federation_id(&self) -> &[u8; 32] {
&self.federation_id
}
/// Build a self-signed [`Action`] targeting one cell with a list of
/// effects.
///
/// The action carries a real `Authorization::Signature(..)` — no
/// `[0u8; 64]` placeholders. The signature binds to this cipherclerk's
/// public key, the action's canonical bytes, and the framework's
/// federation_id.
pub fn make_action(&self, target: CellId, method: &str, effects: Vec<Effect>) -> Action {
self.read()
.make_action(target, method, effects, &self.federation_id)
}
/// Build an Ed25519-only action for a browser's in-tab executor.
///
/// A `wasm32-unknown-unknown` module cannot link the verified Lean ML-DSA core, and the PQ
/// audit gate correctly aborts rather than silently substituting the Rust fallback. Browser
/// worlds are therefore an explicitly local replay surface: they authorize their embedded
/// turns classically, retain the exact receipt tape, and require a native verified host to
/// re-execute/re-authorize any later publication. This method does not exist on native targets,
/// so server, node, and federation code cannot accidentally select the weaker profile.
#[cfg(target_arch = "wasm32")]
pub fn make_browser_local_action(
&self,
target: CellId,
method: &str,
effects: Vec<Effect>,
) -> Action {
let clerk = self.read();
let nonce = clerk.next_turn_nonce();
let unsigned = dregg_sdk::raw::unsigned_action_named(target, method, effects);
clerk.sign_action_classical(unsigned, &self.federation_id, nonce)
}
/// Build a self-signed [`Action`] targeting this cipherclerk's own cell.
///
/// Equivalent to `cclerk.make_action(cclerk.cell_id(), method, effects)`.
/// Use this for app-internal actions where the target is the agent's
/// own cell (transferring between an app's own cells, mutating the
/// app's local state slot, etc.) and the caller does not want to
/// repeat `cclerk.cell_id()` at every call site.
///
/// See `APPS-USERSPACE-GAPS.md` §Gap 3 for the design framing.
pub fn make_self_action(&self, method: &str, effects: Vec<Effect>) -> Action {
self.make_action(self.cell_id(), method, effects)
}
/// Re-sign an already-built [`Action`] with this cipherclerk, overwriting
/// its existing authorization.
///
/// Use this when an action needs to be assembled by lower-level
/// builders (e.g. multi-step `ActionBuilder` typestate flows that
/// the framework cannot anticipate) but should still carry a real
/// framework-issued signature.
pub fn sign_action(&self, action: Action) -> Action {
self.read().sign_action(action, &self.federation_id)
}
/// The turn nonce the next submitted turn will carry (the underlying
/// cipherclerk's receipt-chain length) — the value [`Self::sign_action`]
/// binds into the signature (`dregg-action-sig-v3`). Verifiers of a
/// framework-signed action recompute the signing message over this nonce.
pub fn next_turn_nonce(&self) -> u64 {
self.read().next_turn_nonce()
}
/// Wrap a signed [`Action`] in a [`Turn`] ready for submission.
///
/// The Turn's `agent` is `self.cell_id()`, `previous_receipt_hash` is
/// pulled from the cipherclerk's chain head, `nonce` defaults to 0 (the
/// caller's submission path is expected to set the real nonce; see
/// `dregg_sdk::AgentRuntime::execute`), and forest/tree hashes are
/// zeroed and filled in by `compute_turn_bytes` at signing time.
pub fn make_turn(&self, action: Action) -> Turn {
self.read().make_turn_for(&self.domain, action)
}
/// Wrap multiple already-signed [`Action`]s in one [`Turn`] (atomic
/// group). All actions appear as roots in the same call forest — they
/// commit or roll back together.
///
/// Use this for cross-action consistency in app settlement flows:
/// orderbook settlement (release one escrow + create the counterparty
/// escrow), escrow-swap, multi-leg trades, etc. The per-action
/// signatures are preserved as-is; this method does not re-sign.
///
/// See `APPS-USERSPACE-GAPS.md` §Gap 5 for the design framing.
pub fn make_turn_with_actions(&self, actions: Vec<Action>) -> Turn {
self.read()
.make_turn_with_actions_for(&self.domain, actions)
}
/// Sign a fully assembled [`Turn`] for remote submission.
///
/// App code still constructs actions through the narrow framework surface;
/// this method only covers the final transport envelope used by remote node
/// APIs such as `/turns/submit`.
pub fn sign_turn(&self, turn: &Turn) -> SignedTurn {
self.read().sign_turn(turn)
}
/// Build a signed [`Turn`] that mints a new cell from a deployed
/// factory descriptor via the canonical `Effect::CreateCellFromFactory`
/// path.
///
/// This is the *userspace* entry to constructor-transparency cell
/// birth: the extension cipherclerk's `window.dregg.createFromFactory`,
/// the wasm runtime's `create_agent`, and any in-process app that
/// mints cells go through here. No callers should reach past the
/// `AppCipherclerk` for `Effect::CreateCellFromFactory` — when they do, a
/// new framework method is the right answer.
///
/// # Arguments
///
/// * `factory_vk` — VK hash of a factory previously deployed via
/// `TurnExecutor::deploy_factory`. The factory must accept the
/// `mode` from `params`; mismatches surface as a runtime
/// `FactoryError::ModeMismatch` when the turn executes.
/// * `owner_pubkey` — the ed25519 public key of the new cell's owner.
/// * `token_id` — the token-domain identifier for the new cell.
/// * `params` — additional creation parameters (program VK, initial
/// fields/caps, mode). `params.owner_pubkey` is canonicalized to
/// `owner_pubkey` before signing so the effect-level owner and
/// descriptor-validation owner cannot diverge.
///
/// The issuing cell is this cipherclerk's `cell_id()`; the federation_id
/// is the cipherclerk's bound `federation_id`.
///
/// The returned `Turn` is fully signed (real
/// `Authorization::Signature(..)`); pair with
/// [`EmbeddedExecutor::submit_turn`] (or any in-process executor /
/// remote node `/turns/submit`) to actually mint the cell.
#[must_use = "the signed Turn must be submitted to an executor to actually mint the cell"]
pub fn create_from_factory(
&self,
factory_vk: [u8; 32],
owner_pubkey: [u8; 32],
token_id: [u8; 32],
mut params: dregg_cell::FactoryCreationParams,
) -> Turn {
params.owner_pubkey = owner_pubkey;
let issuer = self.cell_id();
self.read().create_from_factory(
issuer,
factory_vk,
owner_pubkey,
token_id,
params,
&self.federation_id,
)
}
/// Get a shared handle to the underlying SDK cipherclerk lock.
///
/// Used by the framework to construct an [`EmbeddedExecutor`] that
/// shares this cipherclerk's receipt chain and signing key. App
/// code should not call this — if you find yourself reaching here
/// from an `apps/*` crate, the framework is missing a narrow
/// method.
pub fn shared_cipherclerk(&self) -> Arc<RwLock<AgentCipherclerk>> {
Arc::clone(&self.inner)
}
/// Legacy alias for [`Self::shared_cipherclerk`].
#[doc(hidden)]
pub fn shared_cclerk(&self) -> Arc<RwLock<AgentCipherclerk>> {
self.shared_cipherclerk()
}
/// Take a read lock on the underlying SDK cipherclerk (panic-safe).
///
/// Lock poisoning is recovered by surfacing the (possibly stale)
/// inner value — matches the convention `dregg_sdk::AgentRuntime`
/// already uses.
fn read(&self) -> std::sync::RwLockReadGuard<'_, AgentCipherclerk> {
self.inner.read().unwrap_or_else(|e| e.into_inner())
}
}
impl std::fmt::Debug for AppCipherclerk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppCipherclerk")
.field("public_key", &hex_short(&self.public_key().0))
.field("domain", &self.domain)
.field("federation_id", &hex_short(&self.federation_id))
.finish()
}
}
/// An embedded executor + ledger handle suitable for app-internal turn
/// submission.
///
/// This is the "load-bearing" closure of `APPS-USERSPACE-GAPS.md` §Gap 4:
/// the framework owns a private [`AgentRuntime`] (cipherclerk + local ledger +
/// turn executor) that handlers can reach through an axum
/// `Extension<EmbeddedExecutor>`. When a handler builds a signed
/// [`Action`] via the [`AppCipherclerk`], it can immediately submit the
/// resulting [`Turn`] through this executor and receive a real
/// [`TurnReceipt`] — closing the "action authored and dropped on the
/// floor" pattern that the gap analysis flagged as the remaining seam
/// in the userspace surface.
///
/// Cheap to clone (the runtime sits behind an `Arc`).
///
/// ## When to use embedded vs federation client
///
/// - **`EmbeddedExecutor`** (this type) — for in-process apps that own
/// their own state (nameservice, identity, single-process demos). The
/// ledger lives inside the framework; no network call.
/// - **`FederationClient`** (future) — for apps that submit to a remote
/// federation node. Same handler-side shape; different backend.
///
/// Both routes converge on `submit_turn(turn) -> Result<TurnReceipt, _>`,
/// which is the only method handlers need.
#[derive(Clone)]
pub struct EmbeddedExecutor {
/// `AgentRuntime` is not Send/Sync because `TurnExecutor` holds a
/// `RefCell<EpochMinter>`. We make the framework-side wrapper
/// Send+Sync by serializing all access through a `Mutex`. This is
/// adequate for single-process apps (the embedded executor is
/// per-process — there is no contention model that benefits from
/// concurrent submission, since turns mutate the same ledger).
runtime: Arc<Mutex<AgentRuntime>>,
/// Cached read-only handle on the cell id so `Debug` and
/// `cell_id()` accessors do not have to take the mutex.
cell_id: CellId,
/// The app's DECLARED per-turn computron fee, or `None` for
/// [`DEFAULT_TURN_FEE`]. See [`Self::with_turn_fee`].
turn_fee: Option<u64>,
/// Whether the declared fee is a CEILING to price under rather than a flat charge.
/// See [`Self::with_metered_turn_fee`]. Default `false` — every existing app keeps the
/// flat-stamp behaviour byte for byte.
metered_fee: bool,
}
/// The per-turn computron fee [`EmbeddedExecutor::submit_turn`] stamps on a turn that
/// arrives with `fee == 0` and whose app declared nothing.
///
/// This is the historical hardcoded fallback, now named. It is a WHOLE-STATE budget, not a
/// meter reading: `TurnExecutor` treats `turn.fee` as the turn's computron LIMIT *and*
/// debits it in full from the agent (`execute`'s Phase 1, never rolled back — THE EPOCH §5
/// "fees as moves"). So it is simultaneously "how big a turn may I write" and "what does
/// every turn cost me", and raising it for one app would tax every other app's purse.
/// Apps whose state does not fit inside it declare their own with
/// [`EmbeddedExecutor::with_turn_fee`] instead.
pub const DEFAULT_TURN_FEE: u64 = 10_000;
impl EmbeddedExecutor {
/// Construct an executor that shares the given [`AppCipherclerk`]'s
/// underlying SDK cipherclerk — so the action-signing handle and the
/// turn-submission handle both see the same receipt chain head and
/// signing key.
///
/// The framework wraps the shared cipherclerk in an [`AgentRuntime`] —
/// which constructs a local [`dregg_cell::Ledger`] seeded with the
/// agent's cell (1M computrons default balance, see
/// `AgentRuntime::new_simple`).
///
/// `domain` is the agent's domain string; should match the
/// `AppCipherclerk`'s [`AppCipherclerk::with_domain`] setting if it was
/// customized. Defaults to `"default"`.
pub fn new(cipherclerk: &AppCipherclerk, domain: &str) -> Self {
let shared = cipherclerk.shared_cipherclerk();
let mut runtime = AgentRuntime::new(shared, domain);
runtime.set_local_federation_id(*cipherclerk.federation_id());
let cell_id = runtime.cell_id();
Self {
runtime: Arc::new(Mutex::new(runtime)),
cell_id,
turn_fee: None,
metered_fee: false,
}
}
/// Construct an executor wrapping a caller-provided runtime.
///
/// Use this when the app constructs an `AgentRuntime` with custom
/// shared state (e.g., a ledger restored from disk via
/// `AgentRuntime::with_ledger`). The framework does not own the
/// runtime in that case — it simply borrows it for submission.
pub fn from_runtime(runtime: AgentRuntime) -> Self {
let cell_id = runtime.cell_id();
Self {
runtime: Arc::new(Mutex::new(runtime)),
cell_id,
turn_fee: None,
metered_fee: false,
}
}
/// **DECLARE this app's per-turn computron fee**, replacing [`DEFAULT_TURN_FEE`] for
/// turns this executor submits with `fee == 0`.
///
/// An app declares a fee because `turn.fee` is the executor's computron LIMIT for the
/// turn: a world whose one seeding turn writes a 121-square board meters ~17k
/// computrons and is REFUSED by the 10 000 default
/// (`BudgetExceeded { limit: 10000, .. }`) before it can write its opening state. The
/// fee is not a cap the app can raise for free — `execute` debits it IN FULL from the
/// declaring agent's purse whether the turn lands or is refused — so the number is a
/// real budget the app owns: it must cover the app's largest turn, and its size
/// divided into the agent's endowment bounds how many turns that agent can ever take.
/// That trade is exactly why this is per-app and not a bigger shared default: raising
/// [`DEFAULT_TURN_FEE`] would charge every non-declaring app for a board it does not
/// have.
///
/// Declaring changes NOTHING for a turn that already carries a nonzero `fee` (the
/// caller's explicit budget still wins) and nothing for any app that never calls this.
pub fn with_turn_fee(mut self, fee: u64) -> Self {
self.turn_fee = Some(fee);
self
}
/// **DECLARE `ceiling` as a per-turn computron CEILING and pay each turn's OWN price under
/// it** — the metered form of [`Self::with_turn_fee`].
///
/// A flat fee is two numbers wearing one hat, and they want opposite things. As a LIMIT it
/// must cover the app's WIDEST turn (a world-cell's genesis seed writes its whole state); as
/// a CHARGE it is debited in full from the agent's fixed endowment on EVERY turn, so the
/// widest turn's price is what the narrowest turn pays. The endowment divided by that price
/// is a hard cap on how many turns the app can ever take — and it is a cap nobody chose:
/// automatafl, declaring a fee sized to its 137-write genesis, could open a match and then
/// play exactly 51 turns, seven of its rounds, before `InsufficientBalance` ended it mid-board
/// (`dregg-automatafl/tests/match_length_purse.rs`).
///
/// Metered mode splits the two back apart. The stamped fee becomes
/// `min(estimate_turn_cost(turn), ceiling)`:
///
/// * a turn that fits pays EXACTLY what the executor meters it at (the estimator and the
/// running meter walk the same charge points off the same cost table — see
/// [`AgentRuntime::estimate_turn_cost`]), so the endowment now bounds total WORK rather
/// than turn COUNT, and
/// * a turn that does not fit is stamped at `ceiling` and refused by the executor with
/// `BudgetExceeded { limit: ceiling, .. }` — the app's declared DoS bound still bites,
/// unchanged, which is the half of the flat fee worth keeping.
///
/// OPT-IN and additive: an app that never calls this keeps [`Self::with_turn_fee`]'s flat
/// stamp (or [`DEFAULT_TURN_FEE`]) exactly, and an explicit nonzero `turn.fee` from the
/// caller still wins over both.
pub fn with_metered_turn_fee(mut self, ceiling: u64) -> Self {
self.turn_fee = Some(ceiling);
self.metered_fee = true;
self
}
/// This app's declared per-turn fee (a flat charge, or the CEILING under
/// [`Self::with_metered_turn_fee`]), or `None` if it takes [`DEFAULT_TURN_FEE`].
pub fn declared_turn_fee(&self) -> Option<u64> {
self.turn_fee
}
/// Whether the declared fee is a metered CEILING ([`Self::with_metered_turn_fee`]) rather
/// than a flat per-turn charge.
pub fn meters_turn_fee(&self) -> bool {
self.metered_fee
}
/// Construct over a caller-authenticated durable ledger and receipt log.
///
/// The supplied [`AppCipherclerk`] must already contain the strictly
/// validated receipt chain that corresponds to `ledger`. The SDK restores
/// the agent nonce and executor causal head from those two artifacts before
/// this handle becomes usable for another turn.
pub fn from_restored_ledger(
cipherclerk: &AppCipherclerk,
domain: &str,
ledger: dregg_cell::Ledger,
) -> Result<Self, String> {
let shared = cipherclerk.shared_cipherclerk();
let mut runtime =
AgentRuntime::with_restored_ledger(shared, domain, Arc::new(Mutex::new(ledger)))
.map_err(|error| error.to_string())?;
runtime.set_local_federation_id(*cipherclerk.federation_id());
Ok(Self::from_runtime(runtime))
}
/// The cell id of the agent the embedded runtime drives turns from.
pub fn cell_id(&self) -> CellId {
self.cell_id
}
/// ROUTE (ii), app-framework side — install a HOST executor signing seed on
/// the embedded runtime, so every subsequently COMMITTED receipt carries a
/// genuine Ed25519 `executor_signature` over
/// [`dregg_turn::TurnReceipt::canonical_executor_signed_message`]
/// (dregg-turn Stage 9 R-4). This is the app-layer mirror of
/// [`dregg_sdk::AgentRuntime::set_executor_signing_key`]: a deos-app fire
/// path (`GatedAffordance::fire_through_executor_with` →
/// [`Self::submit_turn`]) then produces a FORGE-ADMISSIBLE receipt —
/// `dregg_turn::verify_receipt_signature_with_keys` against
/// [`Self::executor_pubkey`] passes, exactly the check a forge
/// `RequiredCheck::CommittedReceipt` runs. Additive + opt-in: without it the
/// runtime keeps today's UNSIGNED behaviour (`executor_signature == None`).
pub fn set_executor_signing_key(&self, seed: [u8; 32]) {
let mut rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.set_executor_signing_key(seed);
}
/// The Ed25519 executor PUBLIC key the embedded runtime signs receipts under
/// (derived from the installed seed via
/// [`dregg_sdk::executor_pubkey_from_seed`]), or `None` if no seed is
/// installed. A forge / auditor pins this in `trusted_executor_keys` to
/// admit this runtime's signed receipts.
pub fn executor_pubkey(&self) -> Option<[u8; 32]> {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.executor_pubkey()
}
/// The embedded runtime's current agent nonce — the value the next submitted
/// turn will ride (see [`Self::submit_turn_executed`], which sets
/// `turn.nonce = rt.nonce()`). Exposed so a caller can DETERMINISTICALLY
/// pre-image the next fire's `turn_hash` (naming a check turn before it runs,
/// the analogue of `dregg_doc::ExecutorDrivenDoc::planned_turn_hash`).
pub fn agent_nonce(&self) -> u64 {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.nonce()
}
/// THE SWAP — toggle producer mode on the embedded runtime (authority inversion).
///
/// When enabled, every turn this executor submits is committed by the VERIFIED Lean executor
/// (`dregg_turn::lean_apply::produce_via_lean`, via [`dregg_sdk::AgentRuntime`]) and the Rust
/// `TurnExecutor` is demoted to a logged differential. This is the seam through which the common
/// app substrate (and the discord-bot + starbridge-apps that inherit it) routes the user-facing
/// surface through the verified kernel.
///
/// Default mirrors `DREGG_LEAN_PRODUCER`. The producer is compiled into every native build
/// (Lean unconditional); it is absent only under the `no-lean-link` platform gate
/// (wasm32/zkvm). Returns the runtime's resulting producer-mode state.
pub fn set_lean_producer(&self, enabled: bool) -> bool {
let mut rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.set_lean_producer(enabled);
rt.lean_producer_enabled()
}
/// Whether producer mode is active on the embedded runtime. See [`Self::set_lean_producer`].
pub fn lean_producer_enabled(&self) -> bool {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.lean_producer_enabled()
}
/// Ensure a cell exists in the embedded ledger.
///
/// If the cell is not already present, it is inserted with the given
/// state. Used by integration tests that need multiple agent cells
/// (e.g. a voter whose cell is distinct from the executor's primary
/// agent) in the same ledger.
pub fn ensure_cell(&self, cell: dregg_cell::Cell) -> Result<(), String> {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
let mut ledger = rt.ledger().lock().unwrap();
let cell_id = cell.id();
if ledger.get(&cell_id).is_none() {
ledger.insert_cell(cell).map_err(|e| e.to_string())?;
}
Ok(())
}
/// Install a [`CellProgram`] on an existing cell in the embedded ledger.
///
/// Used by integration tests that need the executor to enforce
/// program constraints (e.g. `Monotonic`, `MonotonicSequence`) on a
/// cell created by `AgentRuntime::new`.
pub fn install_program(&self, cell_id: CellId, program: dregg_cell::CellProgram) {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
let mut ledger = rt.ledger().lock().unwrap();
if let Some(cell) = ledger.get_mut(&cell_id) {
cell.program = program;
}
}
/// Replace the embedded runtime's witnessed-predicate registry.
///
/// The embedded runtime defaults to the REAL STARK-backed registry (so
/// `SenderAuthorized { PublicRoot }` and the other `dregg-circuit`-backed
/// witnessed predicates enforce for real on the honest fire path). Call this
/// to opt out — e.g. `dregg_cell::WitnessedPredicateRegistry::empty()` for a
/// negative test that wants fail-closed `SenderAuthorized` — or to install a
/// host-context-extended registry
/// (`dregg_turn::executor::registry_with_real_verifiers_full`).
pub fn set_witnessed_registry(&self, registry: dregg_cell::WitnessedPredicateRegistry) {
let mut rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.set_witnessed_registry(registry);
}
/// Deploy a [`FactoryDescriptor`] into the embedded runtime's executor.
///
/// This is the app-side birth path: after deploying a starbridge-app's
/// factory, a `Turn` carrying `Effect::CreateCellFromFactory` (built via
/// [`AppCipherclerk::create_from_factory`]) births a child cell whose
/// slot caveats (`state_constraints`) are installed as its `CellProgram`
/// — so the gating actually bites on every subsequent turn that touches
/// the factory-born cell. Returns the deployed `factory_vk`.
pub fn deploy_factory(&self, descriptor: dregg_cell::FactoryDescriptor) -> [u8; 32] {
let mut rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
rt.deploy_factory(descriptor)
}
/// Run a closure with mutable access to the embedded ledger.
///
/// Used by integration tests that need to set up a governance cell's
/// initial state (fields, permissions, program) before driving actions
/// through the executor.
pub fn with_ledger_mut<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut dregg_cell::Ledger) -> R,
{
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
let mut ledger = rt.ledger().lock().unwrap();
f(&mut ledger)
}
/// The **live [`CellState`]** of `cell` in the embedded ledger, if present.
///
/// This is the read the deos affordance layer needs to gate on the cell's
/// LIVE STATE (the htmx-on-crack "may the button be pressed RIGHT NOW"): the
/// framework reads the cell's current state here, then a
/// [`crate::affordance::GatedAffordance`] evaluates its `state_cond` (the REAL
/// [`dregg_cell::CellProgram::evaluate`]) against it — so an app author does NOT
/// hand-thread `(old, new)` state at the HTTP boundary; the framework supplies
/// the live state. Returns `None` if the cell is not in the embedded ledger
/// (e.g. a remote cell, or one not yet birthed). A clone, so the lock is not
/// held by the caller.
///
/// `DEOS-APPS.md` §"the deos app model": durable verified state means "reads are
/// free … writes are verified turns." This is the in-process read; the pg-dregg
/// seam ([`crate::deos_app::PersistenceSeam::PgDregg`]) is the durable analogue.
pub fn cell_state(&self, cell: CellId) -> Option<dregg_cell::state::CellState> {
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
let ledger = rt.ledger().lock().unwrap();
ledger.get(&cell).map(|c| c.state.clone())
}
/// Submit a pre-built [`Turn`] to the embedded executor and return
/// its [`TurnReceipt`].
///
/// The submitted turn is executed against the framework's private
/// ledger. On success the agent's receipt chain is extended (in the
/// runtime's owned cipherclerk copy). This is the path that closes
/// `APPS-USERSPACE-GAPS.md` §Gap 4 — handlers can now actually
/// observe a receipt instead of building an action and dropping it.
#[must_use = "dropping the receipt silently discards proof that the turn was committed"]
pub fn submit_turn(&self, turn: &Turn) -> Result<TurnReceipt, ExecutorSubmitError> {
self.submit_turn_executed(turn).map(|(receipt, _)| receipt)
}
/// Like [`Self::submit_turn`] but ALSO returns the turn exactly as executed — with `fee`/`nonce`
/// normalized to what the executor ran, so the returned turn's `hash()` equals the receipt's
/// `turn_hash`. A caller retains this to cross-check later (e.g. an orchestration audit binding a
/// logged step to its receipt's authentic turn). Behaviour-identical to `submit_turn`.
pub fn submit_turn_executed(
&self,
turn: &Turn,
) -> Result<(TurnReceipt, Turn), ExecutorSubmitError> {
let mut turn = turn.clone();
let rt = self.runtime.lock().unwrap_or_else(|e| e.into_inner());
// A turn built by `AppCipherclerk::make_turn` carries `fee: 0` ("the framework
// decides"). Stamp the app's DECLARED budget if it has one, else the shared
// default. An explicit nonzero fee is the caller's own budget and is left alone.
if turn.fee == 0 {
let declared = self.turn_fee.unwrap_or(DEFAULT_TURN_FEE);
turn.fee = if self.metered_fee {
// METERED (`with_metered_turn_fee`): the declared number is a CEILING, and this
// turn pays its OWN price under it. Pricing here, before the nonce stamp and the
// stale-nonce re-sign below, is safe: neither the nonce nor a re-signature
// changes an effect, an authorization VARIANT, or the cost table, so the estimate
// is the one the executor will meter. `max(1)` keeps a (degenerate) zero-cost
// turn from re-entering this `fee == 0` branch downstream in
// `AgentRuntime::execute_turn`, which has a default of its own.
rt.estimate_turn_cost(&turn).min(declared).max(1)
} else {
declared
};
}
turn.nonce = rt.nonce();
// dregg-action-sig-v3 binds the SUBMITTING turn's nonce into every
// Full-commitment action signature. App clerks sign at build time over
// their next-turn nonce (the receipt-chain length), which can LAG the
// live replay counter stamped above: the executor's Phase-1 fee+nonce
// commit is never rolled back, so a REFUSED turn still burns an
// on-ledger nonce without growing the receipt chain. Repair EXACTLY
// the signatures this identity's own clerk produced over that stale
// nonce (`resign_full_commitment_at` verifies ownership before
// re-signing) — foreign or adversarial authorizations are never
// rewritten.
{
let clerk = rt.cipherclerk().read().unwrap_or_else(|e| e.into_inner());
let stale_nonce = clerk.next_turn_nonce();
let live_nonce = turn.nonce;
if stale_nonce != live_nonce {
let fed = rt.local_federation_id();
for root in &mut turn.call_forest.roots {
resign_own_stale_signatures(&clerk, &fed, stale_nonce, live_nonce, root);
}
}
}
let receipt = rt
.execute_turn(&turn)
.map_err(|e| ExecutorSubmitError(e.to_string()))?;
Ok((receipt, turn))
}
/// Convenience: submit a single signed [`Action`] by wrapping it in
/// a turn (via [`AppCipherclerk::make_turn`]'s shape) and running through
/// [`Self::submit_turn`].
///
/// The action's signature is preserved verbatim; the wrapping just
/// builds the canonical single-action call forest the executor
/// expects. Useful for endpoints that produced their own signed
/// action through [`AppCipherclerk::make_action`] and just want to ship it.
#[must_use = "dropping the receipt silently discards proof that the turn was committed"]
pub fn submit_action(
&self,
cipherclerk: &AppCipherclerk,
action: Action,
) -> Result<TurnReceipt, ExecutorSubmitError> {
let turn = cipherclerk.make_turn(action);
self.submit_turn(&turn)
}
}
/// Walk a call tree re-signing every action whose Full-commitment
/// authorization is PROVABLY `clerk`'s own signature over `stale_nonce`
/// (see [`AgentCipherclerk::resign_full_commitment_at`] — ownership is
/// verified before any rewrite, so foreign/adversarial signatures pass
/// through untouched).
fn resign_own_stale_signatures(
clerk: &AgentCipherclerk,
federation_id: &[u8; 32],
stale_nonce: u64,
live_nonce: u64,
tree: &mut dregg_turn::forest::CallTree,
) {
if let Some(resigned) =
clerk.resign_full_commitment_at(&tree.action, federation_id, stale_nonce, live_nonce)
{
tree.action = resigned;
}
for child in &mut tree.children {
resign_own_stale_signatures(clerk, federation_id, stale_nonce, live_nonce, child);
}
}
impl std::fmt::Debug for EmbeddedExecutor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmbeddedExecutor")
.field("cell_id", &self.cell_id)
.finish()
}
}
/// Error returned by [`EmbeddedExecutor::submit_turn`] /
/// [`EmbeddedExecutor::submit_action`] when the executor rejects the
/// submission.
///
/// Wraps the underlying `dregg_sdk::SdkError` string so the framework
/// surface does not leak the SDK error enum (apps just need to surface
/// the failure to clients; the structured details live in the
/// receipt-chain side of the runtime).
#[derive(Clone, Debug)]
pub struct ExecutorSubmitError(pub String);
impl std::fmt::Display for ExecutorSubmitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "embedded executor rejected submission: {}", self.0)
}
}
impl std::error::Error for ExecutorSubmitError {}
fn hex_short(bytes: &[u8]) -> String {
let n = bytes.len().min(8);
let mut s = String::with_capacity(2 * n + 1);
for b in &bytes[..n] {
s.push_str(&format!("{b:02x}"));
}
if bytes.len() > n {
s.push('…');
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cclerk_signs_action_with_real_signature() {
let sdk_cclerk = AgentCipherclerk::new();
let fed = [7u8; 32];
let cclerk = AppCipherclerk::new(sdk_cclerk, fed);
let target = CellId::from_bytes([1u8; 32]);
let action = cclerk.make_action(target, "noop", vec![]);
// The whole point: not Unchecked, and not a zero signature. Since the
// hybrid flip, the framework signing path emits HybridSignature
// (ed25519 + ML-DSA over the same canonical message).
match action.authorization {
dregg_turn::action::Authorization::HybridSignature {
ed25519, ml_dsa, ..
} => {
assert!(ed25519 != [0u8; 64], "ed25519 half must be non-zero");
assert!(!ml_dsa.is_empty(), "PQ half must be present");
}
other => panic!("expected HybridSignature variant, got {other:?}"),
}
}
#[test]
fn cclerk_make_turn_binds_to_default_domain() {
let sdk_cclerk = AgentCipherclerk::new();
let cclerk = AppCipherclerk::new(sdk_cclerk, [0u8; 32]);
let cell = cclerk.cell_id();
let action = cclerk.make_action(cell, "noop", vec![]);
let turn = cclerk.make_turn(action);
assert_eq!(turn.agent, cell);
assert_eq!(turn.nonce, 0);
}
#[test]
fn with_domain_changes_cell_id() {
let sdk_cclerk = AgentCipherclerk::new();
let w1 = AppCipherclerk::new(sdk_cclerk, [0u8; 32]);
let w2 = w1.clone().with_domain("alt-domain");
assert_ne!(w1.cell_id(), w2.cell_id());
}
// NOTE: the "sign_action overwrites Unchecked" test lives in
// `app-framework/tests/cipherclerk_sign_action.rs` (an integration test
// directory) — the in-`src/` grep guard
// (`tests/no_unchecked.rs`) refuses to allow the literal
// `Authorization::Unchecked` anywhere under `src/`, including in
// `#[cfg(test)]` blocks. That is by design.
#[test]
fn make_self_action_targets_cclerk_cell() {
// Gap 3: ergonomic wrapper for app-internal actions.
let sdk_cclerk = AgentCipherclerk::new();
let cclerk = AppCipherclerk::new(sdk_cclerk, [11u8; 32]);
let action = cclerk.make_self_action("local-bump", vec![]);
assert_eq!(action.target, cclerk.cell_id());
match action.authorization {
dregg_turn::action::Authorization::HybridSignature { ed25519, .. } => {
assert!(ed25519 != [0u8; 64]);
}
other => panic!("expected HybridSignature variant, got {other:?}"),
}
}
#[test]
fn create_from_factory_emits_signed_turn_with_factory_effect() {
// The canonical constructor-transparency mint path: AppCipherclerk wraps
// AgentCipherclerk::create_from_factory and binds it to the framework's
// federation_id. The returned Turn must carry one
// Effect::CreateCellFromFactory action with a real signature.
use dregg_cell::{CellMode, FactoryCreationParams};
use dregg_turn::action::{Authorization, Effect};
let sdk_cclerk = AgentCipherclerk::new();
let cclerk = AppCipherclerk::new(sdk_cclerk, [33u8; 32]);
let factory_vk = [44u8; 32];
let owner = [55u8; 32];
let token = [66u8; 32];
let params = FactoryCreationParams {
mode: CellMode::Hosted,
program_vk: None,
initial_fields: vec![],
initial_caps: vec![],
owner_pubkey: owner,
};
let turn = cclerk.create_from_factory(factory_vk, owner, token, params);
// Issuer is the cipherclerk's own cell.
assert_eq!(turn.agent, cclerk.cell_id());
// Exactly one root action, carrying the factory effect.
assert_eq!(turn.call_forest.roots.len(), 1);
let root = &turn.call_forest.roots[0];
assert_eq!(root.action.effects.len(), 1);
match &root.action.effects[0] {
Effect::CreateCellFromFactory {
factory_vk: fv,
owner_pubkey: op,
token_id: tid,
params,
} => {
assert_eq!(*fv, factory_vk);
assert_eq!(*op, owner);
assert_eq!(*tid, token);
assert_eq!(params.owner_pubkey, owner);
}
other => panic!("expected CreateCellFromFactory effect, got {other:?}"),
}
// Real signature, not Unchecked.
match &root.action.authorization {
Authorization::HybridSignature { ed25519, .. } => {
assert!(*ed25519 != [0u8; 64]);
}
other => panic!("expected HybridSignature variant, got {other:?}"),
}
}
#[test]
fn create_from_factory_canonicalizes_params_owner_to_effect_owner() {
use dregg_cell::{CellMode, FactoryCreationParams};
use dregg_turn::action::Effect;
let sdk_cclerk = AgentCipherclerk::new();
let cclerk = AppCipherclerk::new(sdk_cclerk, [33u8; 32]);
let owner = [55u8; 32];
let params = FactoryCreationParams {
mode: CellMode::Hosted,
program_vk: None,
initial_fields: vec![],
initial_caps: vec![],
owner_pubkey: [0xAA; 32],
};
let turn = cclerk.create_from_factory([44u8; 32], owner, [66u8; 32], params);
let root = &turn.call_forest.roots[0];
match &root.action.effects[0] {
Effect::CreateCellFromFactory { params, .. } => {
assert_eq!(params.owner_pubkey, owner);
}
other => panic!("expected CreateCellFromFactory effect, got {other:?}"),
}
}
#[test]
fn make_turn_with_actions_bundles_all_roots() {
// Gap 5: multi-action atomic turn.
let sdk_cclerk = AgentCipherclerk::new();
let cclerk = AppCipherclerk::new(sdk_cclerk, [22u8; 32]);
let t1 = CellId::from_bytes([1u8; 32]);
let t2 = CellId::from_bytes([2u8; 32]);
let a1 = cclerk.make_action(t1, "first", vec![]);
let a2 = cclerk.make_action(t2, "second", vec![]);
// Sanity: the two actions hash differently (different targets/methods).
assert_ne!(a1.hash(), a2.hash());
let turn = cclerk.make_turn_with_actions(vec![a1.clone(), a2.clone()]);
assert_eq!(turn.call_forest.roots.len(), 2);
assert_eq!(turn.call_forest.roots[0].action.target, t1);
assert_eq!(turn.call_forest.roots[1].action.target, t2);
// Per-action signatures preserved (not re-signed at turn level).
assert_eq!(turn.call_forest.roots[0].action.hash(), a1.hash());
assert_eq!(turn.call_forest.roots[1].action.hash(), a2.hash());
// Turn agent is the cipherclerk's default cell.
assert_eq!(turn.agent, cclerk.cell_id());
}
}