mlua-swarm-cli 0.24.0

Command line interface for mlua-swarm (mse binary with serve / mcp subcommands).
Documentation
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
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# mse — Blueprint authoring guide

A **Blueprint** is the unified package of a `flow.ir` program plus the Swarm
extension layers (agent bindings, Operator role definitions, compiler
hints/strategy, metadata). This guide covers the shape you write by hand or
generate programmatically; for the exact, always-current JSON Schema fetch
`mse://api/blueprint-schema`.

## Top-level shape

```jsonc
{
  "schema_version": "0.1.0",          // optional, defaults to the current schema version
  "id": "my-blueprint",               // required, unique within your namespace
  "flow": { "kind": "seq", "children": [] }, // required, a flow.ir Node (see below)
  "agents": [ /* AgentDef[] */ ],      // optional, default []
  "operators": [ /* OperatorDef[] */ ], // optional, default []
  "hints": { "per_agent": {}, "global": {} }, // optional
  "strategy": { "strict_refs": true, "strict_kind": true }, // optional
  "metadata": { "description": "...", "tags": [] }, // optional
  "spawner_hints": { "layers": [] },   // optional, middleware capability keys
  "default_agent_kind": "operator",    // optional, defaults to "operator"
  "default_operator_kind": "automate", // optional, no default (falls through the cascade)
  "degradation_policy": "warn"         // optional, "warn" (default) | "fail" (opt-in, schema-only today — see the worker degradation reporting section in `mse://guides/operator-execution-model`)
}
```

All fields except `id` and `flow` are optional and fall back to sensible
defaults. `deny_unknown_fields` is enforced throughout the schema — a typo in
a field name is a hard parse error, not a silently-ignored key.

## Flow node kinds (`flow.ir` `Node`)

Every node is tagged with a `kind` discriminator:

| kind      | fields                                             | behavior                                                                                     |
|-----------|------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| `step`    | `ref`, `in`, `out`                                    | Dispatch the agent named `ref` with the evaluated `in` expr as input; write the result to `out` (must be a `path` expr). |
| `seq`     | `children` (`Node[]`)                                 | Evaluate children in order, threading the ctx through each.                                    |
| `branch`  | `cond`, `then`, `else`                                | Evaluate `cond` (must resolve to a JSON bool); run `then` if true, `else` if false.             |
| `loop`    | `counter`, `cond`, `body`, `max`                      | Writes `0` to `counter`, then repeats `body` while `cond` is truthy and `counter < max`, incrementing `counter` after each iteration. |
| `fanout`  | `items`, `bind`, `body`, `join`, `out`                | Evaluate `items` to an array; run `body` once per element (bound to `bind` in a branch-local ctx); aggregate into `out` per `join` mode: `all` (every branch runs, array of final ctx), `any` (first success wins), `race` (first to settle wins), `all_settled` (never raises, per-item `{status, value|reason}` record). |
| `try`     | `body`, `catch`, `err_at?`                            | Run `body`; on error, roll back ctx writes, optionally write the error message to `err_at`, then run `catch`. |
| `assign`  | `at`, `value`                                         | Pure ctx transform: evaluate `value` against the ctx snapshot and write it to `at`. No agent dispatch. |

`out` / `at` / `counter` must always be `path` exprs (write targets).

### Fanout lanes, `$.results`, and the aggregate gate

`fanout` is the one node whose result is not readable the way it looks.
Read this before writing a flow that branches on a fanout's outcome.

**A lane is a disjoint ctx.** `body` runs once per item against a *copy*
of the parent ctx plus the `bind` write. Lane writes never reach the
parent ctx and never reach each other; only `out` crosses back.

**Dispatch arithmetic.** `body` runs once per item, whole. K steps in
the body and N items is K x N dispatches, not K. The canonical body is
one step. Per-item *agent* selection needs a `branch` on the bound item
inside the body (`Step.ref` is a static string on the wire, so it cannot
be computed from the item) — that is the shape `mse bp new fanout`
scaffolds.

**Feeding `items` from a worker's output.** A final body (or staged
part) whose bytes are a JSON object / array folds structured into the
ctx by default, so `items = $.planner.lanes` — or
`items = $.planner.parts["plan-meta.json"].lanes` when the planner
stages the JSON as a part — resolves with no declaration, as long as the
worker actually emits a JSON container there. To make that a *contract*
rather than a best effort, declare `submit_format: "json"` on the
planner's meta channel (`AgentMeta.ctx` / `Blueprint.default_agent_ctx`
/ `$step_meta`): an unparseable final body is then rejected with `422`
instead of silently folding as a string and raising `PathNotFound` at
the fanout. Note the `{out, parts}` wrap: once a step stages any part,
its body moves to `$.<step>.out.lanes`. See
`mse://guides/worker-io-contract` § Structured worker output.

**What `$.results` contains.** Under `join = "all"`, one element per
item, each element the lane's *final ctx object* — not the step's
output. The step output sits inside it, under whatever path that step's
`out` named. So with `out = $.branch_out` in the lane, the value you
want is `$.results[i].branch_out`, not `$.results[i]`. Under
`all_settled` that same object is nested one deeper, under `.value`,
beside a `status`.

**You cannot index it.** flow.ir paths have exactly one segment kind, a
string key. The supported grammar is `$`, `$.a.b`, and `$["a.b"]` for
keys containing a dot — there is no array-index segment:

- `$.results[0]` is a **parse error**: the Blueprint is rejected on
  deserialize (`mse bp build`, `POST /v1/blueprints/:id`), not at run time.
- `$.results.0` parses as the string key `"0"` and raises `PathNotFound`
  at eval — a string key never indexes an array.

**No quantifier either.** No cond op ranges over an array: there is no
`any` / `all` / wildcard. `in` is not an escape hatch — it compares
*whole elements* structurally, and an element here is a full lane ctx
object, so it cannot express "some lane returned BLOCKED". `len` gives
you the lane count and nothing about the lanes.

**Therefore the aggregate step is the gate.** Hand `$.results` to one
agent and let it collapse the array into a scalar you *can* compare:

```jsonc
{"kind": "fanout", "items": {"op":"path","at":"$.d.targets"},
 "bind": {"op":"path","at":"$.item"}, "join": "all",
 "out": {"op":"path","at":"$.results"},
 "body": {"kind":"step","ref":"check",
          "in": {"op":"path","at":"$.item"},
          "out": {"op":"path","at":"$.branch_out"}}},
{"kind": "step", "ref": "aggregate",
 "in": {"op":"path","at":"$.results"},
 "out": {"op":"path","at":"$.aggregate"}},
{"kind": "branch",
 "cond": {"op":"eq","lhs":{"op":"path","at":"$.aggregate.verdict"},
          "rhs":{"op":"lit","value":"PASS"}},
 "then": {"...": "on pass"}, "else": {"...": "on block"}}
```

Give the aggregate agent a verdict contract so the gate compares a
scalar the engine already validated:

```jsonc
{ "name": "aggregate", "verdict": { "channel": "part", "values": ["PASS", "BLOCKED"] } }
```

See § Returning verdicts to drive BP flow below for the two channels, and
`mse://blueprints/samples/10-fanout` for a runnable version of this
shape.

**The one non-agent option.** `call_extern` (see § Expr ops) can fold
`$.results` in a host-registered pure function — the only way to reduce
the array without dispatching an agent. It is host-dependent: it works
only when the embedding host wired an externs registry
(`TaskLaunchService::with_externs`), so a Blueprint using it is not
portable to a plain `mse serve`.

**Writing this shape in the Lua DSL.** `bp_dsl`'s `B.stage "id" { fanout =
{...} }` emits exactly the node described here — items / bind / join / out
defaults, the one-step homogeneous body or the heterogeneous branch cascade
— while the aggregate stage stays an ordinary stage carrying the gate. The
semantics above are unchanged by the sugar; see
`mse://guides/dsl-authoring` § Fanout stages.

## Worker output: `out` vs named parts (GH #36)

A `step` node's OUTPUT is normally a single JSON value — the worker's
final `mse_worker_submit` `body` — addressable downstream via `{"op":
"path", "at": "$.<step>"}`.

A worker may additionally stage any number of *named* output parts
before completing the attempt: call `mse_worker_submit` with `name` set
(see `mse://guides/mcp-tool-reference` § Named multi-part output) once
per part, then finish with an ordinary plain (no-`name`) submit. The
set of `name` values a worker submits is its **staged-names allowlist**
— that allowlist alone determines what the engine folds into the
`parts` map on that step's OUTPUT. Any other artifact the worker (or a
middleware) emits — most notably the after-run audit sidecar
`audit:<step_ref>` (see § Reading prior-step OUTPUT below) — bypasses
the fold and is only reachable via the Worker axis. A step that staged
at least one part ends up with OUTPUT shape

```jsonc
{ "out": /* the final plain-submit body */ "...", "parts": { "plan.md": "...", "notes": { "todo": "..." } } }
```

instead of the plain final-submit body alone. A downstream step reads a
part with RFC 9535-style bracket-notation path syntax — required for any
key containing a literal `.`, like a filename:

```jsonc
{ "op": "path", "at": "$.<step>.parts[\"plan.md\"]" }
```

Bracket segments chain directly (`$.<step>.parts["a"]["b"]`) or combine
with dot segments in either order (`$.<step>.parts["notes"].todo`); keys
support no escaping (a literal `"` inside a name cannot be represented).

**Author caution**: once a step stages any parts, its OUTPUT becomes an
Object (`{"out": ..., "parts": {...}}`) instead of the plain final-submit
value — a downstream `eq`/`ne` expr comparing `$.<step>` directly against
a string (or other scalar) no longer matches; address `$.<step>.out`
instead (or a `parts[...]` entry). Keeping a worker's staging behavior in
sync with the Blueprint's `in` exprs that read its output is the
Blueprint author's responsibility — nothing in the schema enforces it
automatically.

## Reading prior-step OUTPUT (Worker axis): `context.steps`

The `$.<step>` / `$.<step>.parts[...]` paths above are the **BP axis** —
a downstream step declares what to read at Blueprint-authoring time and
the folded value flows into its `in`. Fine when the caller knows the
shape in advance.

The **Worker axis** is the complementary read-back path for a SubAgent
that decides at runtime which prior-step OUTPUT to pull. Every step's
OUTPUT is dual-recorded in the engine's `OutputStore` and surfaced via
`WorkerPayload.context.steps` as a `StepPointer` (GH #20 Contract C; see
`mse://guides/operator-execution-model` § Hop 4), filtered by the
current step's `ContextPolicy.steps` allowlist / `steps_exclude`
denylist:

```jsonc
"context": {
  "steps": {
    "planner":       { "name": "planner",       "size_bytes": 1834, "file_path": "/…/ctx/planner.json",       "content_url": "…", "sha256": "…" },
    "audit:planner": { "name": "audit:planner", "size_bytes":  412, "file_path": "/…/ctx/audit-planner.json", "content_url": "…", "sha256": "…" }
  }
}
```

Key facts:

- **Pointer-only invariant** — a `StepPointer` never carries the OUTPUT
  content itself (no preview, no content bytes inline). The SubAgent
  fetches the actual bytes via `file_path` (local FS `Read`) or
  `content_url` (server HTTP GET, verifiable against `sha256`). Choose
  `file_path` on same-host SubAgents (the common case); choose
  `content_url` when the SubAgent runs elsewhere.
- **`audit:<step_ref>` is a first-class entry** — after-run audit
  artifacts (GH #34) surface as top-level `context.steps` keys named
  `audit:<step_ref>`, **not** as a nested field of the audited step. The
  BP axis (`$.<step>.audit[...]` or similar) does not reach them — audit
  findings are observational sidecars that the fold-final path drops
  from the BP-chain value but the Data-plane dual-write preserves for
  Worker-axis consumers. See `mse://guides/operator-execution-model` §
  After-run audits.
- **Keys are canonical step names** — the map key is the step's
  canonical name resolved from `AgentMeta.projection_name` (GH #23), so
  `ContextPolicy.steps` / `steps_exclude` entries are matched against
  canonical names, not any renamed alias.
- **BP-chain scope vs Data-plane scope** — the fold-final path
  (`fold_final_and_parts`, `src/core/engine.rs`) only stages a step's
  `mse_worker_submit`-with-`name` artifacts (the `staged_names`
  allowlist from § Worker output) into the BP-chain value. Other
  artifacts (audits, out-of-band submissions) bypass fold but remain
  reachable via `context.steps`.

## Returning verdicts to drive BP flow (canonical pattern)

A **verdict** is a small scalar (e.g. `"PASS"`, `"BLOCKED"`, `"ALLOW"`) an
agent emits so that a downstream `branch` or `loop` node can compare it
via `eq($.<step>, lit("BLOCKED"))` and pick a path. `eq` is a
**structural** compare — the whole value at `$.<step>` must equal the
whole `lit(...)` value. That constraint decides how the agent shapes its
submit body.

Two shapes are canonical; a third is a frequently-attempted anti-pattern.

### Pattern A — plain body carries the verdict scalar

The agent's `mse_worker_submit` body is the verdict literal, nothing
else:

```
mse_worker_submit(body="BLOCKED")
```

- Step OUTPUT is exactly the string `"BLOCKED"` — no `parts` field.
- BP-side: a downstream `"in": {"op": "path", "at": "$.gate"}` observes
  the scalar directly, and
  `{"op": "eq", "lhs": {"op": "path", "at": "$.gate"}, "rhs": {"op": "lit", "value": "BLOCKED"}}`
  matches.
- Trade-off: the submit body has room for the verdict only — no
  human-readable report co-exists with it on that step.
- When to use: pure gates where the verdict is the whole point. Working
  example: `mse://blueprints/samples/03-fn-override` — the `mock-gate`
  agent's system prompt is literally `` Always reply `BLOCKED` ``, and
  the top-level `branch` fires on `eq($.verdict, lit("BLOCKED"))`.

### Pattern B — named part carries the verdict, plain body carries the report

The agent stages the verdict as a named part first, then finishes the
attempt with a plain (unnamed) submit whose body is the human-readable
report:

```
mse_worker_submit(name="verdict", body="BLOCKED")     # stage the verdict
mse_worker_submit(body=<full YAML / markdown report>) # finish the attempt
```

- Step OUTPUT shape becomes
  `{"out": <the full report>, "parts": {"verdict": "BLOCKED"}}`.
- BP-side: the verdict is addressed with bracket notation —
  `{"op": "eq", "lhs": {"op": "path", "at": "$.gate.parts[\"verdict\"]"}, "rhs": {"op": "lit", "value": "BLOCKED"}}`
  — while the full report stays reachable as `$.gate.out` for
  downstream consumers (a resolver agent that needs the failure detail,
  or a report artifact for humans).
- Trade-off: the agent issues two `mse_worker_submit` calls, and the BP
  author must know to address the `parts["verdict"]` entry, not the
  plain step name.
- When to use: gates whose verdict must drive flow *and* whose full
  report is a first-class artifact.

### Anti-pattern — full report in the plain body, `eq` against the step name

An agent that submits a full YAML/markdown report as its only submit
body and expects `eq($.gate, lit("BLOCKED"))` to fire **cannot work**:
`$.gate` resolves to the whole report string; `lit("BLOCKED")` is the
four-character literal; they never compare equal. The `branch`'s `else`
path fires on every dispatch, which reads to a caller like the verdict
path was silently swallowed even though the agent said `BLOCKED`.

Debug rule: if a gate's `then` path never fires while the agent visibly
outputs a verdict word, check the submit shape first. It must be a
scalar (Pattern A) or a named part (Pattern B) — `$.gate` cannot be a
report body that *contains* the verdict word.

### Enforcing verdict contracts (opt-in)

Pattern A/B above are conventions — until an agent opts in, nothing
checks that its submit shape actually matches how a downstream `cond`
addresses it. `AgentDef.verdict` is an **optional** field that turns
that convention into two machine checks. It is strictly additive: an
agent that declares no `verdict` behaves exactly as before, byte for
byte, at both boundaries described below. The working sample
`mse://blueprints/samples/02-verdict-loop` is a live example of this —
its `mock-gate` agent declares no `verdict` field and continues to
register and run unchanged; Pattern A's convention alone is still
enough for it.

Declare a contract on the agent whose output a `cond` will compare:

```jsonc
// channel: "body" — Pattern A, the plain step OUTPUT IS the verdict
"agents": [{
  "name": "gate",
  "verdict": {
    "channel": "body",
    "values": ["PASS", "BLOCKED"]
  }
}]
```

```jsonc
// channel: "part" — Pattern B, the verdict is staged as the named part
"agents": [{
  "name": "gate",
  "verdict": {
    "channel": "part",
    "values": ["PASS", "BLOCKED"]
  }
}]
```

`channel: "part"` addresses one literal part name only —
`mse_worker_submit(name="verdict", body=...)` / `$.gate.parts["verdict"]`
— the way Pattern B is documented above. `values` is a closed set of
tokens; a comparison against anything outside it is a violation.

**Register time (compile, read-only lint).** `Compiler::compile` walks
every `Branch`/`Loop` `cond`'s `Eq`/`Ne`/`In` comparisons of a step
output `Path` against a literal, resolves the `Path` back to its
producing agent, and — only for agents that declared a `verdict` —
checks two things:

- The `Path` addresses the channel the agent declared (bare `$.<step>`
  for `channel: "body"`, `$.<step>.parts.verdict` /
  `$.<step>.parts["verdict"]` for `channel: "part"`). A mismatch fails
  the compile with `CompileError::VerdictChannelMismatch`, naming the
  step, the declared channel, and the channel the `cond` actually
  addressed.
- Every literal compared against that `Path` (including every entry of
  an `In` haystack) is a member of the declared `values`. A literal
  outside the set fails the compile with
  `CompileError::VerdictValueNotInContract`, naming the offending
  literal and the declared set.

Compile fails on the **first** violation found (same posture as the
compiler's other static checks). If the `cond` references an agent
that declared **no** `verdict` field, nothing is rejected — at most a
`tracing::warn!` is emitted, and compilation still succeeds. This is
what keeps every pre-existing Blueprint, and every Blueprint whose
authors haven't opted in yet, compiling unchanged.

**Completion time (server, fail-loud producer gate — all 3 completion
routes).** A contract-bearing agent's attempt can complete through 3
different routes: `POST /v1/worker/submit`, the older
`POST /v1/worker/result`, or the WS Operator fallback (a worker
process that never POSTs at all). GH #50 originally gated only the
first of these, and only for `channel: "body"`; GH #51 closes the
remaining gaps by moving the check to the single choke point every
route funnels through — `Engine::submit_worker_result_trusted` /
`Engine::submit_output`, embedded immediately before the value is
written to `output_tail`, not re-implemented per route handler:

- `channel: "body"` — the completing value must be a member of
  `values`.
- `channel: "part"` — a staged `"verdict"` artifact must exist for the
  attempt (**presence**, not just membership — a worker that never
  calls `POST /v1/worker/artifact?name=verdict` at all is now rejected,
  the gap GH #51 exists to close) AND its value must be a member of
  `values`.
- `ok=false` completions are exempt on every route, identically — a
  transport-level failure (`DispatchOutcome::Blocked`, the flow.ir Try
  path) is not a verdict and is never validated against the contract.
- An agent that declared no contract, or declared a contract for the
  other channel, sees this gate as a no-op — behavior unchanged from
  before GH #50/#51.

A violation is rejected **before** the value reaches `output_tail` / the
flow ctx: `POST /v1/worker/submit` and `POST /v1/worker/result` both
surface HTTP 422, echoing the declared `values` (`channel: "body"`
violations) or naming the missing `"verdict"` part (`channel: "part"`
violations). The WS Operator route has no HTTP response to return a 422
on — a rejected completion there simply never writes its `Final`; the
attempt's `output_tail` has no `Final` and the downstream dispatch path
naturally treats it as incomplete (a `tracing::warn!` is logged
server-side). No new WS protocol message is introduced for this — the
deliberate "zero flow-ir changes" design choice, not a gap left to
fill.

The staging-time check at `POST /v1/worker/artifact?name=verdict`
(`channel: "part"` membership only, not presence) still runs — it gives
the worker the fastest possible feedback the moment it stages a bad
token. The completion-time check above is the backstop that guarantees
enforcement no matter which of the 3 routes an agent's attempt actually
completes through.

Together, the register-time and completion-time boundaries turn both
halves of the silent never-match anti-pattern above into loud
failures: an authoring mistake (`cond` addressing the wrong channel, or
comparing against a token the agent will never emit) stops at register
time; a worker that emits a full report where a token was expected, or
skips staging the verdict part entirely, stops at completion time on
every route it could have completed through. Neither boundary touches
`flow.ir` itself — no new `Expr` forms, no eval hooks, no `FlowNode`
rewriting; `Blueprint.flow` stays exactly what the author wrote, and
the contract lives entirely in the Blueprint/schema/compiler/server
layers described here.

#### Symptom → cause: `dispatch failed: no Final in output_tail`

The rejection above is what an author most often meets from the *other*
end — as a missing-Final symptom on a step that looks like it ran fine.
This table is the reverse lookup, because the symptom string does not
contain the word "verdict":

| symptom | likely cause | fix |
|---|---|---|
| `no Final in output_tail`, and the worker's own logs show it emitted a result | `channel: "body"` contract + a terminal value that is not one of `values` (a report / JSON object / prose where a bare token was required) | switch the agent to `channel: "part"` and stage the token separately, or emit the bare token as the body |
| `no Final in output_tail` on a `channel: "part"` agent | nothing was staged under the name `verdict` for that attempt | stage it before the terminal emit (`bus.emit("artifact", …)` in-process, `POST /v1/worker/artifact?name=verdict` over HTTP) |
| `verdict contract violation: … is not a member of the declared values …` | same as row 1, seen with the cause attached | as above |

Rows 1 and 2 are the same rejection as row 3 — only the diagnostic
differs by route. In-process (`kind: agent_block` / `kind: lua`) and HTTP
completions carry the cause into the dispatch error; the WS Operator
fallback emit logs a `tracing::warn!` server-side and leaves the attempt
without a `Final`, so on that route the bare symptom is still what the
caller sees.

Both halves are also visible **before** any dispatch: `bp_doctor`'s
`verdict_contract_lint` family reports every declared verdict value that
no downstream `cond` reads, which is the state a decorative contract is
in. See the next section.

### Declared verdict values must be handled downstream (opt-in strict mode)

The two register-time checks above are the **forward-direction** lint:
"every `Lit` a `cond` compares against must be a member of the agent's
declared `verdict.values`" and "every `cond` must address the declared
channel." The **reverse-direction** lint — "every entry of the declared
`verdict.values` set must be referenced by at least one downstream
`Branch`/`Loop` `cond`" — catches the complementary drift where a flow
author declares a verdict value (e.g. `"BLOCKED"`) but forgets to write
a branch that handles it.

By default the reverse-direction lint only surfaces
`tracing::warn!` at compile — the compile still succeeds. This preserves
back-compat with existing Blueprints that intentionally leave some
declared values as silent-pass informational tokens (an agent may want to
document "we may emit `INFO` as well" without demanding every caller
branch on it).

That default is quiet in the wrong place, though: a `tracing::warn!` goes
to the server's log, not to the response the author is reading. So the
same check is also exposed as a **report-only `bp_doctor` family**,
`verdict_contract_lint`, which runs on an already-registered Blueprint
with no opt-in and reports one `verdict_value_unhandled` WARN per
unhandled declared value:

```
bp_doctor(id = "<bp>")
  → verdict_contract_lint: { findings: [
      { check: "verdict_value_unhandled", severity: "WARN",
        agent: "gate-danger", value: "PASS", channel: "body",
        declared_values: ["PASS", "BLOCKED"], step_ref: "gate-danger",
        message: "… no downstream Branch/Loop cond ever compares against it …" }
    ] }
```

Findings also appear in the unified `diagnostics` array under the kind
`verdict-value-unhandled` — the same kind the strict compile error
projects to. Disable the family with `disable_verdict_contract_lint=true`
for a Blueprint that deliberately declares informational tokens.

The state worth catching this way is a flow with **no `Branch` at all**
plus a `channel: "body"` contract on every gate: it compiles clean, every
declared value is unhandled, and the contract is not merely decorative —
`channel: "body"` constrains the terminal OUTPUT value too, so each gate
that returns a report has its `Final` rejected at completion time. The
lint reports it as N findings before the first dispatch; without it the
first signal is a missing-Final symptom at run time.

#### Aggregate: `verdict_contract_never_read`

A normal halt gate always leaks one `verdict_value_unhandled` finding per
agent (the halt cond only reads the halt token, so the always-unread
`PASS` shows up as a per-value WARN even in a healthy Blueprint). That
baseline noise structurally hides the actual defect this section is about
— the whole gate being dropped so **every** declared value on an agent
goes unread. The concrete regression: a bp.lua authored against the pre-
`bafe47d4` cascade rules (pipeline-level `halt_on` implicitly gates every
stage) rebuilt against the post-flip rules (stages must opt in explicitly
via `gate = true` / stage-level `halt_on` / `retry`) — the opt-OUT stages
silently stopped emitting gates, but each still leaks one baseline WARN,
indistinguishable from a normal one.

To separate the two, the family additionally emits a per-agent aggregate
finding `verdict_contract_never_read` (WARN — one per agent whose entire
declared `verdict.values` set is unread). The count of these equals the
number of agents whose gate is fully dead; the per-value baseline stays
in place for parity with `strict_verdict_handling`. Aggregate findings
appear first in `findings[]`:

```
bp_doctor(id = "<bp>")
  → verdict_contract_lint: { findings: [
      { check: "verdict_contract_never_read", severity: "WARN",
        agent: "gate-danger", channel: "body",
        declared_values: ["PASS", "BLOCKED"], step_ref: "gate-danger",
        message: "… no downstream Branch/Loop cond reads any of them — the
                  contract is decorative and this step cannot halt the
                  flow. Add a gate that reads the verdict (e.g.
                  `gate = true` on the B.pipeline stage) …" },
      { check: "verdict_value_unhandled", severity: "WARN",
        agent: "gate-danger", value: "PASS", … },
      { check: "verdict_value_unhandled", severity: "WARN",
        agent: "gate-danger", value: "BLOCKED", … }
    ] }
```

The aggregate projects to the `verdict-contract-never-read` diagnostic
kind and carries a concrete `Suggestion { patch: "gate = true,",
applicability: MaybeIncorrect }` — `MaybeIncorrect` because the fix
presumes a `B.pipeline` stage record (a hand-rolled `Branch` needs the
equivalent shape by hand). Both aggregate and per-value findings fold
into `verdict_contract_lint_warn_count` and the top-level aggregate
verdict; disabling the family drops both.

To promote the warning to a hard `CompileError::VerdictValueUnhandled`,
opt in via `Blueprint.metadata`:

```json
{
  "metadata": {
    "strict_verdict_handling": true
  }
}
```

Under `strict_verdict_handling: true`, `Compiler::compile` rejects any
Blueprint where a contract-bearing agent declares a `verdict.values`
entry that no downstream `Branch`/`Loop` `cond` references. The
diagnostic names the agent, the unhandled value, the full declared
`values` set (so the fix is unambiguous — either add a handler branch
or drop the value from the declaration), and the `Step.ref_` where the
agent is invoked (best-effort — when the agent is invoked at multiple
sites, the first-encountered site is reported).

Two ways to satisfy the strict lint:

1. **Add a branch per declared value.** The canonical shape — one
   `Branch` per value, or one `Branch` per value pair (e.g. `"PASS"` in
   the `then_`, `"BLOCKED"` in the `else_`).
2. **Cover the whole set with one `In`.** An `In` cond whose `Lit`
   haystack lists every declared value counts every entry as handled in
   one node — useful when the flow author wants a single "any of these
   verdict values ⇒ proceed" branch.

The forward and reverse lints run in the same walk, so the strict
setting has no extra runtime cost; either both fire or neither does.
The setting is a Blueprint-level opt-in (per BP, not per agent), so a
flow author who wants the strict check on some agents but not others
can either split those agents into a separate Blueprint or leave the
setting off and rely on the default `tracing::warn!` output.

### Cross-links

- Named-parts wire format and OUTPUT shape: § Worker output: `out` vs
  named parts (above).
- Working samples that exercise Pattern A end-to-end:
  `mse://blueprints/samples/02-verdict-loop` (a `loop` that retries
  while `$.verdict == "BLOCKED"`) and
  `mse://blueprints/samples/03-fn-override` (a `branch` that hands a
  BLOCKED gate result to an approver step).
- Agent-side declaration (which pattern the agent's own Output format
  section commits to): `mse://guides/agent-md-authoring` § Output
  contract: inline body vs `@file:` sentinel.
- The static verifier that surfaces some verdict-related drift
  (`declared_tools` vs wrapper grants, projection-name/parts-shape
  changes downstream): `mse://guides/agent-md-authoring` § Verifying
  how your agent materializes.

## Expr ops (`flow.ir` `Expr`)

Every expr is tagged with an `op` discriminator:

| op       | fields                    | result                                                                 |
|----------|---------------------------|-------------------------------------------------------------------------|
| `path`   | `at` (e.g. `"$.x.y"`)      | Read a value from ctx. Raises if the path is missing.                   |
| `lit`    | `value`                    | A literal JSON value.                                                   |
| `eq`     | `lhs`, `rhs`               | Structural equality.                                                     |
| `ne`     | `lhs`, `rhs`               | Structural inequality.                                                   |
| `lt` / `lte` / `gt` / `gte` | `lhs`, `rhs` | Comparison: both numbers (`f64`) or both strings (lexicographic, Lua `<` parity). Mixed types raise. |
| `not`    | `arg`                      | Boolean negation (truthy-based).                                        |
| `and`    | `args` (array)             | Short-circuit conjunction; empty array → `true`.                        |
| `or`     | `args` (array)             | Short-circuit disjunction; empty array → `false`.                       |
| `exists` | `arg` (expr)               | `true` iff `arg` resolves to a non-`null` value (missing path → `false`, present-but-`null``false`). |
| `add` / `sub` / `mul` / `div` / `mod` | `lhs`, `rhs` | Numeric arithmetic (`f64`); `div` / `mod` by zero raises. `mod` follows Lua `%` (result takes the sign of `rhs`). |
| `len`    | `arg`                      | Element count (array), char count (string), or key count (object).      |
| `in`     | `needle`, `haystack`       | `true` if `needle` equals any element of the `haystack` array.          |
| `call_extern` | `ref`, `args` (array) | Invoke a host-registered pure function (`Externs` registry) with the evaluated `args`. Unregistered `ref` raises. Value-shape only — no side effects, no flow control. |

No op quantifies over an array — there is no `any` / `all` / wildcard,
and `in` compares whole elements structurally. Gating on a collection
(a `fanout`'s `out`, most commonly) goes through a step that reduces it
to a scalar first: see § Fanout lanes, `$.results`, and the aggregate
gate.

`call_extern` requires the host to register an externs registry
(`TaskLaunchService::with_externs`); without one every `call_extern`
raises an extern error.

Truthy semantics match Lua/JS: `null`/`false` are falsy, everything else
(including `0` and `""`) is truthy.

## Agents (`AgentDef`) and kind resolution

### Two authoring paths

An `AgentDef` can be written in two places, and either is fine:

- **Direct JSON literal (this guide's default form)** — the
  `AgentDef` object appears inline inside the Blueprint JSON. All
  fields (`name`, `kind`, `spec`, `profile.system_prompt`,
  `profile.worker_binding`, `profile.tools`, `meta`, …) are set
  literally in the JSON tree. This is the default authoring shape
  for the samples under `mse://blueprints/samples/*` and for
  programmatic authoring (algocline strategies, skills, dogfood
  harnesses).
- **`$agent_md` file ref** — the entry is a single-key object
  `{ "$agent_md": "agents/foo.md" }` and the loader parses the
  target file's frontmatter (+ Markdown body) into a
  fully-populated `AgentDef`. See the `$agent_md file-ref
  expansion` section below.

Compile-time error messages that name a field (e.g.
`profile.worker_binding`) are actionable on either path — for JSON
authors, add the field to the JSON literal; for `$agent_md` authors,
add it to the `.md` frontmatter. The messages themselves spell both
paths out.

### `AgentDef` shape (JSON-direct form)

Each entry in `agents` maps a name (referenced from `flow.Step.ref`) to a
backend:

```jsonc
{
  "name": "my-agent",
  "kind": "rust_fn",           // lua | rust_fn | agent_block | subprocess | operator
  "spec": { "fn_id": "..." },  // free-form, interpreted per kind
  "profile": { "system_prompt": "...", "model": "...", "tools": [] }, // optional
  "meta": { "description": "...", "tags": [] } // optional
}
```

`AgentKind` is a closed enum (`lua`, `rust_fn`, `agent_block`, `subprocess`,
`operator`) — there is no string-escape-hatch variant. `spec` is free-form
per kind; the keys each kind reads are:

| kind | `spec` keys |
|---|---|
| `lua` / `rust_fn` | `fn_id` (factory registry key), or an inline `source` chunk for `lua` |
| `agent_block` | `script_path` / `project_root` / `mcp_rpc_timeout_ms` / `mcp_servers` — see the section below |
| `subprocess` | `program` + `args` (or a `Runner::Subprocess` template, GH #83) |
| `operator` | `operator_ref` |

When an agent omits
`kind`, resolution falls through a four-tier cascade (highest to lowest
priority): (1) per-`AgentDef.kind` literal, (2) the Blueprint's top-level
`default_agent_kind`, (3) a CLI-level default (e.g. `mse serve
--default-agent-kind`), (4) the schema `Default` impl (`operator`).

### `$agent_md` file-ref expansion

Instead of writing an `AgentDef` object inline, you can reference an
`agent.md` file (frontmatter + Markdown body) and let the loader expand it:

```jsonc
{ "agents": [ { "$agent_md": "agents/researcher.md" } ] }
```

This parses the file's frontmatter + body into a fully-populated `AgentDef`
(`profile.system_prompt`, `meta`, `spec`, etc.). Sibling keys alongside
`$agent_md` are shallow-merged onto the expanded object afterward — handy for
overriding just `spec.operator_ref` or `meta` while keeping the rest of the
`agent.md` content:

```jsonc
{ "$agent_md": "agents/researcher.md", "spec": { "operator_ref": "role-a" } }
```

**Path hygiene**: refs are resolved relative to the Blueprint file's own
directory. Absolute paths and any `..` parent-directory component are
rejected — refs are sandboxed inside the Blueprint's base-directory subtree.
The same rule applies to the more general `$file` ref (`{"$file": "path"}`),
which substitutes a referenced file's raw string contents anywhere in the
JSON tree (e.g. externalizing a large prompt out of a `Step.in` literal).

### Runners (GH #46): `Blueprint.runners` / `AgentDef.runner` / `runner_ref`

A `Runner` declares the execution shell an agent's Worker IMPL dispatches
into — tool grant, model selection, and runtime capabilities for the
backend it targets. Three variants exist today: `ws_operator` (the
platform-neutral standard for a Claude Code, Codex, or other joined MainAI;
`variant` is provider-defined and `tools` is the minimum requested grant),
`ws_claude_code` (the compatibility backend for existing Claude Code wrapper
Blueprints), and `agent_block_in_process`
(agent-block in-process runtime; `tools` is the effective, enforced tool
set). `AgentDef.kind = agent_block` pairs with an `agent_block_in_process`
Runner; every other `AgentDef.kind` normally pairs with `ws_operator`.

Runners are declared through a named, BP-level registry
(`Blueprint.runners: [{ "name": ..., "runner": {...} }]`) — the same
registry shape as `Blueprint.metas` — and resolved per-agent through a
5-tier cascade (highest priority first):

1. `AgentDef.runner` — an inline `Runner` object on the agent itself.
2. `AgentDef.runner_ref` — a name looked up in `Blueprint.runners`.
3. Legacy fallback: `profile.worker_binding` synthesizes a
   `ws_claude_code` Runner from `{ variant: worker_binding, tools:
   profile.tools }`**deprecated** while Blueprints migrate onto
   `runner` / `runner_ref`.
4. `Blueprint.default_runner` — a BP-wide registry name, used only when
   no tier above (1–3) applies to this agent.
5. No Runner declared through any tier — the agent has none.

Note tier 3 outranks tier 4: an agent's own `profile.worker_binding`
still wins over the Blueprint's `default_runner`, mirroring the
`AgentInline > MetaRef > BpGlobal` precedence the ctx-supply cascade
already follows (agent-level declarations always beat BP-global ones).

```jsonc
{
  "runners": [
    { "name": "review-worker", "runner": {
        "backend": "ws_operator", "variant": "mse-worker-reviewer", "tools": ["Read", "Grep"]
    } }
  ],
  "default_runner": "review-worker",
  "agents": [
    { "name": "reviewer", "kind": "operator", "spec": { "operator_ref": "main-ai" }, "runner_ref": "review-worker" }
  ]
}
```

At Run start MSE resolves this cascade once into an immutable `BoundAgent`
snapshot. The snapshot pins the full Agent definition (including role prompt
and verdict contract), the resolved Runner, and the effective static context
policy. Its `binding_digest` is persisted with the Run launch snapshot and
copied onto each step trace; replay keys include the digest, so identical
step input under a different binding is not treated as the same execution.

The legacy `profile.worker_binding` tier is projected only at the Claude Code
compatibility boundary. New Blueprints should use `runner` or `runner_ref`.
Servers default to `legacy_worker_binding_policy = "allow"`; setting it to
`"reject"` (or passing `--legacy-worker-binding-policy reject`) turns this
fallback into a launch-time error. The policy affects fresh resolution only;
persisted Run snapshots retain their pinned Runner.
The Runner's `tools` remain requested/declarative for `ws_operator` and
`ws_claude_code` until
an injected `AgentBindingProvider` attests the execution environment's
effective grant. The generic path is for the Operator/MainAI to implement
that interface. `ManifestBindingProvider` is the reusable reference
implementation: Claude Code and Codex plugins inspect only their own
environment, produce `AgentProviderManifest`, and delegate the common
request-to-receipt mapping to it. The standard Server maps
`AgentDef.spec.operator_ref` to the role claimed by `mse_operator_join` and
resolves the submitted `capability_manifest`; it never reads wrapper files
from the Server filesystem. Core validates one receipt
per requested agent, requires every requested tool and the exact launch
variant, then pins the accepted model, tools, provider revision, and optional
capability snapshot digest as `BindingAttestation`. That attestation is included in the
final `binding_digest` and persisted in the Run snapshot. Resume and replay
reuse it without asking the provider to resolve mutable environment state
again. MSE does not misreport declaration data as an enforced capability.

### In-process agents: `kind = agent_block` (GH #86)

An `agent_block` agent runs headless inside the server process over the
agent-block-core SDK — no operator round-trip, no child process. It is the
backend for deterministic in-process lanes (validation gates, after-run
audits like `mse://blueprints/samples/05-after-run-audit-agent-block`).

Two modes, selected by whether `spec.script_path` is present:

| Mode | Trigger | What runs |
|---|---|---|
| **PromptBasedAgent** | `spec.script_path` absent | The host embeds an invoker that calls the SDK's `agent` module with the declared MCP servers. |
| **ScriptBasedAgent** | `spec.script_path = "<path>"` | Your Lua script runs instead; it owns its own MCP connections. |

`spec` keys (all optional):

```jsonc
{
  "script_path": "gates/danger.lua",   // absent => PromptBasedAgent mode
  "project_root": "/abs/path",         // compile-time fallback cwd
  "mcp_rpc_timeout_ms": 30000,         // default 30s
  "mcp_servers": [                     // pool the tool grant selects from
    { "name": "outline", "command": "outline-mcp", "args": [] }
  ]
}
```

**`mcp_servers[].command` resolution.** The MCP server process is spawned
as a child of the **`mse serve` process**, so a bare command name is
looked up on that process's `PATH` — not your shell's. Under `mse serve`
started from a login shell that is usually the same thing. Under the
`mse server install` LaunchAgent it is not: launchd gives the daemon the
fixed `EnvironmentVariables.PATH` from
`~/Library/LaunchAgents/com.mse.server.plist`, which lists the
`--cargo-bin` directory plus the standard system dirs and nothing else.
For anything installed elsewhere, give an absolute path:

```jsonc
{ "name": "docs", "command": "<your-mcp-binary>", "args": [] }                  // bare: must be on the daemon's PATH
{ "name": "docs", "command": "/opt/homebrew/bin/<your-mcp-binary>", "args": [] } // absolute: always resolves
```

The alternative is to extend `EnvironmentVariables.PATH` in that plist
and reload the job (`launchctl kickstart -k gui/$(id -u)/com.mse.server`).
A command that fails to resolve surfaces at MCP-connect time, not at
Blueprint compile time.

**Input.** Three Lua globals, all per-task and none via the server process
env:

| global | carries |
|---|---|
| `_PROMPT` | the step's evaluated `in`, as a **String** — a structured `in` arrives JSON-stringified, so use `std.json.decode(_PROMPT)` if you want a table |
| `_CONTEXT` | `profile.system_prompt` |
| `_TASK_METADATA` | the launch's `init_ctx.task_metadata` bag, as a real Lua table |
| `_AGENT_CTX` | the Blueprint-declared agent context (`default_agent_ctx` / `AgentMeta.ctx`) after `ContextPolicy` filtering, as a real Lua table |

An absent field sets no global at all, so a script can branch on `nil`.
A `kind: lua` agent gets the same `_TASK_METADATA` / `_AGENT_CTX` pair, so
a gate is portable between the two in-process backends.

Prior-step OUTPUT is not delivered as a global: an in-process gate reads
it through its own `in` expression (`in: $.<prior_step>`), which is more
direct than the pointer list a WebSocket worker has to fetch.

The per-task working directory is not a global: `init_ctx.work_dir` /
`init_ctx.project_root` outrank `spec.project_root` and become the SDK's
`project_root`, which a script reads as `std.env.project_root()` and which
is the default cwd for `sh.exec` and for MCP servers started by
`mcp.connect`. It does **not** `chdir` the server process, so a bare
`io.open("rel/path")` still resolves against the server's own cwd.

**Result.** A script returns its result by calling `bus.emit(<kind>,
payload)` — **not** by returning a value from the chunk. One kind is
reserved:

| emit kind | effect |
|---|---|
| `"artifact"` | stages a named part — `{ name = "...", content = ... }`, `name` required — and lets the script keep running. Any number of these. |
| anything else | the terminal result, **first emit wins**. The host takes `payload.content`, else `payload.response`, else the whole payload, as the step OUTPUT body. |

Both verdict channels work. `channel: "body"` compares the terminal value:

```lua
bus.emit("worker_result", { ok = true, response = "PASS" })
```

`channel: "part"` compares a staged `verdict` part, leaving the body free
for the report:

```lua
bus.emit("artifact", { name = "verdict", content = "PASS" })
bus.emit("worker_result", { ok = true, response = "the full prose report" })
```

The step's value is then `{"out": "the full prose report", "parts":
{"verdict": "PASS"}}` — downstream reads the report at `$.<step>.out` and
branches on `$.<step>.parts["verdict"]`. A step that stages nothing keeps
the plain body, unwrapped.

Pick the channel by what the body has to carry. A gate whose body IS the
report needs `channel: "part"` — under `channel: "body"` that same script
has its `Final` rejected at completion time, and the step fails with
`dispatch failed: no Final in output_tail` (see § Symptom → cause above;
the in-process lane carries the contract violation into the dispatch
error, so the cause travels with it).

**Tool grant.** The effective set is the resolved
`agent_block_in_process` Runner's `tools` when a Runner is declared,
otherwise `profile.tools`. A declared-but-empty list is an enforced-empty
grant, not "unset" — that is how a Blueprint revokes an agent.md's
inherited `tools:` line. The override is pinned into the Run's immutable
`BoundAgent` snapshot at launch, so editing `Blueprint.runners` afterwards
does not change an in-flight Run's grant.

Enforcement is **server-granular**, and differs by mode. PromptBasedAgent
embeds only the `spec.mcp_servers` entries named by an
`mcp__<server>__<tool>` entry of the effective set — an unlisted server is
unreachable, but *every* tool of a listed server is reachable (a connected
MCP server exposes its full tool list to the model). ScriptBasedAgent
cannot be enforced at all, because the script calls `mcp.connect` itself;
declaring `mcp__` entries alongside `spec.script_path` is therefore a
compile error rather than a silent no-op — drop them and let the script own
its connections, or drop `spec.script_path` to get an enforced grant.
Non-`mcp__` names (`Read`, `WebSearch`, …) select no server and are inert
in both modes.

```jsonc
{
  "name": "gate-danger",
  "kind": "agent_block",
  "spec": { "script_path": "gates/danger.lua" },
  "runner": { "backend": "agent_block_in_process", "tools": [] }
}
```

### Execution assurance: `strategy.strict_binding`

Runner-backed agents describe *requested* capabilities; whether MSE demands
a Core-validated provider attestation for them before the Run may launch is
one Blueprint-level switch, `strategy.strict_binding` (default **false**):

- **`strict_binding = false` (default)** — the Blueprint runs without any
  capability manifest at all. An agent whose provider offers no attestation
  (no manifest, the role never joined, or the manifest declares no matching
  launch variant) stays `DeclarationOnly` — its `runner.tools` / `model`
  remain requested/declarative, the Run launches, and the unattested state is
  recorded on `RunRecord.degradations` for after-the-fact observation. The
  requesting side's declaration is carried into the spawn frame so the
  Operator can self-check its own environment (see
  `mse://guides/operator-execution-model` § Operator self-check).
- **`strict_binding = true`** — launch requires a Core-validated provider
  attestation for **every** Runner-backed agent. A missing manifest, a missing
  variant, an insufficient tool grant, or no provider at all fails the launch
  before any Spawn, and the error names the agent plus the requested
  variant/tools it could not satisfy.

This default is deliberately the opposite of `strict_refs` / `strict_kind`
(both default `true`): those guard the Blueprint's *structural* integrity,
which is always resolvable at compile time, whereas binding attestation is an
*execution-assurance opt-in* that needs a live execution environment to attest
against — not available for embed-only or manifest-less launches.

```jsonc
{
  "strategy": { "strict_refs": true, "strict_kind": true, "strict_binding": true }
}
```

Whichever mode a Blueprint is in, the semantics rule is the same: **attestation
is optional, but never wrong** — a receipt that *exists* and contradicts the
request (a tool short of the grant, the wrong launch variant, a digest or model
mismatch) fails in both modes. `strict_binding` controls only whether an
*absent* attestation is tolerated, never whether a *contradicting* one is.

Two tools discover what a Blueprint's Runner-backed agents require, so an
operator can build (or audit) a manifest before launch:

- **Requirements introspection**`GET /v1/blueprints/:id/binding-requirements`
  returns `{blueprint_id, strict_binding, requirements: [BindRequest, …]}`, one
  entry per Runner-backed agent with its declared variant / tools / model (the
  reverse lookup an operator machine-generates a manifest from).
- **`bp_doctor` `binding_lint` family** — the static pass surfaces
  `binding_requirements_info` (INFO: what each Runner-backed agent requests),
  `strict_binding_without_runners` (WARN: `strict_binding = true` with no
  Runner-backed agent — a no-op strict), and `legacy_worker_binding` (WARN:
  `profile.worker_binding` in use) on the top-level `binding_lint.findings`
  array. See `mse://guides/mcp-tool-reference`.

## Versioning

`metadata.version_label` is an optional free-form SemVer string (e.g.
`"1.2.3"`) used as the match target when reading a stored Blueprint by
version. Store readers select a version via one of three selectors:

- `Latest` — the store's current head (the default when unspecified).
- `Fixed { value }` — one exact, previously-committed version.
- `SemverReq { req }` — resolve to the newest stored version whose
  `version_label` satisfies a `semver::VersionReq` (e.g. `"^1.2"`).

`version_label` is rewritten automatically by the Enhance loop on
PATCH/MINOR/MAJOR bumps; you do not need to hand-maintain it once a
Blueprint is under Enhance management.

## Where to go next

- Three worked examples: `mse://blueprints/samples/01-pure-ctx-eval` (zero
  agent dispatch, pure ctx math), `mse://blueprints/samples/02-verdict-loop`
  (retry loop with a self-managed counter), `mse://blueprints/samples/03-fn-override`
  (a blocked verdict overridden by an approver step).
- The exact, always-current JSON Schema: `mse://api/blueprint-schema` (note:
  `flow` itself is opaque in the schema — its grammar is owned by the
  `mlua-flow-ir` crate, referenced above).
- Tool-level operations (running, archiving, schema fetch): `mse://guides/mcp-tool-reference`.
- Verifying an `AgentDef`'s materialized tools/ctx/output before a run
  (`bp_explain_agent`): `mse://guides/agent-md-authoring` §
  Verifying how your agent materializes.
- The DSL surface for authoring Blueprints directly in Lua (Expr method chains, Node builders, bp_dsl pipeline sugar): mse://guides/dsl-authoring, with two DSL samples: mse://blueprints/samples/06-dsl-verdict-loop and mse://blueprints/samples/07-dsl-pipeline.