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
package/scripts/remote_search.py
CHANGED
|
@@ -14,6 +14,16 @@ import zipfile
|
|
|
14
14
|
from intent_parser import IntentPlan
|
|
15
15
|
|
|
16
16
|
GITHUB_SEARCH_API = "https://api.github.com/search/repositories"
|
|
17
|
+
PERMISSIVE_LICENSES = {
|
|
18
|
+
"APACHE-2.0",
|
|
19
|
+
"BSD-2-CLAUSE",
|
|
20
|
+
"BSD-3-CLAUSE",
|
|
21
|
+
"CC0-1.0",
|
|
22
|
+
"ISC",
|
|
23
|
+
"MIT",
|
|
24
|
+
"UNLICENSE",
|
|
25
|
+
}
|
|
26
|
+
REMOTE_AUDIT_FILENAME = "remote-sample-audit.json"
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
@dataclass
|
|
@@ -26,6 +36,25 @@ class RemoteSearchCandidate:
|
|
|
26
36
|
import_status: str
|
|
27
37
|
default_branch: str | None = None
|
|
28
38
|
archive_url: str | None = None
|
|
39
|
+
candidate_promotion_status: str = "reference_only"
|
|
40
|
+
imported_file_count: int = 0
|
|
41
|
+
pkt_file_count: int = 0
|
|
42
|
+
pka_file_count: int = 0
|
|
43
|
+
readme_file_count: int = 0
|
|
44
|
+
license_file_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_permissive_license(license_or_permission: str | None) -> bool:
|
|
48
|
+
value = (license_or_permission or "").strip().upper()
|
|
49
|
+
return value in PERMISSIVE_LICENSES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def candidate_promotion_status(license_or_permission: str | None) -> str:
|
|
53
|
+
return "validated_curated" if is_permissive_license(license_or_permission) else "reference_only"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def candidate_is_curated_eligible(candidate: RemoteSearchCandidate) -> bool:
|
|
57
|
+
return candidate.candidate_promotion_status == "validated_curated"
|
|
29
58
|
|
|
30
59
|
|
|
31
60
|
def build_remote_search_queries(plan: IntentPlan) -> list[str]:
|
|
@@ -91,6 +120,9 @@ def search_remote_candidates(
|
|
|
91
120
|
import_status="discovered",
|
|
92
121
|
default_branch=item.get("default_branch"),
|
|
93
122
|
archive_url=item.get("archive_url"),
|
|
123
|
+
candidate_promotion_status=candidate_promotion_status(
|
|
124
|
+
str((item.get("license") or {}).get("spdx_id") or "unknown")
|
|
125
|
+
),
|
|
94
126
|
)
|
|
95
127
|
)
|
|
96
128
|
if len(candidates) >= max_results:
|
|
@@ -98,19 +130,66 @@ def search_remote_candidates(
|
|
|
98
130
|
return candidates
|
|
99
131
|
|
|
100
132
|
|
|
133
|
+
def _allowed_archive_member(filename: str) -> bool:
|
|
134
|
+
lower_name = filename.lower()
|
|
135
|
+
return (
|
|
136
|
+
lower_name.endswith(".pkt")
|
|
137
|
+
or lower_name.endswith(".pka")
|
|
138
|
+
or lower_name.endswith("readme.md")
|
|
139
|
+
or lower_name.endswith("license")
|
|
140
|
+
or lower_name.endswith("license.md")
|
|
141
|
+
or lower_name.endswith("license.txt")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _increment_file_counters(candidate: RemoteSearchCandidate, target: Path) -> None:
|
|
146
|
+
lower_name = target.name.lower()
|
|
147
|
+
candidate.imported_file_count += 1
|
|
148
|
+
if lower_name.endswith(".pkt"):
|
|
149
|
+
candidate.pkt_file_count += 1
|
|
150
|
+
elif lower_name.endswith(".pka"):
|
|
151
|
+
candidate.pka_file_count += 1
|
|
152
|
+
elif lower_name == "readme.md":
|
|
153
|
+
candidate.readme_file_count += 1
|
|
154
|
+
elif lower_name in {"license", "license.md", "license.txt"}:
|
|
155
|
+
candidate.license_file_count += 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _clear_import_destination(dest_root: Path) -> None:
|
|
159
|
+
if not dest_root.exists():
|
|
160
|
+
return
|
|
161
|
+
for child in dest_root.iterdir():
|
|
162
|
+
if child.is_dir():
|
|
163
|
+
for nested in child.rglob("*"):
|
|
164
|
+
if nested.is_file():
|
|
165
|
+
nested.unlink()
|
|
166
|
+
for nested_dir in sorted([item for item in child.rglob("*") if item.is_dir()], reverse=True):
|
|
167
|
+
nested_dir.rmdir()
|
|
168
|
+
child.rmdir()
|
|
169
|
+
else:
|
|
170
|
+
child.unlink()
|
|
171
|
+
|
|
172
|
+
|
|
101
173
|
def auto_import_remote_candidates(
|
|
102
174
|
candidates: list[RemoteSearchCandidate],
|
|
103
175
|
import_cache_root: Path,
|
|
104
176
|
*,
|
|
105
177
|
max_results: int = 3,
|
|
178
|
+
dry_run: bool = False,
|
|
106
179
|
) -> list[RemoteSearchCandidate]:
|
|
107
180
|
imported: list[RemoteSearchCandidate] = []
|
|
108
|
-
|
|
181
|
+
if not dry_run:
|
|
182
|
+
import_cache_root.mkdir(parents=True, exist_ok=True)
|
|
109
183
|
for candidate in candidates[:max_results]:
|
|
184
|
+
candidate.candidate_promotion_status = candidate_promotion_status(candidate.license_or_permission)
|
|
110
185
|
repo_name = candidate.repo_url.rstrip("/").split("/")[-1]
|
|
111
186
|
owner_name = candidate.repo_url.rstrip("/").split("/")[-2] if "/" in candidate.repo_url.rstrip("/") else "repo"
|
|
112
187
|
dest_root = import_cache_root / f"{owner_name}_{repo_name}"
|
|
113
188
|
archive_url = candidate.archive_url
|
|
189
|
+
if dry_run:
|
|
190
|
+
candidate.import_status = "dry_run"
|
|
191
|
+
imported.append(candidate)
|
|
192
|
+
continue
|
|
114
193
|
if not archive_url:
|
|
115
194
|
imported.append(candidate)
|
|
116
195
|
continue
|
|
@@ -124,39 +203,26 @@ def auto_import_remote_candidates(
|
|
|
124
203
|
continue
|
|
125
204
|
try:
|
|
126
205
|
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
|
|
127
|
-
|
|
128
|
-
for child in dest_root.iterdir():
|
|
129
|
-
if child.is_dir():
|
|
130
|
-
for nested in child.rglob("*"):
|
|
131
|
-
if nested.is_file():
|
|
132
|
-
nested.unlink()
|
|
133
|
-
for nested_dir in sorted([item for item in child.rglob("*") if item.is_dir()], reverse=True):
|
|
134
|
-
nested_dir.rmdir()
|
|
135
|
-
child.rmdir()
|
|
136
|
-
else:
|
|
137
|
-
child.unlink()
|
|
206
|
+
_clear_import_destination(dest_root)
|
|
138
207
|
dest_root.mkdir(parents=True, exist_ok=True)
|
|
139
208
|
for member in archive.infolist():
|
|
140
|
-
|
|
141
|
-
if not (
|
|
142
|
-
lower_name.endswith(".pkt")
|
|
143
|
-
or lower_name.endswith(".pka")
|
|
144
|
-
or lower_name.endswith("readme.md")
|
|
145
|
-
or lower_name.endswith("license")
|
|
146
|
-
or lower_name.endswith("license.md")
|
|
147
|
-
or lower_name.endswith("license.txt")
|
|
148
|
-
):
|
|
209
|
+
if not _allowed_archive_member(member.filename):
|
|
149
210
|
continue
|
|
150
211
|
cleaned = re.sub(r"^[^/]+/", "", member.filename).strip("/")
|
|
151
212
|
if not cleaned:
|
|
152
213
|
continue
|
|
153
214
|
target = dest_root / cleaned
|
|
215
|
+
try:
|
|
216
|
+
target.resolve().relative_to(dest_root.resolve())
|
|
217
|
+
except ValueError:
|
|
218
|
+
continue
|
|
154
219
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
155
220
|
if member.is_dir():
|
|
156
221
|
target.mkdir(parents=True, exist_ok=True)
|
|
157
222
|
continue
|
|
158
223
|
with archive.open(member) as source, target.open("wb") as sink:
|
|
159
224
|
sink.write(source.read())
|
|
225
|
+
_increment_file_counters(candidate, target)
|
|
160
226
|
candidate.path = str(dest_root)
|
|
161
227
|
candidate.import_status = "imported"
|
|
162
228
|
except zipfile.BadZipFile:
|
|
@@ -165,5 +231,115 @@ def auto_import_remote_candidates(
|
|
|
165
231
|
return imported
|
|
166
232
|
|
|
167
233
|
|
|
234
|
+
def _decode_audit_for_root(root: Path, license_or_permission: str) -> dict[str, object]:
|
|
235
|
+
pkt_paths = sorted(root.rglob("*.pkt")) if root.exists() else []
|
|
236
|
+
decode_success_count = 0
|
|
237
|
+
decode_failure_count = 0
|
|
238
|
+
detected_features: set[str] = set()
|
|
239
|
+
detected_families: set[str] = set()
|
|
240
|
+
for pkt_path in pkt_paths:
|
|
241
|
+
try:
|
|
242
|
+
from sample_catalog import summarize_pkt_descriptor
|
|
243
|
+
|
|
244
|
+
descriptor = summarize_pkt_descriptor(
|
|
245
|
+
pkt_path,
|
|
246
|
+
str(pkt_path.relative_to(root)),
|
|
247
|
+
origin="external-reference",
|
|
248
|
+
prototype_eligible=False,
|
|
249
|
+
trust_level="reference-only",
|
|
250
|
+
role="reference",
|
|
251
|
+
license_or_permission=license_or_permission,
|
|
252
|
+
promotion_status="reference_only",
|
|
253
|
+
donor_eligible=False,
|
|
254
|
+
)
|
|
255
|
+
except Exception:
|
|
256
|
+
decode_failure_count += 1
|
|
257
|
+
continue
|
|
258
|
+
decode_success_count += 1
|
|
259
|
+
detected_features.update(descriptor.capability_tags)
|
|
260
|
+
detected_features.update(descriptor.validated_edit_capabilities)
|
|
261
|
+
detected_families.update(descriptor.archetype_tags)
|
|
262
|
+
detected_families.update(descriptor.device_families)
|
|
263
|
+
detected_families.update(descriptor.topology_tags)
|
|
264
|
+
return {
|
|
265
|
+
"decode_status": "not_run" if not pkt_paths else ("ok" if decode_failure_count == 0 else "partial_or_failed"),
|
|
266
|
+
"decode_success_count": decode_success_count,
|
|
267
|
+
"decode_failure_count": decode_failure_count,
|
|
268
|
+
"detected_feature_tags": sorted(detected_features),
|
|
269
|
+
"detected_feature_families": sorted(detected_families),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_remote_sample_audit(
|
|
274
|
+
candidates: list[RemoteSearchCandidate],
|
|
275
|
+
import_cache_root: Path,
|
|
276
|
+
) -> dict[str, object]:
|
|
277
|
+
entries: list[dict[str, object]] = []
|
|
278
|
+
license_counts: dict[str, int] = {}
|
|
279
|
+
for candidate in candidates:
|
|
280
|
+
license_value = candidate.license_or_permission or "unknown"
|
|
281
|
+
license_counts[license_value] = license_counts.get(license_value, 0) + 1
|
|
282
|
+
entry = asdict(candidate)
|
|
283
|
+
if candidate.path:
|
|
284
|
+
entry.update(_decode_audit_for_root(Path(candidate.path), candidate.license_or_permission))
|
|
285
|
+
else:
|
|
286
|
+
entry.update(
|
|
287
|
+
{
|
|
288
|
+
"decode_status": "not_run",
|
|
289
|
+
"decode_success_count": 0,
|
|
290
|
+
"decode_failure_count": 0,
|
|
291
|
+
"detected_feature_tags": [],
|
|
292
|
+
"detected_feature_families": [],
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
if candidate.candidate_promotion_status == "validated_curated" and int(entry.get("decode_success_count", 0)) > 0:
|
|
296
|
+
entry["validation_promotion_status"] = "validated_curated"
|
|
297
|
+
else:
|
|
298
|
+
entry["validation_promotion_status"] = "reference_only"
|
|
299
|
+
entries.append(entry)
|
|
300
|
+
return {
|
|
301
|
+
"audit_version": "1.0",
|
|
302
|
+
"cache_root": str(import_cache_root),
|
|
303
|
+
"raw_pkt_policy": "remote Packet Tracer files stay local/cache-only and are not committed or packed",
|
|
304
|
+
"candidate_count": len(candidates),
|
|
305
|
+
"imported_repo_count": sum(1 for candidate in candidates if candidate.import_status == "imported"),
|
|
306
|
+
"dry_run_repo_count": sum(1 for candidate in candidates if candidate.import_status == "dry_run"),
|
|
307
|
+
"pkt_file_count": sum(candidate.pkt_file_count for candidate in candidates),
|
|
308
|
+
"pka_file_count": sum(candidate.pka_file_count for candidate in candidates),
|
|
309
|
+
"decode_success_count": sum(int(entry.get("decode_success_count", 0)) for entry in entries),
|
|
310
|
+
"decode_failure_count": sum(int(entry.get("decode_failure_count", 0)) for entry in entries),
|
|
311
|
+
"license_counts": license_counts,
|
|
312
|
+
"promotion_status_counts": {
|
|
313
|
+
status: sum(1 for candidate in candidates if candidate.candidate_promotion_status == status)
|
|
314
|
+
for status in sorted({candidate.candidate_promotion_status for candidate in candidates})
|
|
315
|
+
},
|
|
316
|
+
"validation_promotion_status_counts": {
|
|
317
|
+
status: sum(1 for entry in entries if entry.get("validation_promotion_status") == status)
|
|
318
|
+
for status in sorted({str(entry.get("validation_promotion_status")) for entry in entries})
|
|
319
|
+
},
|
|
320
|
+
"top_gaps_filled_by_imported_samples": sorted(
|
|
321
|
+
{
|
|
322
|
+
feature
|
|
323
|
+
for entry in entries
|
|
324
|
+
for feature in entry.get("detected_feature_tags", [])
|
|
325
|
+
if isinstance(feature, str)
|
|
326
|
+
}
|
|
327
|
+
)[:25],
|
|
328
|
+
"candidates": entries,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def write_remote_sample_audit(
|
|
333
|
+
candidates: list[RemoteSearchCandidate],
|
|
334
|
+
import_cache_root: Path,
|
|
335
|
+
audit_path: Path | None = None,
|
|
336
|
+
) -> dict[str, object]:
|
|
337
|
+
payload = build_remote_sample_audit(candidates, import_cache_root)
|
|
338
|
+
target = audit_path or (import_cache_root / REMOTE_AUDIT_FILENAME)
|
|
339
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
341
|
+
return payload
|
|
342
|
+
|
|
343
|
+
|
|
168
344
|
def asdict_list(candidates: list[RemoteSearchCandidate]) -> list[dict[str, object]]:
|
|
169
345
|
return [asdict(candidate) for candidate in candidates]
|
|
@@ -39,24 +39,25 @@ def _packet_tracer_os_name(host_os: str) -> str:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def runtime_env_examples(host_os: str) -> list[str]:
|
|
42
|
+
# PKT_TWOFISH_* is optional: it selects the compiled accelerator. The
|
|
43
|
+
# vendored pure-Python engine is used automatically when it is absent.
|
|
42
44
|
if host_os == "Windows":
|
|
43
45
|
return [
|
|
44
46
|
r"$env:PACKET_TRACER_ROOT='C:\Program Files\Cisco Packet Tracer 9.0.0'",
|
|
45
47
|
r"$env:PACKET_TRACER_COMPAT_DONOR='C:\path\to\your-working-9.0-donor.pkt'",
|
|
46
|
-
r'$env:PKT_TWOFISH_LIBRARY="C:\path\to\_twofish.cp314-win_amd64.pyd"',
|
|
47
|
-
r'$env:PKT_TWOFISH_SEARCH_ROOTS="C:\path\to\bridge-folder"',
|
|
48
|
+
r'# optional speed-up: $env:PKT_TWOFISH_LIBRARY="C:\path\to\_twofish.cp314-win_amd64.pyd"',
|
|
48
49
|
]
|
|
49
50
|
if host_os == "macOS":
|
|
50
51
|
return [
|
|
51
52
|
"export PACKET_TRACER_ROOT='/Applications/Cisco Packet Tracer.app/Contents/Resources'",
|
|
52
53
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
53
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/
|
|
54
|
+
"# optional speed-up: export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/pkt-bridges\"",
|
|
54
55
|
]
|
|
55
56
|
if host_os == "Linux":
|
|
56
57
|
return [
|
|
57
58
|
"export PACKET_TRACER_ROOT='/opt/pt/bin'",
|
|
58
59
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
59
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/
|
|
60
|
+
"# optional speed-up: export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/pkt-bridges\"",
|
|
60
61
|
]
|
|
61
62
|
return []
|
|
62
63
|
|
|
@@ -71,8 +72,8 @@ def _best_next_fix(runtime_blockers: list[str], recommended_next_steps: list[str
|
|
|
71
72
|
"missing_twofish_bridge": "Fix the Twofish bridge next so strict decode/edit/generate can run locally.",
|
|
72
73
|
"missing_packet_tracer_root": "Fix PACKET_TRACER_ROOT so the doctor can resolve the install layout deterministically.",
|
|
73
74
|
"missing_packet_tracer_executable": "Fix the Packet Tracer install root or executable path before relying on validate_open.",
|
|
74
|
-
"windows_first_runtime": "
|
|
75
|
-
"using_external_bridge_only": "
|
|
75
|
+
"windows_first_runtime": "Set PACKET_TRACER_ROOT so the Packet Tracer executable resolves on this host.",
|
|
76
|
+
"using_external_bridge_only": "Verify the vendored pure-Python Twofish engine; the external compiled bridge is only an accelerator.",
|
|
76
77
|
}
|
|
77
78
|
for blocker in runtime_blockers:
|
|
78
79
|
if blocker in blocker_map:
|
|
@@ -94,7 +95,7 @@ def _why_it_is_blocked(runtime_blockers: list[str], bridge_resolution: str) -> s
|
|
|
94
95
|
reasons.append("Packet Tracer executable is not resolved")
|
|
95
96
|
if "windows_first_runtime" in runtime_blockers:
|
|
96
97
|
reasons.append("strict bundled validation is still Windows-first")
|
|
97
|
-
if bridge_resolution == "external_env":
|
|
98
|
+
if bridge_resolution == "external_env" and "using_external_bridge_only" in runtime_blockers:
|
|
98
99
|
reasons.append("strict runtime currently relies on an external bridge override")
|
|
99
100
|
return "; ".join(reasons) + "."
|
|
100
101
|
|
|
@@ -116,9 +117,9 @@ def build_recommended_next_steps(
|
|
|
116
117
|
) -> list[str]:
|
|
117
118
|
guidance: list[str] = []
|
|
118
119
|
if not runtime_supported:
|
|
119
|
-
guidance.append(
|
|
120
|
+
guidance.append(runtime_message)
|
|
120
121
|
if python_support_status != "ok":
|
|
121
|
-
guidance.append("Use Python 3.
|
|
122
|
+
guidance.append("Use Python 3.10 or newer.")
|
|
122
123
|
if packet_tracer_root is None and recommended_root:
|
|
123
124
|
guidance.append(f"Set PACKET_TRACER_ROOT to {recommended_root}.")
|
|
124
125
|
if donor_status != "ok":
|
|
@@ -147,7 +148,7 @@ def build_recommended_next_steps(
|
|
|
147
148
|
message.append("or set PKT_TWOFISH_LIBRARY / PKT_TWOFISH_SEARCH_ROOTS.")
|
|
148
149
|
guidance.append(" ".join(message))
|
|
149
150
|
if host_os in {"macOS", "Linux"} and not runtime_supported:
|
|
150
|
-
guidance.append("
|
|
151
|
+
guidance.append("On non-Windows hosts set PACKET_TRACER_ROOT explicitly; the codec itself needs no extra setup.")
|
|
151
152
|
return guidance
|
|
152
153
|
|
|
153
154
|
|
|
@@ -176,14 +177,27 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
176
177
|
bridge_resolution = "external_env"
|
|
177
178
|
else:
|
|
178
179
|
bridge_resolution = "missing"
|
|
180
|
+
twofish_backend = str(twofish.get("twofish_backend") or "")
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
# The Windows-only restriction existed because the codec needed a compiled
|
|
183
|
+
# Twofish bridge that was only ever built for Windows. The vendored
|
|
184
|
+
# pure-Python engine removed that dependency, and `packet_tracer_env` already
|
|
185
|
+
# resolves install layouts for macOS and Linux, so any host with Packet Tracer
|
|
186
|
+
# installed is supported. What differs by platform is how much has been
|
|
187
|
+
# exercised in practice, which is a confidence note, not a blocker.
|
|
188
|
+
runtime_supported = packet_tracer_exe is not None
|
|
189
|
+
if runtime_supported and host_os == "Windows":
|
|
190
|
+
runtime_message = f"resolved Windows Packet Tracer runtime at {packet_tracer_exe}"
|
|
191
|
+
elif runtime_supported:
|
|
192
|
+
runtime_message = (
|
|
193
|
+
f"resolved {host_os} Packet Tracer runtime at {packet_tracer_exe}. "
|
|
194
|
+
"Non-Windows hosts are supported but less exercised; verify a generated file opens."
|
|
195
|
+
)
|
|
185
196
|
else:
|
|
186
|
-
runtime_message =
|
|
197
|
+
runtime_message = (
|
|
198
|
+
"no Packet Tracer executable was resolved. Decode, inventory and edit still work; "
|
|
199
|
+
"set PACKET_TRACER_ROOT to enable validate_open."
|
|
200
|
+
)
|
|
187
201
|
|
|
188
202
|
blocking_reasons: list[str] = []
|
|
189
203
|
if twofish.get("python_support_status") != "ok":
|
|
@@ -201,7 +215,7 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
201
215
|
if packet_tracer_root is None:
|
|
202
216
|
blocking_reasons.append("packet_tracer_root:not_set")
|
|
203
217
|
if not runtime_supported:
|
|
204
|
-
blocking_reasons.append(
|
|
218
|
+
blocking_reasons.append("packet_tracer_executable:not_resolved")
|
|
205
219
|
|
|
206
220
|
recommended_next_steps = build_recommended_next_steps(
|
|
207
221
|
host_os=host_os,
|
|
@@ -235,9 +249,18 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
235
249
|
runtime_blockers.append("missing_packet_tracer_root")
|
|
236
250
|
if packet_tracer_exe is None:
|
|
237
251
|
runtime_blockers.append("missing_packet_tracer_executable")
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
# `windows_first_runtime` is no longer raised: a missing Packet Tracer
|
|
253
|
+
# executable is already reported as `missing_packet_tracer_executable`, and
|
|
254
|
+
# the host OS by itself no longer blocks anything.
|
|
255
|
+
# An externally-resolved compiled bridge is no longer a runtime blocker: it is
|
|
256
|
+
# an optional accelerator over the vendored pure-Python engine, which is always
|
|
257
|
+
# repo-local. Only flag it when the pure engine itself could not be verified.
|
|
258
|
+
if (
|
|
259
|
+
bridge_resolution == "external_env"
|
|
260
|
+
and twofish_backend != "pure_python"
|
|
261
|
+
and str(twofish.get("twofish_load_status")) != "ok"
|
|
262
|
+
and "using_external_bridge_only" not in runtime_blockers
|
|
263
|
+
):
|
|
241
264
|
runtime_blockers.append("using_external_bridge_only")
|
|
242
265
|
if runtime_blockers:
|
|
243
266
|
runtime_grade = "blocked" if len(ready_operations) == 0 else "partially_ready"
|
|
@@ -281,20 +304,43 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
281
304
|
)
|
|
282
305
|
why_it_is_blocked = _why_it_is_blocked(runtime_blockers, bridge_resolution)
|
|
283
306
|
best_next_fix = _best_next_fix(runtime_blockers, recommended_next_steps)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
307
|
+
if twofish_backend == "pure_python":
|
|
308
|
+
bridge_recommendation = (
|
|
309
|
+
"Vendored pure-Python Twofish is in use; no bridge is required. "
|
|
310
|
+
"A compiled bridge is optional and only speeds up large labs (~12x)."
|
|
311
|
+
)
|
|
312
|
+
elif bridge_resolution == "external_env":
|
|
313
|
+
bridge_recommendation = (
|
|
314
|
+
"A compiled accelerator is being used from an external path. "
|
|
315
|
+
"This is optional; the vendored pure-Python engine is the supported baseline."
|
|
316
|
+
)
|
|
317
|
+
elif bridge_resolution == "missing":
|
|
318
|
+
bridge_recommendation = "Provide PKT_TWOFISH_LIBRARY or PKT_TWOFISH_SEARCH_ROOTS to resolve a local bridge."
|
|
319
|
+
else:
|
|
320
|
+
bridge_recommendation = "Repo-local bridge is resolved."
|
|
291
321
|
runtime_contract_notes = (
|
|
292
|
-
"
|
|
322
|
+
"The vendored pure-Python Twofish engine is repo-local, so this checkout can run strict "
|
|
323
|
+
"decode/edit/generate with no binaries and no environment variables."
|
|
324
|
+
if twofish_backend == "pure_python"
|
|
325
|
+
else "Repo-local bridge and donor are present, so this checkout can run strict decode/edit/generate locally."
|
|
293
326
|
if bridge_resolution == "repo_local"
|
|
294
327
|
else "Strict decode/edit/generate currently rely on an external bridge path. Repo-local runtime packaging is still incomplete."
|
|
295
328
|
if bridge_resolution == "external_env"
|
|
296
329
|
else "No bridge is resolved. validate_open may still work when Packet Tracer is installed, but strict decode/edit/generate remain blocked."
|
|
297
330
|
)
|
|
331
|
+
strict_gate_ready = twofish.get("twofish_load_status") == "ok"
|
|
332
|
+
runtime_gate_status = {
|
|
333
|
+
"default_gate": "unit/doc surface can pass; requires_twofish tests skip when the bridge is missing",
|
|
334
|
+
"strict_gate": "requires PKT_REQUIRE_TWOFISH_TESTS=1 plus PKT_TWOFISH_LIBRARY or PKT_TWOFISH_SEARCH_ROOTS",
|
|
335
|
+
"strict_gate_ready": strict_gate_ready,
|
|
336
|
+
"strict_gate_command": "PKT_REQUIRE_TWOFISH_TESTS=1 python -m pytest tests -q",
|
|
337
|
+
}
|
|
338
|
+
user_summary = {
|
|
339
|
+
"status": runtime_grade,
|
|
340
|
+
"message": doctor_summary,
|
|
341
|
+
"next_best_action": best_next_fix,
|
|
342
|
+
"runtime_gate_status": runtime_gate_status,
|
|
343
|
+
}
|
|
298
344
|
|
|
299
345
|
return {
|
|
300
346
|
"host_os": host_os,
|
|
@@ -303,7 +349,7 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
303
349
|
"message": "supported" if installer_supported else "unknown host platform",
|
|
304
350
|
},
|
|
305
351
|
"real_pkt_runtime_support": {
|
|
306
|
-
"status": "validated" if runtime_supported else "
|
|
352
|
+
"status": "validated" if runtime_supported else "packet_tracer_not_resolved",
|
|
307
353
|
"message": runtime_message,
|
|
308
354
|
},
|
|
309
355
|
"env_examples": runtime_env_examples(host_os),
|
|
@@ -320,6 +366,7 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
320
366
|
"twofish_search_roots": resolved_search_roots,
|
|
321
367
|
"resolved_twofish_path": resolved_twofish_path,
|
|
322
368
|
"twofish_source": twofish.get("twofish_source", ""),
|
|
369
|
+
"twofish_backend": twofish.get("twofish_backend", ""),
|
|
323
370
|
"twofish_load_status": twofish.get("twofish_load_status", "unknown"),
|
|
324
371
|
"twofish_message": twofish.get("twofish_message", "unknown"),
|
|
325
372
|
"twofish_sha256": twofish.get("twofish_sha256", ""),
|
|
@@ -330,6 +377,8 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
330
377
|
"target_version": donor.get("target_version", ""),
|
|
331
378
|
"resolved_donor_path": donor.get("resolved_donor_path", ""),
|
|
332
379
|
"donor_version": donor.get("donor_version", ""),
|
|
380
|
+
"donor_policy": donor.get("donor_policy", ""),
|
|
381
|
+
"donor_compatibility_tier": donor.get("compatibility_tier", ""),
|
|
333
382
|
"donor_source": donor.get("donor_source", ""),
|
|
334
383
|
"donor_status": donor.get("status", "unknown"),
|
|
335
384
|
"donor_message": donor.get("message", "unknown"),
|
|
@@ -343,6 +392,8 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
343
392
|
"what_is_blocked": what_is_blocked,
|
|
344
393
|
"why_it_is_blocked": why_it_is_blocked,
|
|
345
394
|
"best_next_fix": best_next_fix,
|
|
395
|
+
"user_summary": user_summary,
|
|
396
|
+
"runtime_gate_status": runtime_gate_status,
|
|
346
397
|
"doctor_summary": doctor_summary,
|
|
347
398
|
"runtime_grade": runtime_grade,
|
|
348
399
|
"recommended_next_steps": recommended_next_steps,
|