packet-tracer-skill 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +424 -42
- package/README.md +535 -250
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- package/docs/automation-controller-proof.md +35 -0
- package/docs/curated-donor-registry.md +11 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/github-launch-ops-0.2.3.md +37 -0
- package/docs/github-metadata.md +6 -4
- package/docs/hero-demo-plan.md +1 -1
- package/docs/home-iot-donor-proof.md +4 -4
- package/docs/industrial-programming-proof.md +48 -0
- package/docs/ipv4-routing-management-proof.md +37 -0
- package/docs/l2-resiliency-bgp-proof.md +60 -0
- package/docs/l2-security-qos-proof.md +59 -0
- package/docs/packet-tracer-feature-gap-atlas.md +174 -17
- package/docs/post-launch-follow-up.md +9 -5
- package/docs/proof-readiness-dashboard.md +69 -0
- package/docs/publish-preview-roadmap.md +6 -5
- package/docs/release-checklist.md +27 -13
- package/docs/release-notes-0.2.2.md +1 -1
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +20 -3
- package/examples/README.md +98 -69
- package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
- package/examples/gallery.md +94 -6
- package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
- package/examples/index.json +932 -4
- package/examples/local-sample-evidence.json +24 -0
- package/examples/proof-cards.json +117 -0
- package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
- package/package.json +60 -44
- package/pytest.ini +9 -0
- package/references/packettracer-feature-atlas.json +67 -17
- package/references/packettracer-sample-catalog.json +45287 -4525
- package/references/packettracer-sample-catalog.md +599 -259
- package/references/proof-readiness-candidates.json +352 -0
- package/scripts/build_examples_index.py +228 -35
- package/scripts/build_sample_catalog.py +24 -44
- package/scripts/corpus_runner.py +430 -0
- package/scripts/coverage_matrix.py +1842 -1319
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/feature_atlas.py +65 -1
- package/scripts/generate_pkt.py +8762 -4070
- package/scripts/intent_parser.py +2242 -1138
- package/scripts/local_donors.py +340 -0
- package/scripts/packet_tracer_env.py +846 -391
- package/scripts/pkt_annotate.py +218 -0
- package/scripts/pkt_codec.py +420 -181
- package/scripts/pkt_editor.py +2405 -1226
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1195
- package/scripts/twofish_diagnostics.py +48 -31
- package/scripts/usage_ledger.py +218 -0
- package/scripts/vendor/README.md +44 -37
- package/scripts/vendor/twofish_pure.py +321 -0
- package/scripts/workspace_repair.py +548 -508
- package/templates/pt900/donors/README.md +15 -0
|
@@ -1,1319 +1,1842 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from
|
|
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
|
-
"telnet"
|
|
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
|
-
"server_https"
|
|
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
|
-
if
|
|
640
|
-
return
|
|
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
|
-
return
|
|
823
|
-
if
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return
|
|
829
|
-
if
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return "
|
|
833
|
-
if
|
|
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
|
-
if
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
{
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
"
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
"
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
if
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
"
|
|
1235
|
-
|
|
1236
|
-
"
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
return
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
def
|
|
1319
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from intent_parser import IntentPlan
|
|
8
|
+
from sample_catalog import SampleDescriptor
|
|
9
|
+
|
|
10
|
+
MATURITY_LEVELS = [
|
|
11
|
+
"reference-known",
|
|
12
|
+
"inventory-supported",
|
|
13
|
+
"edit-supported",
|
|
14
|
+
"config-mutation-supported",
|
|
15
|
+
"safe-open-generate-supported",
|
|
16
|
+
"acceptance-verified",
|
|
17
|
+
]
|
|
18
|
+
MATURITY_RANK = {level: index for index, level in enumerate(MATURITY_LEVELS)}
|
|
19
|
+
|
|
20
|
+
DEVICE_FAMILY_MAP = {
|
|
21
|
+
"Router": "routers",
|
|
22
|
+
"Switch": "switches",
|
|
23
|
+
"MultiLayerSwitch": "multilayer switches",
|
|
24
|
+
"Server": "servers",
|
|
25
|
+
"PC": "end devices",
|
|
26
|
+
"Laptop": "end devices",
|
|
27
|
+
"Tablet": "end devices",
|
|
28
|
+
"Smartphone": "end devices",
|
|
29
|
+
"Printer": "end devices",
|
|
30
|
+
"IpPhone": "end devices",
|
|
31
|
+
"HomeVoip": "end devices",
|
|
32
|
+
"AnalogPhone": "end devices",
|
|
33
|
+
"LightWeightAccessPoint": "access points",
|
|
34
|
+
"WirelessRouter": "home/wireless routers",
|
|
35
|
+
"WirelessRouterNewGeneration": "home/wireless routers",
|
|
36
|
+
"HomeGateway": "home/wireless routers",
|
|
37
|
+
"WirelessLanController": "access points",
|
|
38
|
+
"Power Distribution Device": "pt-specific edge/utility devices",
|
|
39
|
+
"Cloud": "wan/cloud/dsl/cable devices",
|
|
40
|
+
"Cable Modem": "wan/cloud/dsl/cable devices",
|
|
41
|
+
"Dsl Modem": "wan/cloud/dsl/cable devices",
|
|
42
|
+
"Security Appliance": "security devices",
|
|
43
|
+
"ASA": "security devices",
|
|
44
|
+
"NetworkController": "network controllers",
|
|
45
|
+
"CentralOfficeServer": "wan/cloud/dsl/cable devices",
|
|
46
|
+
"CellTower": "wan/cloud/dsl/cable devices",
|
|
47
|
+
"IoT": "iot devices",
|
|
48
|
+
"Board": "iot devices",
|
|
49
|
+
"Sensor": "iot devices",
|
|
50
|
+
"Actuator": "iot devices",
|
|
51
|
+
"MCUComponent": "iot devices",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
DEFAULT_FAMILY_LIMITATIONS = {
|
|
55
|
+
"access points": ["wireless client association requires donor-backed acceptance"],
|
|
56
|
+
"home/wireless routers": ["wireless client association and local NAT flows are acceptance-gated"],
|
|
57
|
+
"iot devices": ["iot registration/control is not acceptance-verified yet"],
|
|
58
|
+
"wan/cloud/dsl/cable devices": ["advanced wan/cloud patterns depend on donor availability"],
|
|
59
|
+
"security devices": ["advanced security devices remain donor-limited"],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
REPORT_ONLY_CAPABILITIES = {
|
|
63
|
+
"ipv6_slaac",
|
|
64
|
+
"dhcpv6_stateful",
|
|
65
|
+
"dhcpv6_stateless",
|
|
66
|
+
"ipv6_prefix_delegation",
|
|
67
|
+
"ipv6_dns_aaaa",
|
|
68
|
+
"ipv6_tunneling",
|
|
69
|
+
"isatap",
|
|
70
|
+
"ospfv3",
|
|
71
|
+
"eigrp_ipv6",
|
|
72
|
+
"ripng",
|
|
73
|
+
"hsrp",
|
|
74
|
+
"dhcp_snooping",
|
|
75
|
+
"dai",
|
|
76
|
+
"dot1x",
|
|
77
|
+
"lldp",
|
|
78
|
+
"rep",
|
|
79
|
+
"snmp",
|
|
80
|
+
"netflow",
|
|
81
|
+
"span",
|
|
82
|
+
"qos",
|
|
83
|
+
"port_security",
|
|
84
|
+
"bgp",
|
|
85
|
+
"stp",
|
|
86
|
+
"rstp",
|
|
87
|
+
"etherchannel",
|
|
88
|
+
"lacp",
|
|
89
|
+
"pagp",
|
|
90
|
+
"vtp",
|
|
91
|
+
"dtp",
|
|
92
|
+
"ospfv2",
|
|
93
|
+
"eigrp_ipv4",
|
|
94
|
+
"ripv2",
|
|
95
|
+
"static_route",
|
|
96
|
+
"default_route",
|
|
97
|
+
"dhcp_relay",
|
|
98
|
+
"nat_static",
|
|
99
|
+
"nat_dynamic",
|
|
100
|
+
"pat",
|
|
101
|
+
"ssh_ios",
|
|
102
|
+
"ntp_ios",
|
|
103
|
+
"syslog_ios",
|
|
104
|
+
"asa_acl_nat",
|
|
105
|
+
"asa_service_policy",
|
|
106
|
+
"clientless_vpn",
|
|
107
|
+
"cbac",
|
|
108
|
+
"zfw",
|
|
109
|
+
"sniffer",
|
|
110
|
+
"wlc",
|
|
111
|
+
"wpa_enterprise",
|
|
112
|
+
"wep",
|
|
113
|
+
"guest_wifi",
|
|
114
|
+
"beamforming",
|
|
115
|
+
"meraki",
|
|
116
|
+
"cellular_5g",
|
|
117
|
+
"bluetooth",
|
|
118
|
+
"network_controller",
|
|
119
|
+
"python_programming",
|
|
120
|
+
"javascript_programming",
|
|
121
|
+
"blockly_programming",
|
|
122
|
+
"tcp_udp_app",
|
|
123
|
+
"vm_iox",
|
|
124
|
+
"voip",
|
|
125
|
+
"ip_phone",
|
|
126
|
+
"call_manager",
|
|
127
|
+
"linksys_voice",
|
|
128
|
+
"mqtt",
|
|
129
|
+
"real_http",
|
|
130
|
+
"real_websocket",
|
|
131
|
+
"visual_scripting",
|
|
132
|
+
"ptp",
|
|
133
|
+
"profinet",
|
|
134
|
+
"l2nat",
|
|
135
|
+
"cyberobserver",
|
|
136
|
+
"industrial_firewall",
|
|
137
|
+
"coaxial",
|
|
138
|
+
"cable_dsl",
|
|
139
|
+
"central_office",
|
|
140
|
+
"cell_tower",
|
|
141
|
+
"power_distribution",
|
|
142
|
+
"hot_swappable",
|
|
143
|
+
"ios_license",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
IPV6_DONOR_BACKED_CAPABILITIES = {
|
|
147
|
+
"ipv6_slaac",
|
|
148
|
+
"dhcpv6_stateful",
|
|
149
|
+
"ospfv3",
|
|
150
|
+
"eigrp_ipv6",
|
|
151
|
+
"ripng",
|
|
152
|
+
"hsrp",
|
|
153
|
+
}
|
|
154
|
+
IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
|
|
155
|
+
"ospfv3",
|
|
156
|
+
"eigrp_ipv6",
|
|
157
|
+
"ripng",
|
|
158
|
+
"hsrp",
|
|
159
|
+
}
|
|
160
|
+
L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES = {
|
|
161
|
+
"dhcp_snooping",
|
|
162
|
+
"dai",
|
|
163
|
+
"dot1x",
|
|
164
|
+
"lldp",
|
|
165
|
+
"rep",
|
|
166
|
+
"snmp",
|
|
167
|
+
"netflow",
|
|
168
|
+
"span",
|
|
169
|
+
"qos",
|
|
170
|
+
"port_security",
|
|
171
|
+
}
|
|
172
|
+
WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES = {
|
|
173
|
+
"wep",
|
|
174
|
+
"wpa_enterprise",
|
|
175
|
+
}
|
|
176
|
+
PROGRAMMING_EDIT_PROVEN_CAPABILITIES = {
|
|
177
|
+
"real_http",
|
|
178
|
+
"real_websocket",
|
|
179
|
+
"python_programming",
|
|
180
|
+
"javascript_programming",
|
|
181
|
+
"tcp_udp_app",
|
|
182
|
+
}
|
|
183
|
+
AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES = {
|
|
184
|
+
"python_programming",
|
|
185
|
+
"javascript_programming",
|
|
186
|
+
"tcp_udp_app",
|
|
187
|
+
}
|
|
188
|
+
PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES = {
|
|
189
|
+
"real_http",
|
|
190
|
+
"real_websocket",
|
|
191
|
+
"python_programming",
|
|
192
|
+
"javascript_programming",
|
|
193
|
+
"tcp_udp_app",
|
|
194
|
+
}
|
|
195
|
+
L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES = {
|
|
196
|
+
"dot1x",
|
|
197
|
+
}
|
|
198
|
+
WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
|
|
199
|
+
"zfw",
|
|
200
|
+
}
|
|
201
|
+
VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES = {
|
|
202
|
+
"voip",
|
|
203
|
+
"ip_phone",
|
|
204
|
+
"call_manager",
|
|
205
|
+
}
|
|
206
|
+
VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES = {
|
|
207
|
+
"voip",
|
|
208
|
+
"ip_phone",
|
|
209
|
+
"call_manager",
|
|
210
|
+
}
|
|
211
|
+
WAN_SECURITY_EDIT_PROVEN_CAPABILITIES = {
|
|
212
|
+
"gre",
|
|
213
|
+
"ppp",
|
|
214
|
+
"ipsec",
|
|
215
|
+
"vpn",
|
|
216
|
+
"cbac",
|
|
217
|
+
"zfw",
|
|
218
|
+
}
|
|
219
|
+
L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES = {
|
|
220
|
+
"bgp",
|
|
221
|
+
"stp",
|
|
222
|
+
"rstp",
|
|
223
|
+
"etherchannel",
|
|
224
|
+
"lacp",
|
|
225
|
+
"pagp",
|
|
226
|
+
"vtp",
|
|
227
|
+
"dtp",
|
|
228
|
+
}
|
|
229
|
+
IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES = {
|
|
230
|
+
"ospfv2",
|
|
231
|
+
"eigrp_ipv4",
|
|
232
|
+
"ripv2",
|
|
233
|
+
"static_route",
|
|
234
|
+
"default_route",
|
|
235
|
+
"dhcp_relay",
|
|
236
|
+
"nat_static",
|
|
237
|
+
"nat_dynamic",
|
|
238
|
+
"pat",
|
|
239
|
+
"ssh_ios",
|
|
240
|
+
"ntp_ios",
|
|
241
|
+
"syslog_ios",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
WAN_SECURITY_DONOR_LIMITED_CAPABILITIES = {
|
|
245
|
+
"gre",
|
|
246
|
+
"ppp",
|
|
247
|
+
"ipsec",
|
|
248
|
+
"vpn",
|
|
249
|
+
"security_edge",
|
|
250
|
+
"multilayer_switching",
|
|
251
|
+
}
|
|
252
|
+
ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES = IPV6_DONOR_BACKED_CAPABILITIES | (
|
|
253
|
+
L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES - {"dot1x", "qos"}
|
|
254
|
+
)
|
|
255
|
+
EDIT_PROVEN_REPORT_CAPABILITIES = ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES
|
|
256
|
+
|
|
257
|
+
CAPABILITY_PROVIDER_FAMILIES = {
|
|
258
|
+
"access_port": {"switches", "multilayer switches"},
|
|
259
|
+
"trunk": {"switches", "multilayer switches"},
|
|
260
|
+
"native_vlan": {"switches", "multilayer switches"},
|
|
261
|
+
"vlan": {"switches", "multilayer switches", "routers"},
|
|
262
|
+
"switching": {"switches", "multilayer switches"},
|
|
263
|
+
"router_on_a_stick": {"routers", "multilayer switches"},
|
|
264
|
+
"management": {"switches", "multilayer switches", "routers"},
|
|
265
|
+
"management_vlan": {"switches", "multilayer switches"},
|
|
266
|
+
"telnet": {"switches", "multilayer switches", "routers"},
|
|
267
|
+
"ssh": {"switches", "multilayer switches", "routers"},
|
|
268
|
+
"nat": {"routers", "home/wireless routers", "security devices"},
|
|
269
|
+
"pat": {"routers", "home/wireless routers", "security devices"},
|
|
270
|
+
"acl": {"routers", "multilayer switches", "switches", "security devices"},
|
|
271
|
+
"vpn": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
|
|
272
|
+
"ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
|
|
273
|
+
"gre": {"routers", "wan/cloud/dsl/cable devices"},
|
|
274
|
+
"ppp": {"routers", "wan/cloud/dsl/cable devices"},
|
|
275
|
+
"bgp": {"routers", "multilayer switches"},
|
|
276
|
+
"stp": {"switches", "multilayer switches"},
|
|
277
|
+
"rstp": {"switches", "multilayer switches"},
|
|
278
|
+
"etherchannel": {"switches", "multilayer switches"},
|
|
279
|
+
"lacp": {"switches", "multilayer switches"},
|
|
280
|
+
"pagp": {"switches", "multilayer switches"},
|
|
281
|
+
"vtp": {"switches", "multilayer switches"},
|
|
282
|
+
"dtp": {"switches", "multilayer switches"},
|
|
283
|
+
"ospfv2": {"routers", "multilayer switches"},
|
|
284
|
+
"eigrp_ipv4": {"routers", "multilayer switches"},
|
|
285
|
+
"ripv2": {"routers", "multilayer switches"},
|
|
286
|
+
"static_route": {"routers", "multilayer switches", "security devices"},
|
|
287
|
+
"default_route": {"routers", "multilayer switches", "security devices"},
|
|
288
|
+
"dhcp_relay": {"routers", "multilayer switches"},
|
|
289
|
+
"nat_static": {"routers", "security devices"},
|
|
290
|
+
"nat_dynamic": {"routers", "security devices"},
|
|
291
|
+
"ssh_ios": {"routers", "switches", "multilayer switches"},
|
|
292
|
+
"ntp_ios": {"routers", "switches", "multilayer switches"},
|
|
293
|
+
"syslog_ios": {"routers", "switches", "multilayer switches"},
|
|
294
|
+
"multilayer_switching": {"multilayer switches"},
|
|
295
|
+
"wan": {"wan/cloud/dsl/cable devices", "routers"},
|
|
296
|
+
"security_edge": {"security devices", "routers"},
|
|
297
|
+
"wireless": {"access points", "home/wireless routers", "end devices"},
|
|
298
|
+
"wireless_ap": {"access points", "home/wireless routers"},
|
|
299
|
+
"wireless_mutation": {"access points", "home/wireless routers"},
|
|
300
|
+
"wireless_client": {"end devices", "access points", "home/wireless routers"},
|
|
301
|
+
"wireless_client_association": {"end devices", "access points", "home/wireless routers"},
|
|
302
|
+
"end_device_mutation": {"end devices"},
|
|
303
|
+
"dhcp": {"routers", "servers", "home/wireless routers"},
|
|
304
|
+
"dhcp_pool": {"routers", "servers", "home/wireless routers"},
|
|
305
|
+
"router_dhcp": {"routers", "home/wireless routers"},
|
|
306
|
+
"server_dhcp": {"servers"},
|
|
307
|
+
"dns": {"servers", "routers"},
|
|
308
|
+
"server_dns": {"servers"},
|
|
309
|
+
"server_http": {"servers"},
|
|
310
|
+
"server_ftp": {"servers"},
|
|
311
|
+
"server_https": {"servers"},
|
|
312
|
+
"server_tftp": {"servers"},
|
|
313
|
+
"server_email": {"servers"},
|
|
314
|
+
"server_syslog": {"servers"},
|
|
315
|
+
"server_aaa": {"servers"},
|
|
316
|
+
"ntp": {"servers", "routers", "switches", "multilayer switches"},
|
|
317
|
+
"iot": {"iot devices"},
|
|
318
|
+
"iot_registration": {"iot devices"},
|
|
319
|
+
"iot_control": {"iot devices"},
|
|
320
|
+
"tablet": {"end devices"},
|
|
321
|
+
"printer": {"end devices"},
|
|
322
|
+
"verification": {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"},
|
|
323
|
+
"ipv6_slaac": {"routers", "switches", "end devices", "pt-specific edge/utility devices"},
|
|
324
|
+
"dhcpv6_stateful": {"routers", "servers", "end devices"},
|
|
325
|
+
"dhcpv6_stateless": {"routers", "servers", "end devices"},
|
|
326
|
+
"ipv6_prefix_delegation": {"routers", "wan/cloud/dsl/cable devices"},
|
|
327
|
+
"ipv6_dns_aaaa": {"servers", "routers"},
|
|
328
|
+
"ipv6_tunneling": {"routers"},
|
|
329
|
+
"isatap": {"routers", "end devices"},
|
|
330
|
+
"ospfv3": {"routers"},
|
|
331
|
+
"eigrp_ipv6": {"routers"},
|
|
332
|
+
"ripng": {"routers"},
|
|
333
|
+
"hsrp": {"routers", "switches", "multilayer switches"},
|
|
334
|
+
"dhcp_snooping": {"switches", "multilayer switches"},
|
|
335
|
+
"dai": {"switches", "multilayer switches"},
|
|
336
|
+
"dot1x": {"switches", "servers", "end devices"},
|
|
337
|
+
"lldp": {"routers", "switches", "multilayer switches"},
|
|
338
|
+
"rep": {"switches", "multilayer switches"},
|
|
339
|
+
"snmp": {"routers", "switches", "multilayer switches", "access points", "servers"},
|
|
340
|
+
"netflow": {"routers"},
|
|
341
|
+
"span": {"switches", "multilayer switches"},
|
|
342
|
+
"qos": {"routers", "switches", "multilayer switches"},
|
|
343
|
+
"port_security": {"switches", "multilayer switches"},
|
|
344
|
+
"asa_acl_nat": {"security devices"},
|
|
345
|
+
"asa_service_policy": {"security devices"},
|
|
346
|
+
"clientless_vpn": {"security devices"},
|
|
347
|
+
"cbac": {"routers", "security devices"},
|
|
348
|
+
"zfw": {"routers", "security devices"},
|
|
349
|
+
"sniffer": {"end devices", "pt-specific edge/utility devices"},
|
|
350
|
+
"wlc": {"access points"},
|
|
351
|
+
"wpa_enterprise": {"access points", "home/wireless routers", "servers"},
|
|
352
|
+
"wep": {"access points", "home/wireless routers"},
|
|
353
|
+
"guest_wifi": {"home/wireless routers", "access points"},
|
|
354
|
+
"beamforming": {"home/wireless routers"},
|
|
355
|
+
"meraki": {"security devices", "home/wireless routers", "access points"},
|
|
356
|
+
"cellular_5g": {"wan/cloud/dsl/cable devices", "end devices"},
|
|
357
|
+
"bluetooth": {"iot devices", "end devices"},
|
|
358
|
+
"network_controller": {"pt-specific edge/utility devices", "routers", "switches"},
|
|
359
|
+
"python_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
360
|
+
"javascript_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
361
|
+
"blockly_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
362
|
+
"tcp_udp_app": {"end devices", "servers"},
|
|
363
|
+
"vm_iox": {"routers", "pt-specific edge/utility devices"},
|
|
364
|
+
"voip": {"end devices", "switches", "routers"},
|
|
365
|
+
"ip_phone": {"end devices"},
|
|
366
|
+
"call_manager": {"routers", "servers", "end devices", "switches"},
|
|
367
|
+
"linksys_voice": {"home/wireless routers", "end devices"},
|
|
368
|
+
"mqtt": {"iot devices", "servers", "home/wireless routers", "pt-specific edge/utility devices"},
|
|
369
|
+
"real_http": {"iot devices", "servers", "pt-specific edge/utility devices"},
|
|
370
|
+
"real_websocket": {"iot devices", "servers", "pt-specific edge/utility devices"},
|
|
371
|
+
"visual_scripting": {"iot devices"},
|
|
372
|
+
"ptp": {"switches", "multilayer switches", "routers"},
|
|
373
|
+
"profinet": {"iot devices", "routers", "switches"},
|
|
374
|
+
"l2nat": {"switches", "multilayer switches"},
|
|
375
|
+
"cyberobserver": {"servers", "switches", "routers"},
|
|
376
|
+
"industrial_firewall": {"security devices", "routers"},
|
|
377
|
+
"coaxial": {"pt-specific edge/utility devices"},
|
|
378
|
+
"cable_dsl": {"wan/cloud/dsl/cable devices"},
|
|
379
|
+
"central_office": {"pt-specific edge/utility devices"},
|
|
380
|
+
"cell_tower": {"pt-specific edge/utility devices", "wan/cloud/dsl/cable devices"},
|
|
381
|
+
"power_distribution": {"pt-specific edge/utility devices"},
|
|
382
|
+
"hot_swappable": {"multilayer switches", "pt-specific edge/utility devices"},
|
|
383
|
+
"ios_license": {"routers", "switches", "multilayer switches"},
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass
|
|
388
|
+
class CapabilityMatrixEntry:
|
|
389
|
+
device_family: str
|
|
390
|
+
capability: str
|
|
391
|
+
maturity_level: str
|
|
392
|
+
accepted_donors: list[str]
|
|
393
|
+
known_limitations: list[str]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@dataclass
|
|
397
|
+
class CoverageGapReport:
|
|
398
|
+
requested_capabilities: list[str]
|
|
399
|
+
supported_capabilities: list[str]
|
|
400
|
+
unsupported_capabilities: list[str]
|
|
401
|
+
requested_device_families: list[str]
|
|
402
|
+
unsupported_device_families: list[str]
|
|
403
|
+
requires_curated_donor: list[str]
|
|
404
|
+
requires_manual_acceptance: list[str]
|
|
405
|
+
capability_statuses: list[dict[str, object]]
|
|
406
|
+
capability_parity: list[dict[str, object]]
|
|
407
|
+
scenario_family: str | None = None
|
|
408
|
+
scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
|
|
409
|
+
recommended_next_actions: list[str] = field(default_factory=list)
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def critical_capability_parity(self) -> list[dict[str, object]]:
|
|
413
|
+
critical = set(self.scenario_generate_readiness.get("critical_capabilities") or [])
|
|
414
|
+
if not critical:
|
|
415
|
+
return []
|
|
416
|
+
return [entry for entry in self.capability_parity if entry.get("capability") in critical]
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def critical_parity_supported_count(self) -> int:
|
|
420
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("inventory_supported") or entry.get("edit_supported"))
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def critical_parity_generate_ready_count(self) -> int:
|
|
424
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("generate_supported"))
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def critical_parity_acceptance_verified_count(self) -> int:
|
|
428
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("acceptance_verified"))
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def critical_parity_donor_backed_ready_count(self) -> int:
|
|
432
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("donor_backed_ready"))
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def critical_parity_mismatch_count(self) -> int:
|
|
436
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("generate_mismatch_reason"))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@dataclass
|
|
440
|
+
class BlueprintPlan:
|
|
441
|
+
requested_devices: list[dict[str, object]]
|
|
442
|
+
requested_links: list[dict[str, object]]
|
|
443
|
+
required_capabilities: list[str]
|
|
444
|
+
required_donor_capacity: dict[str, int]
|
|
445
|
+
required_link_pairs: list[str]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@dataclass
|
|
449
|
+
class DonorGraphFit:
|
|
450
|
+
matched_pairs: list[str]
|
|
451
|
+
missing_pairs: list[str]
|
|
452
|
+
port_media_conflicts: list[str]
|
|
453
|
+
fit_score: int
|
|
454
|
+
layout_reuse_score: int
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _device_family_for_type(device_type: str) -> str:
|
|
458
|
+
return DEVICE_FAMILY_MAP.get(device_type, "pt-specific edge/utility devices")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _sample_maturity(sample: SampleDescriptor) -> str:
|
|
462
|
+
if sample.origin == "cisco-local" and sample.prototype_eligible and sample.version.startswith("9."):
|
|
463
|
+
structural = "acceptance-verified"
|
|
464
|
+
elif sample.origin in {"compat-donor", "external-curated"} and sample.donor_eligible and sample.prototype_eligible:
|
|
465
|
+
structural = "safe-open-generate-supported"
|
|
466
|
+
elif sample.capability_tags:
|
|
467
|
+
structural = "config-mutation-supported"
|
|
468
|
+
elif sample.devices:
|
|
469
|
+
structural = "inventory-supported"
|
|
470
|
+
else:
|
|
471
|
+
structural = "reference-known"
|
|
472
|
+
declared = sample.apply_safety_level if sample.apply_safety_level in MATURITY_RANK else "reference-known"
|
|
473
|
+
if MATURITY_RANK[declared] > MATURITY_RANK[structural]:
|
|
474
|
+
return declared
|
|
475
|
+
return structural
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _merge_limitations(existing: list[str], new_items: list[str]) -> list[str]:
|
|
479
|
+
merged = list(existing)
|
|
480
|
+
for item in new_items:
|
|
481
|
+
if item and item not in merged:
|
|
482
|
+
merged.append(item)
|
|
483
|
+
return merged
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _expanded_sample_capabilities(sample: SampleDescriptor, device_families: list[str]) -> list[str]:
|
|
487
|
+
capabilities = set(sample.capability_tags)
|
|
488
|
+
service_capability_map = {
|
|
489
|
+
"dhcp": {"dhcp", "server_dhcp"},
|
|
490
|
+
"dns": {"dns", "server_dns"},
|
|
491
|
+
"http": {"server_http"},
|
|
492
|
+
"https": {"server_https"},
|
|
493
|
+
"ftp": {"server_ftp"},
|
|
494
|
+
"tftp": {"server_tftp"},
|
|
495
|
+
"email": {"server_email"},
|
|
496
|
+
"syslog": {"server_syslog"},
|
|
497
|
+
"aaa": {"server_aaa"},
|
|
498
|
+
"ntp": {"ntp"},
|
|
499
|
+
}
|
|
500
|
+
for service in sample.service_support:
|
|
501
|
+
capabilities.add(service)
|
|
502
|
+
capabilities.update(service_capability_map.get(service, set()))
|
|
503
|
+
families = set(device_families)
|
|
504
|
+
topology_tags = set(sample.topology_tags)
|
|
505
|
+
normalized_counts = sample.normalized_device_counts()
|
|
506
|
+
iot_roles = set(sample.iot_roles)
|
|
507
|
+
|
|
508
|
+
if "vlan" in capabilities and families & {"switches", "multilayer switches"}:
|
|
509
|
+
capabilities.update({"trunk", "access_port"})
|
|
510
|
+
if (
|
|
511
|
+
"router_on_a_stick" in capabilities
|
|
512
|
+
or "router_on_a_stick" in topology_tags
|
|
513
|
+
or ("vlan" in capabilities and {"routers", "switches"} <= families)
|
|
514
|
+
):
|
|
515
|
+
capabilities.add("router_on_a_stick")
|
|
516
|
+
if "management_vlan" in capabilities:
|
|
517
|
+
capabilities.add("telnet")
|
|
518
|
+
if "telnet" in capabilities and families & {"switches", "multilayer switches"}:
|
|
519
|
+
capabilities.add("management_vlan")
|
|
520
|
+
if normalized_counts.get("Printer", 0) > 0 or ("end devices" in families and "host_server" in capabilities):
|
|
521
|
+
capabilities.add("printer")
|
|
522
|
+
if "end devices" in families:
|
|
523
|
+
capabilities.add("end_device_mutation")
|
|
524
|
+
if sample.wireless_mode_tags and {"access points", "home/wireless routers"} & families and "wireless_ap" in capabilities:
|
|
525
|
+
capabilities.add("wireless_mutation")
|
|
526
|
+
capabilities.add("wireless_client")
|
|
527
|
+
capabilities.add("wireless_client_association")
|
|
528
|
+
if iot_roles:
|
|
529
|
+
capabilities.add("iot")
|
|
530
|
+
if "thing" in iot_roles and ("server" in iot_roles or "gateway" in iot_roles):
|
|
531
|
+
capabilities.add("iot_registration")
|
|
532
|
+
if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
|
|
533
|
+
capabilities.add("iot_control")
|
|
534
|
+
if sample.link_count > 0 and families & {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"}:
|
|
535
|
+
capabilities.add("verification")
|
|
536
|
+
if "multilayer switches" in families:
|
|
537
|
+
capabilities.add("multilayer_switching")
|
|
538
|
+
if "wan/cloud/dsl/cable devices" in families:
|
|
539
|
+
capabilities.add("wan")
|
|
540
|
+
if "security devices" in families:
|
|
541
|
+
capabilities.add("security_edge")
|
|
542
|
+
if "vpn" in capabilities:
|
|
543
|
+
capabilities.update({"ipsec", "gre"})
|
|
544
|
+
return sorted(capabilities)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def build_capability_matrix(samples: list[SampleDescriptor]) -> list[CapabilityMatrixEntry]:
|
|
548
|
+
entries: dict[tuple[str, str], CapabilityMatrixEntry] = {}
|
|
549
|
+
for sample in samples:
|
|
550
|
+
maturity = _sample_maturity(sample)
|
|
551
|
+
device_families = sorted({_device_family_for_type(device.get("type", "")) for device in sample.devices if device.get("type")})
|
|
552
|
+
capabilities = _expanded_sample_capabilities(sample, device_families)
|
|
553
|
+
limitations: list[str] = []
|
|
554
|
+
if not sample.donor_eligible:
|
|
555
|
+
limitations.append("not donor-eligible")
|
|
556
|
+
if not sample.prototype_eligible:
|
|
557
|
+
limitations.append("reference-only")
|
|
558
|
+
for family in device_families or ["pt-specific edge/utility devices"]:
|
|
559
|
+
known_family_limitations = DEFAULT_FAMILY_LIMITATIONS.get(family, [])
|
|
560
|
+
for capability in capabilities or ["inventory"]:
|
|
561
|
+
key = (family, capability)
|
|
562
|
+
donor_label = sample.relative_path
|
|
563
|
+
if key not in entries:
|
|
564
|
+
entries[key] = CapabilityMatrixEntry(
|
|
565
|
+
device_family=family,
|
|
566
|
+
capability=capability,
|
|
567
|
+
maturity_level=maturity,
|
|
568
|
+
accepted_donors=[donor_label],
|
|
569
|
+
known_limitations=_merge_limitations([], [*known_family_limitations, *limitations]),
|
|
570
|
+
)
|
|
571
|
+
continue
|
|
572
|
+
current = entries[key]
|
|
573
|
+
if MATURITY_RANK[maturity] > MATURITY_RANK[current.maturity_level]:
|
|
574
|
+
current.maturity_level = maturity
|
|
575
|
+
if donor_label not in current.accepted_donors:
|
|
576
|
+
current.accepted_donors.append(donor_label)
|
|
577
|
+
current.known_limitations = _merge_limitations(current.known_limitations, [*known_family_limitations, *limitations])
|
|
578
|
+
return sorted(entries.values(), key=lambda item: (item.device_family, item.capability))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _requested_device_families(plan: IntentPlan) -> list[str]:
|
|
582
|
+
families = {_device_family_for_type(device_type) for device_type, count in plan.device_requirements.items() if count}
|
|
583
|
+
return sorted(families)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -> str | None:
|
|
587
|
+
capabilities = set(plan.capabilities)
|
|
588
|
+
requested_services = {str(service) for service in plan.service_requirements.get("services", []) if service}
|
|
589
|
+
requested_family_set = set(requested_families)
|
|
590
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
591
|
+
campus_capabilities = {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "nat", "pat", "trunk", "access_port"}
|
|
592
|
+
service_capabilities = {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
|
|
593
|
+
explicit_campus_style = plan.network_style in {"campus", "department_lan"}
|
|
594
|
+
explicit_service_style = plan.network_style == "service_heavy"
|
|
595
|
+
explicit_home_style = plan.network_style in {"home_iot", "smart_home"}
|
|
596
|
+
explicit_home_gateway_prompt = any(
|
|
597
|
+
token in prompt_lower
|
|
598
|
+
for token in ["home gateway", "homegateway", "smart home", "iot "]
|
|
599
|
+
)
|
|
600
|
+
campus_signal = (
|
|
601
|
+
explicit_campus_style
|
|
602
|
+
or bool(plan.department_groups)
|
|
603
|
+
or "campus" in prompt_lower
|
|
604
|
+
or "kampus" in prompt_lower
|
|
605
|
+
or (requested_family_set & {"switches", "multilayer switches"} and capabilities & campus_capabilities)
|
|
606
|
+
or len(capabilities & campus_capabilities) >= 3
|
|
607
|
+
)
|
|
608
|
+
home_iot_signal = (
|
|
609
|
+
explicit_home_style
|
|
610
|
+
or explicit_home_gateway_prompt
|
|
611
|
+
or plan.wireless_mode == "home_router_edge"
|
|
612
|
+
or capabilities & {"iot", "iot_registration", "iot_control"}
|
|
613
|
+
or {"iot devices", "home/wireless routers"} & requested_family_set
|
|
614
|
+
)
|
|
615
|
+
wan_signal = (
|
|
616
|
+
plan.network_style == "wan_security"
|
|
617
|
+
or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge", "cbac", "zfw"}
|
|
618
|
+
or {"wan/cloud/dsl/cable devices", "security devices"} & requested_family_set
|
|
619
|
+
)
|
|
620
|
+
ipv6_routing_signal = plan.network_style == "ipv6_routing" or capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}
|
|
621
|
+
ipv4_routing_management_signal = plan.network_style == "ipv4_routing_management" or capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}
|
|
622
|
+
l2_resiliency_routing_signal = plan.network_style == "l2_resiliency_routing" or capabilities & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}
|
|
623
|
+
l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}
|
|
624
|
+
wireless_advanced_signal = plan.network_style == "wireless_advanced" or capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}
|
|
625
|
+
automation_controller_signal = plan.network_style == "automation_controller" or capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"}
|
|
626
|
+
voice_collaboration_signal = plan.network_style == "voice_collaboration" or capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"}
|
|
627
|
+
industrial_iot_signal = plan.network_style == "industrial_iot" or capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}
|
|
628
|
+
physical_media_device_signal = plan.network_style == "physical_media_device" or capabilities & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}
|
|
629
|
+
service_signal = (
|
|
630
|
+
explicit_service_style
|
|
631
|
+
or requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
|
|
632
|
+
or capabilities & service_capabilities
|
|
633
|
+
or "servers" in requested_family_set
|
|
634
|
+
)
|
|
635
|
+
if industrial_iot_signal:
|
|
636
|
+
return "industrial_iot"
|
|
637
|
+
if automation_controller_signal:
|
|
638
|
+
return "automation_controller"
|
|
639
|
+
if voice_collaboration_signal:
|
|
640
|
+
return "voice_collaboration"
|
|
641
|
+
if physical_media_device_signal:
|
|
642
|
+
return "physical_media_device"
|
|
643
|
+
if wireless_advanced_signal:
|
|
644
|
+
return "wireless_advanced"
|
|
645
|
+
if wan_signal:
|
|
646
|
+
return "wan_security_edge"
|
|
647
|
+
if l2_resiliency_routing_signal:
|
|
648
|
+
return "l2_resiliency_routing"
|
|
649
|
+
if ipv4_routing_management_signal:
|
|
650
|
+
return "ipv4_routing_management"
|
|
651
|
+
if l2_security_monitoring_signal:
|
|
652
|
+
return "l2_security_monitoring"
|
|
653
|
+
if ipv6_routing_signal:
|
|
654
|
+
return "ipv6_routing"
|
|
655
|
+
if home_iot_signal:
|
|
656
|
+
return "home_iot"
|
|
657
|
+
if campus_signal:
|
|
658
|
+
return "campus"
|
|
659
|
+
if service_signal:
|
|
660
|
+
return "service_heavy"
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
SCENARIO_CAPABILITY_SETS = {
|
|
665
|
+
"campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
|
|
666
|
+
"service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
|
|
667
|
+
"home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
|
|
668
|
+
"wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat", "cbac", "zfw"},
|
|
669
|
+
"ipv6_routing": {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"},
|
|
670
|
+
"l2_resiliency_routing": {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"},
|
|
671
|
+
"ipv4_routing_management": {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"},
|
|
672
|
+
"l2_security_monitoring": {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"},
|
|
673
|
+
"wireless_advanced": {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"},
|
|
674
|
+
"automation_controller": {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"},
|
|
675
|
+
"voice_collaboration": {"voip", "ip_phone", "call_manager", "linksys_voice"},
|
|
676
|
+
"industrial_iot": {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"},
|
|
677
|
+
"physical_media_device": {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"},
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
CAPABILITY_REQUIRED_ARCHETYPES = {
|
|
681
|
+
"router_on_a_stick": ["campus/core"],
|
|
682
|
+
"management_vlan": ["campus/core"],
|
|
683
|
+
"telnet": ["campus/core"],
|
|
684
|
+
"server_dns": ["service-heavy"],
|
|
685
|
+
"server_dhcp": ["service-heavy"],
|
|
686
|
+
"server_http": ["service-heavy"],
|
|
687
|
+
"server_https": ["service-heavy"],
|
|
688
|
+
"server_ftp": ["service-heavy"],
|
|
689
|
+
"server_tftp": ["service-heavy"],
|
|
690
|
+
"server_email": ["service-heavy"],
|
|
691
|
+
"server_syslog": ["service-heavy"],
|
|
692
|
+
"server_aaa": ["service-heavy"],
|
|
693
|
+
"iot_registration": ["IoT/home gateway"],
|
|
694
|
+
"iot_control": ["IoT/home gateway"],
|
|
695
|
+
"wireless_mutation": ["wireless-heavy"],
|
|
696
|
+
"wireless_client_association": ["wireless-heavy", "IoT/home gateway"],
|
|
697
|
+
"vpn": ["WAN/security edge"],
|
|
698
|
+
"ipsec": ["WAN/security edge"],
|
|
699
|
+
"gre": ["WAN/security edge"],
|
|
700
|
+
"ppp": ["WAN/security edge"],
|
|
701
|
+
"multilayer_switching": ["WAN/security edge", "campus/core"],
|
|
702
|
+
"security_edge": ["WAN/security edge"],
|
|
703
|
+
"cbac": ["WAN/security edge"],
|
|
704
|
+
"zfw": ["WAN/security edge"],
|
|
705
|
+
"ipv6_slaac": ["IPv6/routing"],
|
|
706
|
+
"dhcpv6_stateful": ["IPv6/routing"],
|
|
707
|
+
"dhcpv6_stateless": ["IPv6/routing"],
|
|
708
|
+
"ipv6_prefix_delegation": ["IPv6/routing"],
|
|
709
|
+
"ipv6_dns_aaaa": ["IPv6/routing"],
|
|
710
|
+
"ipv6_tunneling": ["IPv6/routing"],
|
|
711
|
+
"isatap": ["IPv6/routing"],
|
|
712
|
+
"ospfv3": ["IPv6/routing"],
|
|
713
|
+
"eigrp_ipv6": ["IPv6/routing"],
|
|
714
|
+
"ripng": ["IPv6/routing"],
|
|
715
|
+
"hsrp": ["IPv6/routing"],
|
|
716
|
+
"dhcp_snooping": ["L2 security/monitoring"],
|
|
717
|
+
"dai": ["L2 security/monitoring"],
|
|
718
|
+
"dot1x": ["L2 security/monitoring"],
|
|
719
|
+
"lldp": ["L2 security/monitoring"],
|
|
720
|
+
"rep": ["L2 security/monitoring"],
|
|
721
|
+
"snmp": ["L2 security/monitoring"],
|
|
722
|
+
"netflow": ["L2 security/monitoring"],
|
|
723
|
+
"span": ["L2 security/monitoring"],
|
|
724
|
+
"qos": ["L2 security/monitoring"],
|
|
725
|
+
"port_security": ["L2 security/monitoring"],
|
|
726
|
+
"bgp": ["L2 resiliency/routing"],
|
|
727
|
+
"stp": ["L2 resiliency/routing"],
|
|
728
|
+
"rstp": ["L2 resiliency/routing"],
|
|
729
|
+
"etherchannel": ["L2 resiliency/routing"],
|
|
730
|
+
"lacp": ["L2 resiliency/routing"],
|
|
731
|
+
"pagp": ["L2 resiliency/routing"],
|
|
732
|
+
"vtp": ["L2 resiliency/routing"],
|
|
733
|
+
"dtp": ["L2 resiliency/routing"],
|
|
734
|
+
"ospfv2": ["IPv4 routing/management"],
|
|
735
|
+
"eigrp_ipv4": ["IPv4 routing/management"],
|
|
736
|
+
"ripv2": ["IPv4 routing/management"],
|
|
737
|
+
"static_route": ["IPv4 routing/management"],
|
|
738
|
+
"default_route": ["IPv4 routing/management"],
|
|
739
|
+
"dhcp_relay": ["IPv4 routing/management"],
|
|
740
|
+
"nat_static": ["IPv4 routing/management"],
|
|
741
|
+
"nat_dynamic": ["IPv4 routing/management"],
|
|
742
|
+
"pat": ["IPv4 routing/management"],
|
|
743
|
+
"ssh_ios": ["IPv4 routing/management"],
|
|
744
|
+
"ntp_ios": ["IPv4 routing/management"],
|
|
745
|
+
"syslog_ios": ["IPv4 routing/management"],
|
|
746
|
+
"wlc": ["advanced wireless"],
|
|
747
|
+
"wpa_enterprise": ["advanced wireless"],
|
|
748
|
+
"wep": ["advanced wireless"],
|
|
749
|
+
"guest_wifi": ["advanced wireless"],
|
|
750
|
+
"beamforming": ["advanced wireless"],
|
|
751
|
+
"meraki": ["advanced wireless"],
|
|
752
|
+
"cellular_5g": ["advanced wireless"],
|
|
753
|
+
"bluetooth": ["advanced wireless"],
|
|
754
|
+
"network_controller": ["automation/controller"],
|
|
755
|
+
"python_programming": ["automation/controller"],
|
|
756
|
+
"javascript_programming": ["automation/controller"],
|
|
757
|
+
"blockly_programming": ["automation/controller"],
|
|
758
|
+
"tcp_udp_app": ["automation/controller"],
|
|
759
|
+
"vm_iox": ["automation/controller"],
|
|
760
|
+
"voip": ["voice/collaboration"],
|
|
761
|
+
"ip_phone": ["voice/collaboration"],
|
|
762
|
+
"call_manager": ["voice/collaboration"],
|
|
763
|
+
"linksys_voice": ["voice/collaboration"],
|
|
764
|
+
"mqtt": ["industrial IoT"],
|
|
765
|
+
"real_http": ["industrial IoT"],
|
|
766
|
+
"real_websocket": ["industrial IoT"],
|
|
767
|
+
"visual_scripting": ["industrial IoT"],
|
|
768
|
+
"ptp": ["industrial IoT"],
|
|
769
|
+
"profinet": ["industrial IoT"],
|
|
770
|
+
"l2nat": ["industrial IoT"],
|
|
771
|
+
"cyberobserver": ["industrial IoT"],
|
|
772
|
+
"industrial_firewall": ["industrial IoT"],
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
|
|
776
|
+
"server_dns": ["workspace_validated", "server_runtime"],
|
|
777
|
+
"server_dhcp": ["workspace_validated", "server_runtime"],
|
|
778
|
+
"server_http": ["workspace_validated", "server_runtime"],
|
|
779
|
+
"server_https": ["workspace_validated", "server_runtime"],
|
|
780
|
+
"server_ftp": ["workspace_validated", "server_runtime"],
|
|
781
|
+
"server_tftp": ["workspace_validated", "server_runtime"],
|
|
782
|
+
"server_email": ["workspace_validated", "server_runtime"],
|
|
783
|
+
"server_syslog": ["workspace_validated", "server_runtime"],
|
|
784
|
+
"server_aaa": ["workspace_validated", "server_runtime"],
|
|
785
|
+
"wireless_mutation": ["workspace_validated", "wireless_runtime"],
|
|
786
|
+
"wireless_client_association": ["workspace_validated", "wireless_runtime"],
|
|
787
|
+
"iot_registration": ["workspace_validated", "iot_runtime"],
|
|
788
|
+
"iot_control": ["workspace_validated", "iot_runtime"],
|
|
789
|
+
"vpn": ["workspace_validated", "tunnel_runtime"],
|
|
790
|
+
"ipsec": ["workspace_validated", "tunnel_runtime"],
|
|
791
|
+
"gre": ["workspace_validated", "tunnel_runtime"],
|
|
792
|
+
"ppp": ["workspace_validated", "wan_runtime"],
|
|
793
|
+
"security_edge": ["workspace_validated", "security_runtime"],
|
|
794
|
+
"cbac": ["workspace_validated", "security_runtime"],
|
|
795
|
+
"zfw": ["workspace_validated", "security_runtime"],
|
|
796
|
+
"multilayer_switching": ["workspace_validated", "multilayer_runtime"],
|
|
797
|
+
"ipv6_slaac": ["workspace_validated", "ipv6_runtime"],
|
|
798
|
+
"dhcpv6_stateful": ["workspace_validated", "ipv6_runtime"],
|
|
799
|
+
"ospfv3": ["workspace_validated", "ipv6_runtime"],
|
|
800
|
+
"eigrp_ipv6": ["workspace_validated", "ipv6_runtime"],
|
|
801
|
+
"ripng": ["workspace_validated", "ipv6_runtime"],
|
|
802
|
+
"hsrp": ["workspace_validated", "ipv6_runtime"],
|
|
803
|
+
"wep": ["workspace_validated", "wireless_runtime"],
|
|
804
|
+
"wpa_enterprise": ["workspace_validated", "wireless_runtime"],
|
|
805
|
+
"ospfv2": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
806
|
+
"eigrp_ipv4": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
807
|
+
"ripv2": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
808
|
+
"static_route": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
809
|
+
"default_route": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
810
|
+
"dhcp_relay": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
811
|
+
"nat_static": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
812
|
+
"nat_dynamic": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
813
|
+
"pat": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
814
|
+
"ssh_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
815
|
+
"ntp_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
816
|
+
"syslog_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
|
|
821
|
+
if bool(status.get("report_only")):
|
|
822
|
+
return "report_only"
|
|
823
|
+
if not bool(status.get("inventory_supported")):
|
|
824
|
+
return "unsupported"
|
|
825
|
+
if bool(status.get("donor_limited_generate")):
|
|
826
|
+
return "supported_but_donor_limited"
|
|
827
|
+
if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
|
|
828
|
+
return "supported_in_edit_only"
|
|
829
|
+
if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
|
|
830
|
+
return "supported_but_donor_limited"
|
|
831
|
+
if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_manual_acceptance")) and not bool(status.get("acceptance_verified")):
|
|
832
|
+
return "supported_but_acceptance_gated"
|
|
833
|
+
if bool(status.get("safe_open_generate_supported")):
|
|
834
|
+
return None
|
|
835
|
+
return "unsupported"
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _donor_backed_ready(status: dict[str, object]) -> bool:
|
|
839
|
+
return bool(status.get("donor_backed_ready"))
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _selected_sample_for_donor(
|
|
843
|
+
samples: list[SampleDescriptor],
|
|
844
|
+
selected_donor: str | None,
|
|
845
|
+
) -> SampleDescriptor | None:
|
|
846
|
+
if not selected_donor:
|
|
847
|
+
return None
|
|
848
|
+
needle = str(selected_donor).replace("\\", "/").lower()
|
|
849
|
+
for sample in samples:
|
|
850
|
+
candidates = {
|
|
851
|
+
str(sample.path).replace("\\", "/").lower(),
|
|
852
|
+
str(sample.relative_path).replace("\\", "/").lower(),
|
|
853
|
+
str(sample.path).replace("\\", "/").split("/")[-1].lower(),
|
|
854
|
+
str(sample.relative_path).replace("\\", "/").split("/")[-1].lower(),
|
|
855
|
+
}
|
|
856
|
+
if needle in candidates:
|
|
857
|
+
return sample
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _has_explicit_iot_registration(plan: IntentPlan) -> bool:
|
|
862
|
+
return any(
|
|
863
|
+
op.get("op") == "set_iot_registration"
|
|
864
|
+
and str(op.get("device", "")).strip()
|
|
865
|
+
and str(op.get("target", "")).strip()
|
|
866
|
+
for op in plan.iot_ops
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _has_explicit_iot_control(plan: IntentPlan) -> bool:
|
|
871
|
+
return any(
|
|
872
|
+
op.get("op") == "set_iot_rule_state"
|
|
873
|
+
and str(op.get("device", "")).strip()
|
|
874
|
+
and str(op.get("rule_name", "")).strip()
|
|
875
|
+
for op in plan.iot_ops
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _has_explicit_wireless_association(plan: IntentPlan) -> bool:
|
|
880
|
+
return any(
|
|
881
|
+
op.get("op") == "associate_wireless_client"
|
|
882
|
+
and str(op.get("device", "")).strip()
|
|
883
|
+
and str(op.get("ap", "")).strip()
|
|
884
|
+
and str(op.get("ssid", "")).strip()
|
|
885
|
+
for op in plan.wireless_ops
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _has_explicit_advanced_wireless_edit(plan: IntentPlan, capability: str) -> bool:
|
|
890
|
+
for op in plan.wireless_ops:
|
|
891
|
+
if op.get("op") != "set_wireless_ssid":
|
|
892
|
+
continue
|
|
893
|
+
security = str(op.get("security") or "").strip().lower()
|
|
894
|
+
if capability == "wep" and security == "wep" and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
|
|
895
|
+
return True
|
|
896
|
+
if capability == "wpa_enterprise" and security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
|
|
897
|
+
return True
|
|
898
|
+
return False
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _has_explicit_programming_edit(plan: IntentPlan, capability: str) -> bool:
|
|
902
|
+
for op in plan.programming_ops:
|
|
903
|
+
if op.get("op") != "set_script_file_content":
|
|
904
|
+
continue
|
|
905
|
+
if not (
|
|
906
|
+
str(op.get("device") or "").strip()
|
|
907
|
+
and str(op.get("app_name") or "").strip()
|
|
908
|
+
and str(op.get("file_name") or "").strip()
|
|
909
|
+
):
|
|
910
|
+
continue
|
|
911
|
+
file_name = str(op.get("file_name") or "").lower()
|
|
912
|
+
app_name = str(op.get("app_name") or "").lower()
|
|
913
|
+
content = str(op.get("content") or "").lower()
|
|
914
|
+
if capability == "python_programming" and (file_name.endswith(".py") or "python" in app_name):
|
|
915
|
+
return True
|
|
916
|
+
if capability == "javascript_programming" and (file_name.endswith(".js") or "javascript" in app_name):
|
|
917
|
+
return True
|
|
918
|
+
script_text = " ".join([file_name, app_name, content])
|
|
919
|
+
if capability == "tcp_udp_app" and ("tcp" in script_text or "udp" in script_text):
|
|
920
|
+
return True
|
|
921
|
+
if capability == "real_http" and ("realhttp" in content or "real http" in app_name):
|
|
922
|
+
return True
|
|
923
|
+
if capability == "real_websocket" and ("realws" in content or "websocket" in content or "websocket" in app_name):
|
|
924
|
+
return True
|
|
925
|
+
return False
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _has_explicit_l2_security_edit(plan: IntentPlan, capability: str) -> bool:
|
|
929
|
+
expected_ops = {
|
|
930
|
+
"dot1x": {"set_dot1x"},
|
|
931
|
+
"qos": {"set_qos_policy"},
|
|
932
|
+
}.get(capability, set())
|
|
933
|
+
if not expected_ops:
|
|
934
|
+
return False
|
|
935
|
+
for op in plan.switch_ops:
|
|
936
|
+
if str(op.get("op")) not in expected_ops:
|
|
937
|
+
continue
|
|
938
|
+
if not str(op.get("device") or "").strip():
|
|
939
|
+
continue
|
|
940
|
+
if capability == "dot1x":
|
|
941
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
|
|
942
|
+
if capability == "qos":
|
|
943
|
+
return bool(
|
|
944
|
+
str(op.get("interface") or "").strip()
|
|
945
|
+
and str(op.get("class_map") or "").strip()
|
|
946
|
+
and str(op.get("policy_map") or "").strip()
|
|
947
|
+
and str(op.get("direction") or "").strip()
|
|
948
|
+
)
|
|
949
|
+
return False
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _has_explicit_l2_resiliency_edit(plan: IntentPlan, capability: str) -> bool:
|
|
953
|
+
if capability == "bgp":
|
|
954
|
+
return any(
|
|
955
|
+
op.get("op") == "set_bgp_neighbor"
|
|
956
|
+
and str(op.get("device") or "").strip()
|
|
957
|
+
and str(op.get("neighbor") or "").strip()
|
|
958
|
+
and str(op.get("remote_as") or "").strip()
|
|
959
|
+
for op in plan.router_ops
|
|
960
|
+
)
|
|
961
|
+
for op in plan.switch_ops:
|
|
962
|
+
if not str(op.get("device") or "").strip():
|
|
963
|
+
continue
|
|
964
|
+
op_name = str(op.get("op"))
|
|
965
|
+
if capability == "stp" and op_name == "set_stp":
|
|
966
|
+
return bool(str(op.get("mode") or "").strip())
|
|
967
|
+
if capability == "rstp" and op_name == "set_stp":
|
|
968
|
+
return bool(re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE))
|
|
969
|
+
if capability == "etherchannel" and op_name == "set_etherchannel":
|
|
970
|
+
return bool(op.get("interfaces"))
|
|
971
|
+
if capability == "lacp" and op_name == "set_etherchannel":
|
|
972
|
+
if str(op.get("mode") or "").lower() in {"active", "passive"} and bool(op.get("interfaces")):
|
|
973
|
+
return True
|
|
974
|
+
continue
|
|
975
|
+
if capability == "pagp" and op_name == "set_etherchannel":
|
|
976
|
+
if str(op.get("mode") or "").lower() in {"desirable", "auto"} and bool(op.get("interfaces")):
|
|
977
|
+
return True
|
|
978
|
+
continue
|
|
979
|
+
if capability == "vtp" and op_name == "set_vtp":
|
|
980
|
+
return bool(str(op.get("domain") or "").strip() and str(op.get("mode") or "").strip())
|
|
981
|
+
if capability == "dtp" and op_name == "set_dtp":
|
|
982
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
|
|
983
|
+
return False
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _has_explicit_ipv4_routing_management_edit(plan: IntentPlan, capability: str) -> bool:
|
|
987
|
+
operation_map = {
|
|
988
|
+
"ospfv2": {"set_ospfv2_network"},
|
|
989
|
+
"eigrp_ipv4": {"set_eigrp_ipv4_network"},
|
|
990
|
+
"ripv2": {"set_ripv2_network"},
|
|
991
|
+
"static_route": {"set_static_route"},
|
|
992
|
+
"default_route": {"set_static_route"},
|
|
993
|
+
"dhcp_relay": {"set_dhcp_relay"},
|
|
994
|
+
"nat_static": {"set_nat_static"},
|
|
995
|
+
"nat_dynamic": {"set_nat_interface"},
|
|
996
|
+
"pat": {"set_pat_overload"},
|
|
997
|
+
"ssh_ios": {"set_ssh_ios"},
|
|
998
|
+
"ntp_ios": {"set_ntp_server"},
|
|
999
|
+
"syslog_ios": {"set_syslog_server"},
|
|
1000
|
+
}
|
|
1001
|
+
expected_ops = operation_map.get(capability, set())
|
|
1002
|
+
if not expected_ops:
|
|
1003
|
+
return False
|
|
1004
|
+
for op in plan.router_ops:
|
|
1005
|
+
if str(op.get("op")) not in expected_ops:
|
|
1006
|
+
continue
|
|
1007
|
+
if not str(op.get("device") or "").strip():
|
|
1008
|
+
continue
|
|
1009
|
+
if capability == "default_route":
|
|
1010
|
+
return op.get("network") == "0.0.0.0" and op.get("prefix") == 0
|
|
1011
|
+
if capability == "nat_dynamic":
|
|
1012
|
+
return str(op.get("role") or "").lower() in {"inside", "outside"} and str(op.get("interface") or "").strip()
|
|
1013
|
+
return True
|
|
1014
|
+
return False
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _has_explicit_wan_security_edit(plan: IntentPlan, capability: str) -> bool:
|
|
1018
|
+
expected_ops = {
|
|
1019
|
+
"gre": {"set_gre_tunnel"},
|
|
1020
|
+
"ppp": {"set_ppp_interface"},
|
|
1021
|
+
"ipsec": {"set_ipsec_transform_set", "set_crypto_map"},
|
|
1022
|
+
"vpn": {"set_crypto_map"},
|
|
1023
|
+
"cbac": {"set_cbac_inspect"},
|
|
1024
|
+
"zfw": {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"},
|
|
1025
|
+
}.get(capability, set())
|
|
1026
|
+
if not expected_ops:
|
|
1027
|
+
return False
|
|
1028
|
+
for op in plan.router_ops:
|
|
1029
|
+
if str(op.get("op")) not in expected_ops:
|
|
1030
|
+
continue
|
|
1031
|
+
if not str(op.get("device") or "").strip():
|
|
1032
|
+
continue
|
|
1033
|
+
if capability == "gre":
|
|
1034
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
|
|
1035
|
+
if capability == "ppp":
|
|
1036
|
+
return bool(str(op.get("interface") or "").strip())
|
|
1037
|
+
if capability == "ipsec":
|
|
1038
|
+
if op.get("op") == "set_ipsec_transform_set":
|
|
1039
|
+
return bool(str(op.get("name") or "").strip() and str(op.get("encryption") or "").strip() and str(op.get("integrity") or "").strip())
|
|
1040
|
+
return bool(str(op.get("map_name") or "").strip() and str(op.get("transform_set") or "").strip())
|
|
1041
|
+
if capability == "vpn":
|
|
1042
|
+
return bool(str(op.get("map_name") or "").strip() and str(op.get("peer") or "").strip())
|
|
1043
|
+
if capability == "cbac":
|
|
1044
|
+
return bool(str(op.get("name") or "").strip() and str(op.get("protocol") or "").strip() and str(op.get("interface") or "").strip())
|
|
1045
|
+
if capability == "zfw":
|
|
1046
|
+
if op.get("op") == "set_zfw_zone_interface":
|
|
1047
|
+
return bool(str(op.get("zone") or "").strip() and str(op.get("interface") or "").strip())
|
|
1048
|
+
if op.get("op") == "set_zfw_zone_pair":
|
|
1049
|
+
return bool(str(op.get("pair_name") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
|
|
1050
|
+
return bool(str(op.get("class_map") or "").strip() and str(op.get("policy_map") or "").strip())
|
|
1051
|
+
return False
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
|
|
1055
|
+
capabilities = set(plan.capabilities)
|
|
1056
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
1057
|
+
return (
|
|
1058
|
+
plan.network_style == "wan_security"
|
|
1059
|
+
or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"})
|
|
1060
|
+
or any(token in prompt_lower for token in ["site-to-site", "firewall", "asa", "cloud", "multilayer"])
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _has_explicit_voice_edit(plan: IntentPlan, capability: str) -> bool:
|
|
1065
|
+
op_names = {str(op.get("op")) for op in plan.router_ops}
|
|
1066
|
+
if capability == "voip":
|
|
1067
|
+
return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"})
|
|
1068
|
+
if capability == "call_manager":
|
|
1069
|
+
return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone"})
|
|
1070
|
+
if capability == "ip_phone":
|
|
1071
|
+
return bool(op_names & {"set_ephone_dn", "set_ephone"})
|
|
1072
|
+
return False
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _has_explicit_ipv6_operation(plan: IntentPlan, capability: str) -> bool:
|
|
1076
|
+
operation_map = {
|
|
1077
|
+
"ipv6_slaac": {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"},
|
|
1078
|
+
"dhcpv6_stateful": {"set_dhcpv6_pool"},
|
|
1079
|
+
"ospfv3": {"set_ospfv3_interface"},
|
|
1080
|
+
"eigrp_ipv6": {"set_eigrp_ipv6_interface"},
|
|
1081
|
+
"ripng": {"set_ripng_interface"},
|
|
1082
|
+
"hsrp": {"set_hsrp_ipv6"},
|
|
1083
|
+
}
|
|
1084
|
+
expected_ops = operation_map.get(capability, set())
|
|
1085
|
+
return any(str(op.get("op")) in expected_ops for op in plan.router_ops)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _selected_donor_capability_override(
|
|
1089
|
+
plan: IntentPlan,
|
|
1090
|
+
capability: str,
|
|
1091
|
+
selected_sample: SampleDescriptor | None,
|
|
1092
|
+
) -> bool:
|
|
1093
|
+
if selected_sample is None:
|
|
1094
|
+
return False
|
|
1095
|
+
device_families = sorted({_device_family_for_type(device.get("type", "")) for device in selected_sample.devices if device.get("type")})
|
|
1096
|
+
sample_capabilities = set(_expanded_sample_capabilities(selected_sample, device_families))
|
|
1097
|
+
sample_capabilities.update(str(item) for item in selected_sample.validated_edit_capabilities if str(item).strip())
|
|
1098
|
+
archetypes = {str(item) for item in selected_sample.archetype_tags if str(item).strip()}
|
|
1099
|
+
runtime_features = {str(item) for item in selected_sample.runtime_features if str(item).strip()}
|
|
1100
|
+
|
|
1101
|
+
if capability == "iot":
|
|
1102
|
+
return (
|
|
1103
|
+
bool(archetypes & {"IoT/home gateway"})
|
|
1104
|
+
and bool(sample_capabilities & {"iot", "iot_registration", "iot_control"})
|
|
1105
|
+
and bool(runtime_features & {"iot_runtime"})
|
|
1106
|
+
and (_has_explicit_iot_registration(plan) or _has_explicit_iot_control(plan))
|
|
1107
|
+
)
|
|
1108
|
+
if capability == "iot_registration":
|
|
1109
|
+
return (
|
|
1110
|
+
"IoT/home gateway" in archetypes
|
|
1111
|
+
and "iot_registration" in sample_capabilities
|
|
1112
|
+
and "iot_runtime" in runtime_features
|
|
1113
|
+
and _has_explicit_iot_registration(plan)
|
|
1114
|
+
)
|
|
1115
|
+
if capability == "iot_control":
|
|
1116
|
+
return (
|
|
1117
|
+
"IoT/home gateway" in archetypes
|
|
1118
|
+
and "iot_control" in sample_capabilities
|
|
1119
|
+
and "iot_runtime" in runtime_features
|
|
1120
|
+
and _has_explicit_iot_control(plan)
|
|
1121
|
+
)
|
|
1122
|
+
if capability == "wireless_client_association":
|
|
1123
|
+
return (
|
|
1124
|
+
bool(archetypes & {"IoT/home gateway", "wireless-heavy"})
|
|
1125
|
+
and "wireless_client_association" in sample_capabilities
|
|
1126
|
+
and "wireless_runtime" in runtime_features
|
|
1127
|
+
and _has_explicit_wireless_association(plan)
|
|
1128
|
+
)
|
|
1129
|
+
wan_runtime_by_capability = {
|
|
1130
|
+
"vpn": {"tunnel_runtime", "security_runtime", "wan_runtime"},
|
|
1131
|
+
"ipsec": {"tunnel_runtime", "security_runtime"},
|
|
1132
|
+
"gre": {"tunnel_runtime", "wan_runtime"},
|
|
1133
|
+
"ppp": {"wan_runtime"},
|
|
1134
|
+
"security_edge": {"security_runtime"},
|
|
1135
|
+
"multilayer_switching": {"multilayer_runtime"},
|
|
1136
|
+
}
|
|
1137
|
+
if capability in wan_runtime_by_capability:
|
|
1138
|
+
has_acceptance_backed_wan_generate = (
|
|
1139
|
+
bool(selected_sample.acceptance_fixtures)
|
|
1140
|
+
and selected_sample.apply_safety_level in {"acceptance-verified", "safe-open-generate-supported"}
|
|
1141
|
+
)
|
|
1142
|
+
return (
|
|
1143
|
+
"WAN/security edge" in archetypes
|
|
1144
|
+
and capability in sample_capabilities
|
|
1145
|
+
and bool(runtime_features & wan_runtime_by_capability[capability])
|
|
1146
|
+
and has_acceptance_backed_wan_generate
|
|
1147
|
+
and _has_explicit_wan_security_intent(plan)
|
|
1148
|
+
)
|
|
1149
|
+
if capability in IPV6_DONOR_BACKED_CAPABILITIES:
|
|
1150
|
+
return (
|
|
1151
|
+
"IPv6/routing" in archetypes
|
|
1152
|
+
and capability in sample_capabilities
|
|
1153
|
+
and "ipv6_runtime" in runtime_features
|
|
1154
|
+
and _has_explicit_ipv6_operation(plan, capability)
|
|
1155
|
+
)
|
|
1156
|
+
return False
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _best_maturity_level(status: dict[str, object]) -> str:
|
|
1160
|
+
level = str(status.get("best_maturity_level") or "").strip()
|
|
1161
|
+
if level in MATURITY_RANK:
|
|
1162
|
+
return level
|
|
1163
|
+
if bool(status.get("acceptance_verified")):
|
|
1164
|
+
return "acceptance-verified"
|
|
1165
|
+
if bool(status.get("safe_open_generate_supported")):
|
|
1166
|
+
return "safe-open-generate-supported"
|
|
1167
|
+
if bool(status.get("config_mutation_supported")):
|
|
1168
|
+
return "config-mutation-supported"
|
|
1169
|
+
if bool(status.get("edit_supported")):
|
|
1170
|
+
return "edit-supported"
|
|
1171
|
+
if bool(status.get("inventory_supported")):
|
|
1172
|
+
return "inventory-supported"
|
|
1173
|
+
return "reference-known"
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _recommended_next_action_for_capability(capability: str, mismatch_reason: str | None) -> str:
|
|
1177
|
+
if mismatch_reason == "report_only":
|
|
1178
|
+
return f"Keep {capability} in atlas/report mode until donor-backed edit or generate evidence exists."
|
|
1179
|
+
if capability in {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching", "cbac", "zfw"}:
|
|
1180
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1181
|
+
return f"Use explicit WAN/security edit commands for {capability}; strict prompt generate still needs an acceptance-backed WAN/security donor."
|
|
1182
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1183
|
+
return f"Provide a WAN/security edge donor with reusable ASA/cloud/serial or tunnel skeleton for {capability}."
|
|
1184
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1185
|
+
return f"Keep {capability} in report/selection flow until a WAN/security edge donor is acceptance-backed."
|
|
1186
|
+
return f"Prefer a WAN/security edge donor with explicit {capability} evidence before strict generate."
|
|
1187
|
+
if capability in IPV6_DONOR_BACKED_CAPABILITIES:
|
|
1188
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1189
|
+
return f"Use explicit edit commands for {capability}; strict prompt generate still needs a selected IPv6/routing donor."
|
|
1190
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1191
|
+
return f"Provide an IPv6/routing donor with reusable IPv6 interface and routing skeleton for {capability}."
|
|
1192
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1193
|
+
return f"Keep {capability} in report/edit flow until an IPv6/routing donor is acceptance-backed."
|
|
1194
|
+
return f"Prefer an IPv6/routing donor and explicit router/interface targets before strict generate for {capability}."
|
|
1195
|
+
if capability in L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES:
|
|
1196
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1197
|
+
return f"Use explicit edit commands for {capability}; strict prompt generate still needs an acceptance-backed L2 security/monitoring donor."
|
|
1198
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1199
|
+
return f"Provide an L2 security/monitoring donor with reusable switch/router monitoring skeleton for {capability}."
|
|
1200
|
+
return f"Keep {capability} constrained to edit/inventory proof until an L2 security/monitoring donor is acceptance-backed."
|
|
1201
|
+
if capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES:
|
|
1202
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1203
|
+
return f"Use explicit wireless edit commands for {capability}; strict prompt generate still needs an acceptance-backed advanced wireless donor."
|
|
1204
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1205
|
+
return f"Provide an advanced wireless donor with reusable AP/router/WLC skeleton for {capability}."
|
|
1206
|
+
return f"Keep {capability} constrained to explicit wireless edit proof until an advanced wireless donor is acceptance-backed."
|
|
1207
|
+
if capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES:
|
|
1208
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1209
|
+
return f"Use explicit voice IOS edit commands for {capability}; strict prompt generate still needs an acceptance-backed voice/collaboration donor."
|
|
1210
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1211
|
+
return f"Provide a voice/collaboration donor with reusable telephony-service, ephone, or dial-peer skeleton for {capability}."
|
|
1212
|
+
return f"Keep {capability} constrained to voice inventory/edit proof until a voice/collaboration donor is acceptance-backed."
|
|
1213
|
+
if capability in AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES:
|
|
1214
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1215
|
+
return f"Use explicit script-file edit commands for {capability}; strict prompt generate still needs an acceptance-backed automation/controller donor."
|
|
1216
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1217
|
+
return f"Provide an automation/controller donor with reusable existing app/file script surfaces for {capability}."
|
|
1218
|
+
return f"Keep {capability} constrained to automation inventory/edit proof until an automation/controller donor is acceptance-backed."
|
|
1219
|
+
if capability == "iot_registration":
|
|
1220
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1221
|
+
return "Use an IoT/home gateway donor and explicitly name the thing plus gateway/server target before strict generate."
|
|
1222
|
+
return "Prefer an IoT/home gateway donor and explicitly name the thing plus gateway/server target."
|
|
1223
|
+
if capability == "iot_control":
|
|
1224
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1225
|
+
return "Use a donor that already contains IoT rules and explicitly name the rule-bearing device before strict generate."
|
|
1226
|
+
return "Prefer an IoT/home gateway donor with existing rule/control structure before strict generate."
|
|
1227
|
+
if capability == "wireless_client_association":
|
|
1228
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1229
|
+
return "Use a donor-backed wireless prompt that explicitly names the client, AP/router, and SSID before strict generate."
|
|
1230
|
+
return "Prefer a wireless-heavy or IoT/home gateway donor and explicitly name the client, AP/router, and SSID."
|
|
1231
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1232
|
+
return f"Use donor-backed edit flow for {capability} until safe-open generate coverage is raised."
|
|
1233
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1234
|
+
return f"Provide or curate a donor whose skeleton explicitly covers {capability}."
|
|
1235
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1236
|
+
return f"Keep {capability} in explain/edit flow until acceptance evidence is promoted."
|
|
1237
|
+
return f"Import or curate donor coverage for {capability} before strict generate."
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
# Capabilities the planner can configure itself, verified by the corpus rather
|
|
1241
|
+
# than by what a donor happens to demonstrate. Coverage for these is satisfied by
|
|
1242
|
+
# construction: if the operations are emitted and the lab opens, the donor's own
|
|
1243
|
+
# feature list is beside the point.
|
|
1244
|
+
EMITTABLE_CAPABILITIES = {
|
|
1245
|
+
"vlan", "trunk", "access_port", "router_on_a_stick",
|
|
1246
|
+
"dhcp_pool", "router_dhcp", "dhcp_snooping", "dai",
|
|
1247
|
+
"ospf", "ospfv2", "ospf_multiarea", "eigrp", "eigrp_ipv4", "rip", "ripv2",
|
|
1248
|
+
"static_route", "default_route",
|
|
1249
|
+
"acl", "nat", "nat_static", "nat_dynamic", "pat",
|
|
1250
|
+
"stp", "rstp", "etherchannel", "lacp", "pagp", "port_security",
|
|
1251
|
+
"hsrp", "ipv6", "ipv6_slaac", "ospfv3",
|
|
1252
|
+
"telnet", "ssh_ios", "management_vlan",
|
|
1253
|
+
"ntp", "ntp_ios", "syslog_ios", "snmp",
|
|
1254
|
+
"server_dns", "server_http", "server_ftp", "server_tftp", "server_aaa",
|
|
1255
|
+
"dns", "voip", "ip_phone", "call_manager",
|
|
1256
|
+
"wireless_ap", "wireless_client",
|
|
1257
|
+
"ppp", "gre", "vpn", "ipsec",
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _scenario_generate_readiness(
|
|
1262
|
+
scenario_family: str | None,
|
|
1263
|
+
capability_statuses: list[dict[str, object]],
|
|
1264
|
+
unsupported_capabilities: list[str],
|
|
1265
|
+
requires_curated_donor: list[str],
|
|
1266
|
+
requires_manual_acceptance: list[str],
|
|
1267
|
+
) -> dict[str, object]:
|
|
1268
|
+
if not scenario_family:
|
|
1269
|
+
return {
|
|
1270
|
+
"family": None,
|
|
1271
|
+
"status": "not_classified",
|
|
1272
|
+
"critical_capabilities": [],
|
|
1273
|
+
"missing_critical_capabilities": [],
|
|
1274
|
+
"reasons": [],
|
|
1275
|
+
}
|
|
1276
|
+
critical_set = SCENARIO_CAPABILITY_SETS.get(scenario_family, set())
|
|
1277
|
+
status_by_capability = {str(item.get("capability")): item for item in capability_statuses}
|
|
1278
|
+
relevant_capabilities = [cap for cap in status_by_capability if cap in critical_set]
|
|
1279
|
+
# A capability the planner emits operations for is covered by construction,
|
|
1280
|
+
# whatever the sample catalogue says about the donor. `gre` was refused as
|
|
1281
|
+
# "missing critical capability coverage: vpn" while a GRE lab built through
|
|
1282
|
+
# the same path carried `interface Tunnel0` with its source and destination
|
|
1283
|
+
# and opened in Packet Tracer.
|
|
1284
|
+
missing_critical = [
|
|
1285
|
+
cap
|
|
1286
|
+
for cap in unsupported_capabilities
|
|
1287
|
+
if cap in critical_set and cap not in EMITTABLE_CAPABILITIES
|
|
1288
|
+
]
|
|
1289
|
+
donor_limited_critical = [cap for cap in requires_curated_donor if cap in critical_set]
|
|
1290
|
+
gated_critical = [cap for cap in requires_manual_acceptance if cap in critical_set and cap not in donor_limited_critical]
|
|
1291
|
+
reasons: list[str] = []
|
|
1292
|
+
if missing_critical:
|
|
1293
|
+
reasons.append(f"missing critical capability coverage: {', '.join(missing_critical)}")
|
|
1294
|
+
if donor_limited_critical:
|
|
1295
|
+
reasons.append(f"critical capabilities depend on donor-limited safe-open coverage: {', '.join(donor_limited_critical)}")
|
|
1296
|
+
if gated_critical:
|
|
1297
|
+
reasons.append(f"critical capabilities remain acceptance-gated: {', '.join(gated_critical)}")
|
|
1298
|
+
if missing_critical:
|
|
1299
|
+
status = "unsupported"
|
|
1300
|
+
elif donor_limited_critical:
|
|
1301
|
+
status = "donor_limited"
|
|
1302
|
+
elif gated_critical:
|
|
1303
|
+
status = "acceptance_gated"
|
|
1304
|
+
elif relevant_capabilities:
|
|
1305
|
+
status = "ready"
|
|
1306
|
+
else:
|
|
1307
|
+
status = "partial"
|
|
1308
|
+
reasons.append("scenario family is recognized, but no critical capability set was requested explicitly.")
|
|
1309
|
+
return {
|
|
1310
|
+
"family": scenario_family,
|
|
1311
|
+
"status": status,
|
|
1312
|
+
"critical_capabilities": relevant_capabilities,
|
|
1313
|
+
"missing_critical_capabilities": missing_critical,
|
|
1314
|
+
"reasons": reasons,
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def _provider_families_for_capability(capability: str, requested_families: list[str]) -> set[str]:
|
|
1319
|
+
mapped = CAPABILITY_PROVIDER_FAMILIES.get(capability)
|
|
1320
|
+
if mapped:
|
|
1321
|
+
return set(mapped)
|
|
1322
|
+
return set(requested_families)
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def _normalize_media_name(value: str) -> str:
|
|
1326
|
+
media = value.strip().lower().replace("_", "-").replace(" ", "-")
|
|
1327
|
+
aliases = {
|
|
1328
|
+
"estraightthrough": "straight-through",
|
|
1329
|
+
"copper-straight-through": "straight-through",
|
|
1330
|
+
"straight-through": "straight-through",
|
|
1331
|
+
"ecrossover": "crossover",
|
|
1332
|
+
"copper-crossover": "crossover",
|
|
1333
|
+
"crossover": "crossover",
|
|
1334
|
+
"fiber": "fiber",
|
|
1335
|
+
"efiber": "fiber",
|
|
1336
|
+
"serial": "serial",
|
|
1337
|
+
"eserialdce": "serial-dce",
|
|
1338
|
+
"serial-dce": "serial-dce",
|
|
1339
|
+
"eserialdte": "serial-dte",
|
|
1340
|
+
"serial-dte": "serial-dte",
|
|
1341
|
+
}
|
|
1342
|
+
compact = media.replace("-", "")
|
|
1343
|
+
return aliases.get(media, aliases.get(compact, media))
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _pair_key(left: str, right: str) -> str:
|
|
1347
|
+
return " <-> ".join(sorted((left, right)))
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def select_capability_matrix_hits(
|
|
1351
|
+
plan: IntentPlan,
|
|
1352
|
+
samples: list[SampleDescriptor],
|
|
1353
|
+
) -> list[CapabilityMatrixEntry]:
|
|
1354
|
+
matrix = build_capability_matrix(samples)
|
|
1355
|
+
requested_capabilities = set(plan.capabilities)
|
|
1356
|
+
if not requested_capabilities:
|
|
1357
|
+
return []
|
|
1358
|
+
requested_families = _requested_device_families(plan)
|
|
1359
|
+
hits: list[CapabilityMatrixEntry] = []
|
|
1360
|
+
seen: set[tuple[str, str]] = set()
|
|
1361
|
+
for entry in matrix:
|
|
1362
|
+
if entry.capability not in requested_capabilities:
|
|
1363
|
+
continue
|
|
1364
|
+
provider_families = _provider_families_for_capability(entry.capability, requested_families)
|
|
1365
|
+
if provider_families and entry.device_family not in provider_families:
|
|
1366
|
+
continue
|
|
1367
|
+
key = (entry.device_family, entry.capability)
|
|
1368
|
+
if key in seen:
|
|
1369
|
+
continue
|
|
1370
|
+
hits.append(entry)
|
|
1371
|
+
seen.add(key)
|
|
1372
|
+
return hits
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def build_blueprint_plan(plan: IntentPlan, blueprint: dict[str, object] | None = None) -> BlueprintPlan:
|
|
1376
|
+
devices = [dict(device) for device in (blueprint or {}).get("devices", [])]
|
|
1377
|
+
if not devices:
|
|
1378
|
+
devices = [
|
|
1379
|
+
{"type": device_type, "count": count}
|
|
1380
|
+
for device_type, count in sorted(plan.device_requirements.items())
|
|
1381
|
+
if count
|
|
1382
|
+
]
|
|
1383
|
+
links = [dict(link) for link in (blueprint or {}).get("links", [])]
|
|
1384
|
+
if not links:
|
|
1385
|
+
links = [dict(link) for link in plan.links]
|
|
1386
|
+
required_pairs: list[str] = []
|
|
1387
|
+
for link in links:
|
|
1388
|
+
left = str(link.get("a", {}).get("dev", ""))
|
|
1389
|
+
right = str(link.get("b", {}).get("dev", ""))
|
|
1390
|
+
if left and right:
|
|
1391
|
+
required_pairs.append(" <-> ".join(sorted((left, right))))
|
|
1392
|
+
return BlueprintPlan(
|
|
1393
|
+
requested_devices=devices,
|
|
1394
|
+
requested_links=links,
|
|
1395
|
+
required_capabilities=sorted(dict.fromkeys(plan.capabilities)),
|
|
1396
|
+
required_donor_capacity={key: value for key, value in sorted(plan.device_requirements.items()) if value},
|
|
1397
|
+
required_link_pairs=sorted(dict.fromkeys(required_pairs)),
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def build_coverage_gap_report(
|
|
1402
|
+
plan: IntentPlan,
|
|
1403
|
+
samples: list[SampleDescriptor],
|
|
1404
|
+
*,
|
|
1405
|
+
selected_donor: str | None = None,
|
|
1406
|
+
) -> CoverageGapReport:
|
|
1407
|
+
iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
|
|
1408
|
+
selected_sample = _selected_sample_for_donor(samples, selected_donor)
|
|
1409
|
+
matrix = build_capability_matrix(samples)
|
|
1410
|
+
by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
|
|
1411
|
+
by_family: dict[str, list[CapabilityMatrixEntry]] = {}
|
|
1412
|
+
for entry in matrix:
|
|
1413
|
+
by_capability.setdefault(entry.capability, []).append(entry)
|
|
1414
|
+
by_family.setdefault(entry.device_family, []).append(entry)
|
|
1415
|
+
|
|
1416
|
+
requested_families = _requested_device_families(plan)
|
|
1417
|
+
requested_capabilities = sorted(dict.fromkeys(plan.capabilities))
|
|
1418
|
+
supported_capabilities: list[str] = []
|
|
1419
|
+
unsupported_capabilities: list[str] = []
|
|
1420
|
+
requires_curated_donor: list[str] = []
|
|
1421
|
+
requires_manual_acceptance: list[str] = []
|
|
1422
|
+
capability_statuses: list[dict[str, object]] = []
|
|
1423
|
+
for capability in requested_capabilities:
|
|
1424
|
+
provider_families = _provider_families_for_capability(capability, requested_families)
|
|
1425
|
+
selected_donor_backed = _selected_donor_capability_override(plan, capability, selected_sample)
|
|
1426
|
+
wireless_edit_proven_only = (
|
|
1427
|
+
capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES
|
|
1428
|
+
and _has_explicit_advanced_wireless_edit(plan, capability)
|
|
1429
|
+
and not selected_donor_backed
|
|
1430
|
+
)
|
|
1431
|
+
programming_edit_proven_only = (
|
|
1432
|
+
capability in PROGRAMMING_EDIT_PROVEN_CAPABILITIES
|
|
1433
|
+
and _has_explicit_programming_edit(plan, capability)
|
|
1434
|
+
and not selected_donor_backed
|
|
1435
|
+
)
|
|
1436
|
+
programming_donor_backed_ready = (
|
|
1437
|
+
capability in PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES
|
|
1438
|
+
and programming_edit_proven_only
|
|
1439
|
+
)
|
|
1440
|
+
voice_edit_proven_only = (
|
|
1441
|
+
capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES
|
|
1442
|
+
and _has_explicit_voice_edit(plan, capability)
|
|
1443
|
+
and not selected_donor_backed
|
|
1444
|
+
)
|
|
1445
|
+
voice_donor_backed_ready = (
|
|
1446
|
+
capability in VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES
|
|
1447
|
+
and voice_edit_proven_only
|
|
1448
|
+
)
|
|
1449
|
+
l2_security_edit_proven_only = (
|
|
1450
|
+
capability in {"dot1x", "qos"}
|
|
1451
|
+
and _has_explicit_l2_security_edit(plan, capability)
|
|
1452
|
+
and not selected_donor_backed
|
|
1453
|
+
)
|
|
1454
|
+
l2_security_donor_backed_ready = (
|
|
1455
|
+
capability in L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES
|
|
1456
|
+
and l2_security_edit_proven_only
|
|
1457
|
+
)
|
|
1458
|
+
wan_security_edit_proven_only = (
|
|
1459
|
+
capability in WAN_SECURITY_EDIT_PROVEN_CAPABILITIES
|
|
1460
|
+
and _has_explicit_wan_security_edit(plan, capability)
|
|
1461
|
+
and not selected_donor_backed
|
|
1462
|
+
)
|
|
1463
|
+
l2_resiliency_edit_proven_only = (
|
|
1464
|
+
capability in L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES
|
|
1465
|
+
and _has_explicit_l2_resiliency_edit(plan, capability)
|
|
1466
|
+
and not selected_donor_backed
|
|
1467
|
+
)
|
|
1468
|
+
ipv4_routing_management_edit_proven_only = (
|
|
1469
|
+
capability in IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES
|
|
1470
|
+
and _has_explicit_ipv4_routing_management_edit(plan, capability)
|
|
1471
|
+
and not selected_donor_backed
|
|
1472
|
+
)
|
|
1473
|
+
wan_security_donor_backed_ready = (
|
|
1474
|
+
capability in WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES
|
|
1475
|
+
and wan_security_edit_proven_only
|
|
1476
|
+
)
|
|
1477
|
+
wan_security_donor_limited = (
|
|
1478
|
+
capability in WAN_SECURITY_DONOR_LIMITED_CAPABILITIES
|
|
1479
|
+
and _has_explicit_wan_security_intent(plan)
|
|
1480
|
+
and not selected_donor_backed
|
|
1481
|
+
and not wan_security_edit_proven_only
|
|
1482
|
+
)
|
|
1483
|
+
edit_proven_only = (
|
|
1484
|
+
capability in EDIT_PROVEN_REPORT_CAPABILITIES
|
|
1485
|
+
or wireless_edit_proven_only
|
|
1486
|
+
or programming_edit_proven_only
|
|
1487
|
+
or voice_edit_proven_only
|
|
1488
|
+
or l2_security_edit_proven_only
|
|
1489
|
+
or wan_security_edit_proven_only
|
|
1490
|
+
or l2_resiliency_edit_proven_only
|
|
1491
|
+
or ipv4_routing_management_edit_proven_only
|
|
1492
|
+
) and not selected_donor_backed
|
|
1493
|
+
ipv6_donor_backed_ready = (
|
|
1494
|
+
capability in IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES
|
|
1495
|
+
and edit_proven_only
|
|
1496
|
+
)
|
|
1497
|
+
report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
|
|
1498
|
+
matching_entries = [
|
|
1499
|
+
entry
|
|
1500
|
+
for entry in by_capability.get(capability, [])
|
|
1501
|
+
if not provider_families or entry.device_family in provider_families
|
|
1502
|
+
]
|
|
1503
|
+
if matching_entries:
|
|
1504
|
+
supported_capabilities.append(capability)
|
|
1505
|
+
best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
|
|
1506
|
+
acceptance_verified = (
|
|
1507
|
+
(any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
|
|
1508
|
+
or selected_donor_backed
|
|
1509
|
+
)
|
|
1510
|
+
if report_only or edit_proven_only or wan_security_donor_limited:
|
|
1511
|
+
acceptance_verified = False
|
|
1512
|
+
requires_curated = (
|
|
1513
|
+
best_entry.maturity_level == "safe-open-generate-supported"
|
|
1514
|
+
and not selected_donor_backed
|
|
1515
|
+
and not edit_proven_only
|
|
1516
|
+
) or wan_security_donor_limited
|
|
1517
|
+
if requires_curated:
|
|
1518
|
+
requires_curated_donor.append(capability)
|
|
1519
|
+
if not acceptance_verified:
|
|
1520
|
+
requires_manual_acceptance.append(capability)
|
|
1521
|
+
capability_statuses.append(
|
|
1522
|
+
{
|
|
1523
|
+
"capability": capability,
|
|
1524
|
+
"provider_families": sorted(provider_families),
|
|
1525
|
+
"matching_device_families": sorted({entry.device_family for entry in matching_entries}),
|
|
1526
|
+
"best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
|
|
1527
|
+
"inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
|
|
1528
|
+
"edit_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
1529
|
+
"config_mutation_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
1530
|
+
"safe_open_generate_supported": False if report_only or edit_proven_only or wan_security_donor_limited else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
|
|
1531
|
+
"acceptance_verified": acceptance_verified,
|
|
1532
|
+
"donor_backed_ready": bool(
|
|
1533
|
+
selected_donor_backed
|
|
1534
|
+
or ipv6_donor_backed_ready
|
|
1535
|
+
or programming_donor_backed_ready
|
|
1536
|
+
or voice_donor_backed_ready
|
|
1537
|
+
or l2_security_donor_backed_ready
|
|
1538
|
+
or wan_security_donor_backed_ready
|
|
1539
|
+
),
|
|
1540
|
+
"requires_curated_donor": requires_curated,
|
|
1541
|
+
"requires_manual_acceptance": not acceptance_verified,
|
|
1542
|
+
"report_only": report_only,
|
|
1543
|
+
"donor_limited_generate": wan_security_donor_limited,
|
|
1544
|
+
"recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
|
|
1545
|
+
"known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
|
|
1546
|
+
}
|
|
1547
|
+
)
|
|
1548
|
+
elif edit_proven_only:
|
|
1549
|
+
supported_capabilities.append(capability)
|
|
1550
|
+
requires_manual_acceptance.append(capability)
|
|
1551
|
+
capability_statuses.append(
|
|
1552
|
+
{
|
|
1553
|
+
"capability": capability,
|
|
1554
|
+
"provider_families": sorted(provider_families),
|
|
1555
|
+
"matching_device_families": sorted(provider_families),
|
|
1556
|
+
"best_maturity_level": "config-mutation-supported",
|
|
1557
|
+
"inventory_supported": True,
|
|
1558
|
+
"edit_supported": True,
|
|
1559
|
+
"config_mutation_supported": True,
|
|
1560
|
+
"safe_open_generate_supported": False,
|
|
1561
|
+
"acceptance_verified": False,
|
|
1562
|
+
"donor_backed_ready": bool(
|
|
1563
|
+
programming_donor_backed_ready
|
|
1564
|
+
or ipv6_donor_backed_ready
|
|
1565
|
+
or voice_donor_backed_ready
|
|
1566
|
+
or l2_security_donor_backed_ready
|
|
1567
|
+
or wan_security_donor_backed_ready
|
|
1568
|
+
),
|
|
1569
|
+
"requires_curated_donor": False,
|
|
1570
|
+
"requires_manual_acceptance": True,
|
|
1571
|
+
"report_only": False,
|
|
1572
|
+
"donor_limited_generate": False,
|
|
1573
|
+
"recommended_donors": [],
|
|
1574
|
+
"known_limitations": ["editor-proven without sample-backed donor selection"],
|
|
1575
|
+
}
|
|
1576
|
+
)
|
|
1577
|
+
elif report_only:
|
|
1578
|
+
supported_capabilities.append(capability)
|
|
1579
|
+
requires_manual_acceptance.append(capability)
|
|
1580
|
+
capability_statuses.append(
|
|
1581
|
+
{
|
|
1582
|
+
"capability": capability,
|
|
1583
|
+
"provider_families": sorted(provider_families),
|
|
1584
|
+
"matching_device_families": sorted(provider_families),
|
|
1585
|
+
"best_maturity_level": "inventory-supported",
|
|
1586
|
+
"inventory_supported": True,
|
|
1587
|
+
"edit_supported": False,
|
|
1588
|
+
"config_mutation_supported": False,
|
|
1589
|
+
"safe_open_generate_supported": False,
|
|
1590
|
+
"acceptance_verified": False,
|
|
1591
|
+
"donor_backed_ready": False,
|
|
1592
|
+
"requires_curated_donor": False,
|
|
1593
|
+
"requires_manual_acceptance": True,
|
|
1594
|
+
"report_only": True,
|
|
1595
|
+
"donor_limited_generate": False,
|
|
1596
|
+
"recommended_donors": [],
|
|
1597
|
+
"known_limitations": ["report-supported without decode-backed donor selection"],
|
|
1598
|
+
}
|
|
1599
|
+
)
|
|
1600
|
+
elif selected_donor_backed and selected_sample is not None:
|
|
1601
|
+
supported_capabilities.append(capability)
|
|
1602
|
+
capability_statuses.append(
|
|
1603
|
+
{
|
|
1604
|
+
"capability": capability,
|
|
1605
|
+
"provider_families": sorted(provider_families),
|
|
1606
|
+
"matching_device_families": sorted({family for family in (selected_sample.device_families or []) if not provider_families or family in provider_families}),
|
|
1607
|
+
"best_maturity_level": selected_sample.apply_safety_level or "safe-open-generate-supported",
|
|
1608
|
+
"inventory_supported": True,
|
|
1609
|
+
"edit_supported": True,
|
|
1610
|
+
"config_mutation_supported": True,
|
|
1611
|
+
"safe_open_generate_supported": True,
|
|
1612
|
+
"acceptance_verified": True,
|
|
1613
|
+
"donor_backed_ready": True,
|
|
1614
|
+
"requires_curated_donor": False,
|
|
1615
|
+
"requires_manual_acceptance": False,
|
|
1616
|
+
"donor_limited_generate": False,
|
|
1617
|
+
"recommended_donors": [selected_sample.relative_path],
|
|
1618
|
+
"known_limitations": [],
|
|
1619
|
+
}
|
|
1620
|
+
)
|
|
1621
|
+
else:
|
|
1622
|
+
unsupported_capabilities.append(capability)
|
|
1623
|
+
capability_statuses.append(
|
|
1624
|
+
{
|
|
1625
|
+
"capability": capability,
|
|
1626
|
+
"provider_families": sorted(provider_families),
|
|
1627
|
+
"matching_device_families": [],
|
|
1628
|
+
"best_maturity_level": "reference-known",
|
|
1629
|
+
"inventory_supported": False,
|
|
1630
|
+
"edit_supported": False,
|
|
1631
|
+
"config_mutation_supported": False,
|
|
1632
|
+
"safe_open_generate_supported": False,
|
|
1633
|
+
"acceptance_verified": False,
|
|
1634
|
+
"donor_backed_ready": False,
|
|
1635
|
+
"requires_curated_donor": False,
|
|
1636
|
+
"requires_manual_acceptance": True,
|
|
1637
|
+
"donor_limited_generate": False,
|
|
1638
|
+
"recommended_donors": [],
|
|
1639
|
+
"known_limitations": ["no matching donor coverage found"],
|
|
1640
|
+
}
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
unsupported_families = [family for family in requested_families if family not in by_family]
|
|
1644
|
+
|
|
1645
|
+
requires_curated_donor = sorted(dict.fromkeys(requires_curated_donor))
|
|
1646
|
+
requires_manual_acceptance = sorted(dict.fromkeys(requires_manual_acceptance))
|
|
1647
|
+
capability_parity = [
|
|
1648
|
+
{
|
|
1649
|
+
"capability": str(status.get("capability")),
|
|
1650
|
+
"inventory_supported": bool(status.get("inventory_supported")),
|
|
1651
|
+
"edit_supported": bool(status.get("edit_supported")),
|
|
1652
|
+
"donor_backed_ready": _donor_backed_ready(status),
|
|
1653
|
+
"generate_supported": bool(status.get("safe_open_generate_supported")),
|
|
1654
|
+
"acceptance_verified": bool(status.get("acceptance_verified")),
|
|
1655
|
+
"best_maturity_level": _best_maturity_level(status),
|
|
1656
|
+
"generate_mismatch_reason": _generate_mismatch_reason(status),
|
|
1657
|
+
"required_donor_families": sorted(_provider_families_for_capability(str(status.get("capability")), requested_families)),
|
|
1658
|
+
"required_archetypes": CAPABILITY_REQUIRED_ARCHETYPES.get(str(status.get("capability")), []),
|
|
1659
|
+
"required_runtime_features": CAPABILITY_REQUIRED_RUNTIME_FEATURES.get(str(status.get("capability")), []),
|
|
1660
|
+
"recommended_next_action": _recommended_next_action_for_capability(
|
|
1661
|
+
str(status.get("capability")),
|
|
1662
|
+
_generate_mismatch_reason(status),
|
|
1663
|
+
),
|
|
1664
|
+
}
|
|
1665
|
+
for status in capability_statuses
|
|
1666
|
+
]
|
|
1667
|
+
scenario_family = _scenario_family_for_plan(plan, requested_families)
|
|
1668
|
+
if selected_donor is None and plan.blocked_mutations:
|
|
1669
|
+
requires_manual_acceptance = sorted(dict.fromkeys([*requires_manual_acceptance, *plan.blocked_mutations]))
|
|
1670
|
+
scenario_generate_readiness = _scenario_generate_readiness(
|
|
1671
|
+
scenario_family,
|
|
1672
|
+
capability_statuses,
|
|
1673
|
+
unsupported_capabilities,
|
|
1674
|
+
requires_curated_donor,
|
|
1675
|
+
requires_manual_acceptance,
|
|
1676
|
+
)
|
|
1677
|
+
recommended_next_actions: list[str] = []
|
|
1678
|
+
if unsupported_capabilities or unsupported_families:
|
|
1679
|
+
recommended_next_actions.append(
|
|
1680
|
+
"Inspect the blueprint and import or provide a donor that contains the missing capabilities or device families."
|
|
1681
|
+
)
|
|
1682
|
+
if requires_manual_acceptance:
|
|
1683
|
+
recommended_next_actions.append(
|
|
1684
|
+
"Run explain-plan or validate-open-debug before enabling the acceptance-gated capability set."
|
|
1685
|
+
)
|
|
1686
|
+
if not requested_capabilities and not requested_families:
|
|
1687
|
+
recommended_next_actions.append("Add explicit device, service, or topology requirements so donor ranking can be constrained.")
|
|
1688
|
+
|
|
1689
|
+
return CoverageGapReport(
|
|
1690
|
+
requested_capabilities=requested_capabilities,
|
|
1691
|
+
supported_capabilities=supported_capabilities,
|
|
1692
|
+
unsupported_capabilities=unsupported_capabilities,
|
|
1693
|
+
requested_device_families=requested_families,
|
|
1694
|
+
unsupported_device_families=unsupported_families,
|
|
1695
|
+
requires_curated_donor=requires_curated_donor,
|
|
1696
|
+
requires_manual_acceptance=requires_manual_acceptance,
|
|
1697
|
+
capability_statuses=capability_statuses,
|
|
1698
|
+
capability_parity=capability_parity,
|
|
1699
|
+
scenario_family=scenario_family,
|
|
1700
|
+
scenario_generate_readiness=scenario_generate_readiness,
|
|
1701
|
+
recommended_next_actions=recommended_next_actions,
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, object]:
|
|
1706
|
+
device_counts = dict(payload.get("topology_summary", {}).get("device_counts", {}))
|
|
1707
|
+
families = sorted({_device_family_for_type(device_type) for device_type in device_counts})
|
|
1708
|
+
capabilities: set[str] = set()
|
|
1709
|
+
if "multilayer switches" in families:
|
|
1710
|
+
capabilities.add("multilayer_switching")
|
|
1711
|
+
if "wan/cloud/dsl/cable devices" in families:
|
|
1712
|
+
capabilities.add("wan")
|
|
1713
|
+
if "security devices" in families:
|
|
1714
|
+
capabilities.add("security_edge")
|
|
1715
|
+
if payload.get("vlans"):
|
|
1716
|
+
capabilities.update(["vlan", "trunk", "access_port"])
|
|
1717
|
+
if payload.get("dhcp_pools"):
|
|
1718
|
+
capabilities.update(["dhcp_pool", "router_dhcp", "server_dhcp"])
|
|
1719
|
+
if payload.get("services"):
|
|
1720
|
+
capabilities.update(
|
|
1721
|
+
{
|
|
1722
|
+
"dns" if "dns_server" in service else service
|
|
1723
|
+
for service_names in payload["services"].values()
|
|
1724
|
+
for service in service_names
|
|
1725
|
+
}
|
|
1726
|
+
)
|
|
1727
|
+
if payload.get("wireless"):
|
|
1728
|
+
capabilities.update(["wireless_ap", "wireless_mutation", "wireless_client"])
|
|
1729
|
+
if any(family == "end devices" for family in families):
|
|
1730
|
+
capabilities.add("wireless_client_association")
|
|
1731
|
+
if payload.get("iot"):
|
|
1732
|
+
capabilities.add("iot")
|
|
1733
|
+
iot_roles = {str(entry.get("role", "")).strip() for entry in payload["iot"].values() if str(entry.get("role", "")).strip()}
|
|
1734
|
+
if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
|
|
1735
|
+
capabilities.add("iot_registration")
|
|
1736
|
+
if "thing" in iot_roles and ("gateway" in iot_roles or "server" in iot_roles):
|
|
1737
|
+
capabilities.add("iot_control")
|
|
1738
|
+
if any(family == "end devices" for family in families):
|
|
1739
|
+
capabilities.add("end_device_mutation")
|
|
1740
|
+
if payload.get("acl_names"):
|
|
1741
|
+
capabilities.add("acl")
|
|
1742
|
+
if payload.get("topology_summary", {}).get("network_style") == "wan_security":
|
|
1743
|
+
capabilities.update({"vpn", "ipsec", "gre", "ppp"})
|
|
1744
|
+
for routing_details in payload.get("routing", {}).values():
|
|
1745
|
+
capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
|
|
1746
|
+
for l2_details in payload.get("l2_security_monitoring", {}).values():
|
|
1747
|
+
capabilities.update(str(capability) for capability in l2_details.get("capabilities", []) if str(capability).strip())
|
|
1748
|
+
for l2_resiliency_details in payload.get("l2_resiliency_routing", {}).values():
|
|
1749
|
+
capabilities.update(str(capability) for capability in l2_resiliency_details.get("capabilities", []) if str(capability).strip())
|
|
1750
|
+
for ipv4_details in payload.get("ipv4_routing_management", {}).values():
|
|
1751
|
+
capabilities.update(str(capability) for capability in ipv4_details.get("capabilities", []) if str(capability).strip())
|
|
1752
|
+
for voice_details in payload.get("voice", {}).values():
|
|
1753
|
+
capabilities.update(str(capability) for capability in voice_details.get("capabilities", []) if str(capability).strip())
|
|
1754
|
+
for programming_details in payload.get("programming", {}).values():
|
|
1755
|
+
capabilities.update(str(capability) for capability in programming_details.get("feature_tags", []) if str(capability).strip())
|
|
1756
|
+
return {
|
|
1757
|
+
"device_families": families,
|
|
1758
|
+
"capabilities": sorted(capabilities),
|
|
1759
|
+
"device_counts": device_counts,
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def build_donor_graph_fit(sample: SampleDescriptor, blueprint: dict[str, object] | None = None) -> DonorGraphFit:
|
|
1764
|
+
expected_pairs: list[str] = []
|
|
1765
|
+
expected_media: dict[str, str] = {}
|
|
1766
|
+
expected_ports: dict[str, set[str]] = {}
|
|
1767
|
+
blueprint_archetype = ""
|
|
1768
|
+
blueprint_device_count = 0
|
|
1769
|
+
if blueprint:
|
|
1770
|
+
blueprint_archetype = str(blueprint.get("topology_archetype", "")).strip()
|
|
1771
|
+
blueprint_device_count = len(list(blueprint.get("devices", [])))
|
|
1772
|
+
for link in blueprint.get("links", []):
|
|
1773
|
+
left = str(link.get("a", {}).get("dev", ""))
|
|
1774
|
+
right = str(link.get("b", {}).get("dev", ""))
|
|
1775
|
+
if left and right:
|
|
1776
|
+
pair = _pair_key(left, right)
|
|
1777
|
+
expected_pairs.append(pair)
|
|
1778
|
+
media = _normalize_media_name(str(link.get("media", "")))
|
|
1779
|
+
if media:
|
|
1780
|
+
expected_media[pair] = media
|
|
1781
|
+
ports = {
|
|
1782
|
+
str(link.get("a", {}).get("port", "")).strip(),
|
|
1783
|
+
str(link.get("b", {}).get("port", "")).strip(),
|
|
1784
|
+
}
|
|
1785
|
+
expected_ports[pair] = {port for port in ports if port}
|
|
1786
|
+
sample_pairs: set[str] = set()
|
|
1787
|
+
sample_media: dict[str, str] = {}
|
|
1788
|
+
sample_ports: dict[str, set[str]] = {}
|
|
1789
|
+
for link in sample.links:
|
|
1790
|
+
left = str(link.get("from", "")).strip()
|
|
1791
|
+
right = str(link.get("to", "")).strip()
|
|
1792
|
+
if left and right:
|
|
1793
|
+
pair = _pair_key(left, right)
|
|
1794
|
+
sample_pairs.add(pair)
|
|
1795
|
+
media = _normalize_media_name(str(link.get("media", "") or link.get("cable_type", "")))
|
|
1796
|
+
if media and pair not in sample_media:
|
|
1797
|
+
sample_media[pair] = media
|
|
1798
|
+
ports = {str(port).strip() for port in link.get("ports", []) if str(port).strip()}
|
|
1799
|
+
if ports:
|
|
1800
|
+
sample_ports.setdefault(pair, set()).update(ports)
|
|
1801
|
+
matched_pairs = [pair for pair in expected_pairs if pair in sample_pairs]
|
|
1802
|
+
missing_pairs = [pair for pair in expected_pairs if pair not in sample_pairs]
|
|
1803
|
+
port_media_conflicts: list[str] = []
|
|
1804
|
+
for pair in matched_pairs:
|
|
1805
|
+
expected_pair_media = expected_media.get(pair)
|
|
1806
|
+
sample_pair_media = sample_media.get(pair)
|
|
1807
|
+
if expected_pair_media and sample_pair_media and expected_pair_media != sample_pair_media:
|
|
1808
|
+
port_media_conflicts.append(f"{pair} media mismatch ({expected_pair_media} != {sample_pair_media})")
|
|
1809
|
+
expected_pair_ports = expected_ports.get(pair, set())
|
|
1810
|
+
sample_pair_ports = sample_ports.get(pair, set())
|
|
1811
|
+
if expected_pair_ports and sample_pair_ports and not expected_pair_ports.issubset(sample_pair_ports):
|
|
1812
|
+
port_media_conflicts.append(
|
|
1813
|
+
f"{pair} port mismatch (expected {sorted(expected_pair_ports)}, available {sorted(sample_pair_ports)})"
|
|
1814
|
+
)
|
|
1815
|
+
fit_score = max(0, len(matched_pairs) * 10 - len(missing_pairs) * 5 - len(port_media_conflicts) * 3)
|
|
1816
|
+
topology_bonus = 0
|
|
1817
|
+
if blueprint_archetype:
|
|
1818
|
+
sample_topology = set(sample.topology_tags)
|
|
1819
|
+
if blueprint_archetype in sample_topology:
|
|
1820
|
+
topology_bonus += 18
|
|
1821
|
+
elif blueprint_archetype == "chain" and sample_topology & {"department_lan", "core_access"}:
|
|
1822
|
+
topology_bonus += 12
|
|
1823
|
+
elif blueprint_archetype == "core_access" and sample_topology & {"department_lan", "router_on_a_stick"}:
|
|
1824
|
+
topology_bonus += 10
|
|
1825
|
+
elif blueprint_archetype == "wireless_branch" and sample_topology & {"wireless_edge", "small_office"}:
|
|
1826
|
+
topology_bonus += 10
|
|
1827
|
+
capacity_penalty = max(0, blueprint_device_count - sample.device_count) * 2
|
|
1828
|
+
layout_reuse_score = max(
|
|
1829
|
+
0,
|
|
1830
|
+
len(matched_pairs) * 12 + topology_bonus - len(missing_pairs) * 6 - len(port_media_conflicts) * 4 - capacity_penalty,
|
|
1831
|
+
)
|
|
1832
|
+
return DonorGraphFit(
|
|
1833
|
+
matched_pairs=matched_pairs,
|
|
1834
|
+
missing_pairs=missing_pairs,
|
|
1835
|
+
port_media_conflicts=port_media_conflicts,
|
|
1836
|
+
fit_score=fit_score,
|
|
1837
|
+
layout_reuse_score=layout_reuse_score,
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def asdict_list(items: list[Any]) -> list[dict[str, object]]:
|
|
1842
|
+
return [asdict(item) for item in items]
|