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.
Files changed (66) hide show
  1. package/CHANGELOG.md +424 -42
  2. package/README.md +535 -250
  3. package/SKILL.md +337 -262
  4. package/bin/packet-tracer-skill.js +29 -2
  5. package/docs/automation-controller-proof.md +35 -0
  6. package/docs/curated-donor-registry.md +11 -0
  7. package/docs/generate-ready-pilot-design.md +30 -0
  8. package/docs/github-launch-ops-0.2.3.md +37 -0
  9. package/docs/github-metadata.md +6 -4
  10. package/docs/hero-demo-plan.md +1 -1
  11. package/docs/home-iot-donor-proof.md +4 -4
  12. package/docs/industrial-programming-proof.md +48 -0
  13. package/docs/ipv4-routing-management-proof.md +37 -0
  14. package/docs/l2-resiliency-bgp-proof.md +60 -0
  15. package/docs/l2-security-qos-proof.md +59 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +174 -17
  17. package/docs/post-launch-follow-up.md +9 -5
  18. package/docs/proof-readiness-dashboard.md +69 -0
  19. package/docs/publish-preview-roadmap.md +6 -5
  20. package/docs/release-checklist.md +27 -13
  21. package/docs/release-notes-0.2.2.md +1 -1
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/release-notes-0.2.4.md +20 -0
  24. package/docs/runtime-truth.md +33 -8
  25. package/docs/security-edge-deepening-proof.md +65 -0
  26. package/docs/voice-collaboration-proof.md +38 -0
  27. package/docs/wan-security-donor-proof.md +20 -3
  28. package/examples/README.md +98 -69
  29. package/examples/complex_campus_master_edit_v4.inventory.json +12 -2
  30. package/examples/gallery.md +94 -6
  31. package/examples/home_iot_cli_edit_v1.inventory.json +11 -2
  32. package/examples/index.json +932 -4
  33. package/examples/local-sample-evidence.json +24 -0
  34. package/examples/proof-cards.json +117 -0
  35. package/examples/service_heavy_cli_edit_v1.inventory.json +11 -2
  36. package/package.json +60 -44
  37. package/pytest.ini +9 -0
  38. package/references/packettracer-feature-atlas.json +67 -17
  39. package/references/packettracer-sample-catalog.json +45287 -4525
  40. package/references/packettracer-sample-catalog.md +599 -259
  41. package/references/proof-readiness-candidates.json +352 -0
  42. package/scripts/build_examples_index.py +228 -35
  43. package/scripts/build_sample_catalog.py +24 -44
  44. package/scripts/corpus_runner.py +430 -0
  45. package/scripts/coverage_matrix.py +1842 -1319
  46. package/scripts/donor_cache.py +354 -0
  47. package/scripts/donor_diagnostics.py +3 -1
  48. package/scripts/feature_atlas.py +65 -1
  49. package/scripts/generate_pkt.py +8762 -4070
  50. package/scripts/intent_parser.py +2242 -1138
  51. package/scripts/local_donors.py +340 -0
  52. package/scripts/packet_tracer_env.py +846 -391
  53. package/scripts/pkt_annotate.py +218 -0
  54. package/scripts/pkt_codec.py +420 -181
  55. package/scripts/pkt_editor.py +2405 -1226
  56. package/scripts/pkt_transformer.py +1072 -727
  57. package/scripts/pkt_verify.py +461 -0
  58. package/scripts/remote_search.py +197 -21
  59. package/scripts/runtime_doctor.py +80 -29
  60. package/scripts/sample_catalog.py +1372 -1195
  61. package/scripts/twofish_diagnostics.py +48 -31
  62. package/scripts/usage_ledger.py +218 -0
  63. package/scripts/vendor/README.md +44 -37
  64. package/scripts/vendor/twofish_pure.py +321 -0
  65. package/scripts/workspace_repair.py +548 -508
  66. package/templates/pt900/donors/README.md +15 -0
@@ -23,9 +23,25 @@ def _vendor_dir() -> Path:
23
23
  return Path(__file__).resolve().parent / "vendor"
24
24
 
25
25
 
26
+ MINIMUM_PYTHON = (3, 10)
27
+
28
+
29
+ def _pure_python_status() -> tuple[bool, str]:
30
+ """Verify the vendored pure-Python engine against the official vectors."""
31
+ try:
32
+ sys.path.insert(0, str(_vendor_dir()))
33
+ from twofish_pure import self_test
34
+
35
+ self_test()
36
+ return True, "vendored pure-Python Twofish (official test vectors pass)"
37
+ except Exception as exc: # pragma: no cover - runtime diagnostics
38
+ return False, f"vendored pure-Python Twofish failed: {exc}"
39
+
40
+
26
41
  def collect_twofish_diagnostics() -> dict[str, str]:
27
42
  python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
28
- python_supported = sys.version_info[:2] == SUPPORTED_PYTHON
43
+ python_supported = sys.version_info[:2] >= MINIMUM_PYTHON
44
+ compiled_abi_match = sys.version_info[:2] == SUPPORTED_PYTHON
29
45
  result = {
30
46
  "host_os": normalized_host_os(),
31
47
  "python_version": python_version,
@@ -33,49 +49,50 @@ def collect_twofish_diagnostics() -> dict[str, str]:
33
49
  "python_support_message": (
34
50
  "supported"
35
51
  if python_supported
36
- else f"requires Python {SUPPORTED_PYTHON[0]}.{SUPPORTED_PYTHON[1]}.x"
52
+ else f"requires Python {MINIMUM_PYTHON[0]}.{MINIMUM_PYTHON[1]} or newer"
37
53
  ),
38
54
  "expected_twofish_patterns": expected_bridge_patterns(),
39
55
  "twofish_search_roots": recommended_search_roots(_vendor_dir()),
40
56
  "resolved_twofish_path": "",
41
57
  "twofish_source": "",
58
+ "twofish_backend": "",
42
59
  "twofish_load_status": "missing",
43
- "twofish_message": "no local Twofish bridge was found",
60
+ "twofish_message": "no Twofish engine could be resolved",
44
61
  "twofish_sha256": "",
45
62
  }
46
63
 
47
- for source, candidate in candidate_bridge_paths(_vendor_dir(), env_path=os.getenv("PKT_TWOFISH_LIBRARY")):
48
- if not candidate.exists():
49
- continue
50
- result["resolved_twofish_path"] = str(candidate)
51
- result["twofish_source"] = source
52
- result["twofish_sha256"] = hashlib.sha256(candidate.read_bytes()).hexdigest()
53
- if not python_supported:
54
- result["twofish_load_status"] = "python_unsupported"
55
- result["twofish_message"] = (
56
- f"found {candidate}, but this bridge is only supported with Python "
57
- f"{SUPPORTED_PYTHON[0]}.{SUPPORTED_PYTHON[1]}.x"
58
- )
59
- return result
60
- try:
61
- library = CDLL(str(candidate))
62
- getattr(library, "exp_Twofish_encrypt")
63
- getattr(library, "exp_Twofish_decrypt")
64
+ # The compiled bridge is an optional accelerator: preferred when it loads,
65
+ # but never required, because the vendored pure-Python engine always works.
66
+ if compiled_abi_match:
67
+ for source, candidate in candidate_bridge_paths(
68
+ _vendor_dir(), env_path=os.getenv("PKT_TWOFISH_LIBRARY")
69
+ ):
70
+ if not candidate.exists():
71
+ continue
72
+ try:
73
+ library = CDLL(str(candidate))
74
+ getattr(library, "exp_Twofish_encrypt")
75
+ getattr(library, "exp_Twofish_decrypt")
76
+ except Exception: # pragma: no cover - runtime diagnostics
77
+ continue
78
+ result["resolved_twofish_path"] = str(candidate)
79
+ result["twofish_source"] = source
80
+ result["twofish_sha256"] = hashlib.sha256(candidate.read_bytes()).hexdigest()
81
+ result["twofish_backend"] = "compiled"
64
82
  result["twofish_load_status"] = "ok"
65
- result["twofish_message"] = f"loaded {candidate}"
66
- return result
67
- except Exception as exc: # pragma: no cover - runtime diagnostics
68
- result["twofish_load_status"] = "load_error"
69
- result["twofish_message"] = f"{candidate}: {exc}"
83
+ result["twofish_message"] = f"loaded compiled accelerator {candidate}"
70
84
  return result
71
85
 
72
- env_path = os.getenv("PKT_TWOFISH_LIBRARY")
73
- if env_path:
74
- result["resolved_twofish_path"] = str(Path(env_path).expanduser())
75
- result["twofish_source"] = "env"
76
- result["twofish_load_status"] = "missing"
77
- result["twofish_message"] = f"set but missing: {env_path}"
86
+ pure_ok, pure_message = _pure_python_status()
87
+ if pure_ok and python_supported:
88
+ result["twofish_backend"] = "pure_python"
89
+ result["twofish_source"] = "vendored"
90
+ result["resolved_twofish_path"] = str(_vendor_dir() / "twofish_pure.py")
91
+ result["twofish_load_status"] = "ok"
92
+ result["twofish_message"] = pure_message
93
+ return result
78
94
 
95
+ result["twofish_message"] = pure_message
79
96
  return result
80
97
 
81
98
 
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """A local, append-only record of what actually worked, so the skill improves with use.
3
+
4
+ Donor selection is expensive and mostly repetitive: the same scenario families
5
+ come back, and the same handful of donors keep winning or keep failing for the
6
+ same reasons. Rediscovering that on every run wastes several seconds per
7
+ candidate and, worse, throws away the only real evidence the skill ever gets
8
+ about which donors survive a Packet Tracer open.
9
+
10
+ The ledger records the outcome of each generation and feeds it back into donor
11
+ ranking on the next run. Nothing is inferred or guessed: an entry is written
12
+ only after a real attempt produced a real result.
13
+
14
+ Privacy and safety rules, deliberately strict:
15
+
16
+ - the ledger is local only. It is written under `output/`, which is gitignored,
17
+ and it is never committed, packaged, or transmitted anywhere.
18
+ - prompts are stored as a normalised fingerprint, not verbatim text, so a lab
19
+ description containing names or addresses does not end up on disk.
20
+ - the file is bounded. Old entries are dropped once the cap is reached.
21
+ - a corrupt or unreadable ledger is ignored, never fatal. Learning is an
22
+ optimisation; the skill must work identically with the ledger deleted.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import re
31
+ from collections import defaultdict
32
+ from dataclasses import dataclass, field
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+
36
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
37
+ DEFAULT_LEDGER_PATH = SKILL_ROOT / "output" / "usage-ledger.jsonl"
38
+ MAX_ENTRIES = 2000
39
+ LEDGER_VERSION = 1
40
+
41
+ OUTCOME_GENERATED_VERIFIED = "generated_verified"
42
+ OUTCOME_GENERATED_UNVERIFIED = "generated_unverified"
43
+ OUTCOME_REFUSED = "refused"
44
+ OUTCOMES = (OUTCOME_GENERATED_VERIFIED, OUTCOME_GENERATED_UNVERIFIED, OUTCOME_REFUSED)
45
+
46
+ # Outcomes that count as evidence a donor works, best first.
47
+ _SUCCESS_WEIGHT = {
48
+ OUTCOME_GENERATED_VERIFIED: 3,
49
+ OUTCOME_GENERATED_UNVERIFIED: 1,
50
+ }
51
+
52
+
53
+ def ledger_path() -> Path:
54
+ override = os.getenv("PKT_USAGE_LEDGER")
55
+ return Path(override).expanduser() if override else DEFAULT_LEDGER_PATH
56
+
57
+
58
+ def ledger_enabled() -> bool:
59
+ """Learning is on by default; `PKT_USAGE_LEDGER=off` disables it entirely."""
60
+ return (os.getenv("PKT_USAGE_LEDGER") or "").strip().lower() not in {"off", "0", "false", "none"}
61
+
62
+
63
+ def prompt_fingerprint(prompt: str) -> str:
64
+ """A stable, non-reversible fingerprint of a prompt's *shape*.
65
+
66
+ Digits are collapsed to `#` and case and spacing are normalised, so
67
+ "3 switch 6 pc" and "5 switch 2 pc" share a fingerprint: they are the same
68
+ kind of request, which is exactly the granularity donor reuse needs. The
69
+ result is hashed so no prompt text is ever written to disk.
70
+ """
71
+ normalised = re.sub(r"\d+", "#", (prompt or "").strip().lower())
72
+ normalised = re.sub(r"[^\w#]+", " ", normalised).strip()
73
+ return hashlib.sha256(normalised.encode("utf-8")).hexdigest()[:16]
74
+
75
+
76
+ @dataclass
77
+ class LedgerEntry:
78
+ scenario_family: str
79
+ donor: str
80
+ outcome: str
81
+ prompt_shape: str = ""
82
+ target_version: str = ""
83
+ donor_version: str = ""
84
+ rejected_donors: list[str] = field(default_factory=list)
85
+ rejection_codes: list[str] = field(default_factory=list)
86
+ recorded_at: str = ""
87
+
88
+ def to_json(self) -> dict[str, object]:
89
+ return {
90
+ "v": LEDGER_VERSION,
91
+ "recorded_at": self.recorded_at or datetime.now(timezone.utc).isoformat(timespec="seconds"),
92
+ "scenario_family": self.scenario_family,
93
+ "prompt_shape": self.prompt_shape,
94
+ "donor": self.donor,
95
+ "donor_version": self.donor_version,
96
+ "target_version": self.target_version,
97
+ "outcome": self.outcome,
98
+ "rejected_donors": self.rejected_donors[:20],
99
+ "rejection_codes": self.rejection_codes[:20],
100
+ }
101
+
102
+
103
+ def record(entry: LedgerEntry, path: Path | None = None) -> bool:
104
+ """Append one outcome. Returns False if the ledger is off or unwritable."""
105
+ if not ledger_enabled():
106
+ return False
107
+ if entry.outcome not in OUTCOMES:
108
+ raise ValueError(f"unknown outcome: {entry.outcome}")
109
+
110
+ target = path or ledger_path()
111
+ try:
112
+ target.parent.mkdir(parents=True, exist_ok=True)
113
+ with target.open("a", encoding="utf-8") as handle:
114
+ handle.write(json.dumps(entry.to_json(), ensure_ascii=False) + "\n")
115
+ except OSError:
116
+ return False
117
+
118
+ _trim(target)
119
+ return True
120
+
121
+
122
+ def _trim(path: Path) -> None:
123
+ try:
124
+ lines = path.read_text(encoding="utf-8").splitlines()
125
+ except OSError:
126
+ return
127
+ if len(lines) <= MAX_ENTRIES:
128
+ return
129
+ try:
130
+ path.write_text("\n".join(lines[-MAX_ENTRIES:]) + "\n", encoding="utf-8")
131
+ except OSError:
132
+ return
133
+
134
+
135
+ def load_entries(path: Path | None = None) -> list[dict[str, object]]:
136
+ """Read the ledger. A damaged file yields whatever lines still parse."""
137
+ target = path or ledger_path()
138
+ if not ledger_enabled() or not target.exists():
139
+ return []
140
+ entries: list[dict[str, object]] = []
141
+ try:
142
+ raw_lines = target.read_text(encoding="utf-8").splitlines()
143
+ except OSError:
144
+ return []
145
+ for line in raw_lines:
146
+ line = line.strip()
147
+ if not line:
148
+ continue
149
+ try:
150
+ parsed = json.loads(line)
151
+ except json.JSONDecodeError:
152
+ continue
153
+ if isinstance(parsed, dict) and parsed.get("donor"):
154
+ entries.append(parsed)
155
+ return entries
156
+
157
+
158
+ def donor_scores(
159
+ scenario_family: str,
160
+ prompt_shape: str = "",
161
+ path: Path | None = None,
162
+ ) -> dict[str, int]:
163
+ """Learned preference per donor, as `relative_path -> score`.
164
+
165
+ Positive means the donor has produced output for this kind of request
166
+ before; negative means it has been rejected. An exact prompt-shape match
167
+ counts double, because it is stronger evidence than family alone.
168
+ """
169
+ scores: dict[str, int] = defaultdict(int)
170
+ for entry in load_entries(path):
171
+ if str(entry.get("scenario_family") or "") != scenario_family:
172
+ continue
173
+ multiplier = 2 if prompt_shape and entry.get("prompt_shape") == prompt_shape else 1
174
+
175
+ donor = str(entry.get("donor") or "")
176
+ weight = _SUCCESS_WEIGHT.get(str(entry.get("outcome") or ""), 0)
177
+ if donor and weight:
178
+ scores[donor] += weight * multiplier
179
+
180
+ for rejected in entry.get("rejected_donors") or []:
181
+ name = str(rejected)
182
+ if name:
183
+ scores[name] -= multiplier
184
+ return dict(scores)
185
+
186
+
187
+ def summary(path: Path | None = None) -> dict[str, object]:
188
+ """Human-facing view of what the skill has learned so far."""
189
+ entries = load_entries(path)
190
+ by_outcome: dict[str, int] = defaultdict(int)
191
+ by_family: dict[str, int] = defaultdict(int)
192
+ proven: dict[str, int] = defaultdict(int)
193
+ for entry in entries:
194
+ outcome = str(entry.get("outcome") or "")
195
+ by_outcome[outcome] += 1
196
+ by_family[str(entry.get("scenario_family") or "unknown")] += 1
197
+ if outcome in _SUCCESS_WEIGHT:
198
+ proven[str(entry.get("donor") or "")] += _SUCCESS_WEIGHT[outcome]
199
+ return {
200
+ "ledger_path": str(path or ledger_path()),
201
+ "enabled": ledger_enabled(),
202
+ "entry_count": len(entries),
203
+ "outcomes": dict(by_outcome),
204
+ "scenario_families": dict(by_family),
205
+ "proven_donors": sorted(
206
+ ({"donor": donor, "score": score} for donor, score in proven.items() if score > 0),
207
+ key=lambda item: (-int(item["score"]), str(item["donor"])),
208
+ )[:10],
209
+ }
210
+
211
+
212
+ def main() -> int:
213
+ print(json.dumps(summary(), ensure_ascii=False, indent=2))
214
+ return 0
215
+
216
+
217
+ if __name__ == "__main__":
218
+ raise SystemExit(main())
@@ -1,58 +1,65 @@
1
- # Twofish Bridge Setup
1
+ # Twofish Engines
2
2
 
3
- This repository does not ship a prebuilt Twofish bridge binary by default.
3
+ The Packet Tracer `.pkt` codec uses Twofish in EAX mode. Two engines live here.
4
4
 
5
- The Packet Tracer `.pkt` codec needs a local Twofish bridge at runtime for
6
- modern Packet Tracer 9.x encode/decode operations.
5
+ ## `twofish_pure.py` — the baseline (always available)
7
6
 
8
- ## Supported loading paths
7
+ A vendored pure-Python Twofish. Twofish is unpatented and uncopyrighted by
8
+ design, so it can be implemented and shipped directly.
9
9
 
10
- The wrapper in `twofish.py` loads the bridge from one of these locations:
10
+ - no compiled artifacts, no environment variables, no per-host setup
11
+ - works on any supported Python (3.10+) and any OS
12
+ - verified at import-time diagnostics against the three official test vectors
13
+ from section B.2 of the Twofish book (128/192/256-bit)
14
+ - bit-identical to the compiled bridge; `tests/test_twofish_pure.py` asserts
15
+ this whenever both are present
11
16
 
12
- 1. `PKT_TWOFISH_LIBRARY`
13
- 2. directories listed in `PKT_TWOFISH_SEARCH_ROOTS`
14
- 3. a sibling file in this folder named like:
15
- - `_twofish*.pyd`
16
- - `_twofish*.so`
17
- - `_twofish*.dylib`
18
- - `_twofish*.dll`
17
+ This is what `pkt_codec` uses unless a compiled bridge is found. Nothing has to
18
+ be installed for `decode`, `inventory`, `edit`, or `generate` to work.
19
+
20
+ Cost: roughly 14 µs per block. A typical 280 KB lab decodes in under a second;
21
+ the largest labs seen so far (~2.8 MB) take about 12 seconds.
22
+
23
+ ## `twofish.py` — the optional accelerator
24
+
25
+ A ctypes wrapper around a compiled `_twofish` C library. Roughly 12x faster than
26
+ the pure engine, and worth setting up if you routinely process large labs or
27
+ scan the whole sample corpus. It is never required.
19
28
 
20
- ## Recommended public-repo workflow
29
+ The wrapper loads the bridge from, in order:
21
30
 
22
- - keep this repository free of prebuilt machine-specific binaries
23
- - keep the bridge local to your machine
24
- - preferred: place the bridge next to `scripts/vendor/twofish.py` inside the installed skill folder
25
- - optional: store the bridge elsewhere and point `PKT_TWOFISH_LIBRARY` at that local file
26
- - optional: store bridges in one or more local directories and set `PKT_TWOFISH_SEARCH_ROOTS`
31
+ 1. `PKT_TWOFISH_LIBRARY`
32
+ 2. directories listed in `PKT_TWOFISH_SEARCH_ROOTS`
33
+ 3. a sibling file in this folder named like `_twofish*.pyd` / `.so` / `.dylib` / `.dll`
27
34
 
28
35
  Example on Windows:
29
36
 
30
37
  ```powershell
31
- $env:PKT_TWOFISH_LIBRARY="$env:USERPROFILE\.codex\skills\pkt\scripts\vendor\_twofish.cp314-win_amd64.pyd"
38
+ $env:PKT_TWOFISH_LIBRARY="C:\path\to\_twofish.cp314-win_amd64.pyd"
32
39
  ```
33
40
 
34
- Example for multiple local search roots:
41
+ The compiled bridge is ABI-locked to one Python version. The current filename
42
+ contract is `_twofish.cp314-win_amd64.pyd` (macOS `_twofish.cp314-macos*.dylib`,
43
+ Linux `_twofish.cp314-linux*.so`). On any other Python version the accelerator is
44
+ skipped and the pure engine is used instead — this is not an error.
35
45
 
36
- ```powershell
37
- $env:PKT_TWOFISH_SEARCH_ROOTS="$env:USERPROFILE\.codex\skills\pkt\scripts\vendor;$env:USERPROFILE\pkt-bridges"
38
- ```
46
+ ## Which engine am I using?
39
47
 
40
- ## Supported runtime
48
+ ```bash
49
+ python scripts/runtime_doctor.py
50
+ ```
41
51
 
42
- - supported Python runtime: `3.14.x`
43
- - current bridge filename: `_twofish.cp314-win_amd64.pyd`
44
- - recommended non-Windows naming contract:
45
- - macOS: `_twofish.cp314-macos*.dylib`
46
- - Linux: `_twofish.cp314-linux*.so`
47
- - other Python ABIs are not considered supported by this public setup
52
+ Read `twofish_backend`: `pure_python` or `compiled`.
48
53
 
49
54
  ## Security and privacy
50
55
 
51
- - do not commit machine-specific binaries unless you have reviewed them
52
- - do not commit binaries that embed private paths, usernames, or internal build metadata
53
- - prefer rebuilding or sourcing the bridge in a reproducible way for your own machine
56
+ - this repository ships no prebuilt machine-specific binaries
57
+ - do not commit binaries that embed private paths, usernames, or build metadata
58
+ - prefer rebuilding the bridge reproducibly for your own machine
54
59
 
55
- ## Failure mode
60
+ ## Licensing
56
61
 
57
- If the bridge is missing, the wrapper raises an `ImportError` with setup guidance
58
- instead of silently loading a repo-shipped binary.
62
+ `twofish_pure.py` is original work implemented from the published Twofish
63
+ specification and is covered by this repository's licence. `twofish.py` derives
64
+ from the BSD-3-Clause Python Twofish ctypes bindings; see
65
+ `LICENSES/LICENSE.Twofish-BSD-3-Clause.txt`.