packet-tracer-skill 0.2.3 → 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 -73
- package/README.md +557 -442
- package/SKILL.md +337 -262
- package/bin/packet-tracer-skill.js +29 -2
- 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/l2-security-qos-proof.md +1 -1
- package/docs/packet-tracer-feature-gap-atlas.md +4 -4
- 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 +17 -8
- package/docs/release-notes-0.2.4.md +20 -0
- package/docs/runtime-truth.md +33 -8
- package/docs/security-edge-deepening-proof.md +1 -1
- 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 -53
- package/pytest.ini +9 -0
- 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 -1812
- package/scripts/donor_cache.py +354 -0
- package/scripts/donor_diagnostics.py +3 -1
- package/scripts/generate_pkt.py +8762 -4228
- package/scripts/intent_parser.py +2242 -1657
- 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 -1703
- package/scripts/pkt_transformer.py +1072 -727
- package/scripts/pkt_verify.py +461 -0
- package/scripts/runtime_doctor.py +80 -29
- package/scripts/sample_catalog.py +1372 -1250
- 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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Notes, frames and shapes on the logical workspace.
|
|
3
|
+
|
|
4
|
+
Packet Tracer's drawing tools are part of what makes a large lab readable: a
|
|
5
|
+
labelled frame around each building, a coloured box behind the server farm, a
|
|
6
|
+
note explaining what the topology demonstrates. None of that was reachable from
|
|
7
|
+
this skill.
|
|
8
|
+
|
|
9
|
+
Formats measured from Cisco's own bundled labs rather than guessed:
|
|
10
|
+
|
|
11
|
+
<RECTANGLE> <NOTE>
|
|
12
|
+
<TopLeftX>64</TopLeftX> <X>547</X>
|
|
13
|
+
<TopLeftY>174</TopLeftY> <Y>329</Y>
|
|
14
|
+
<BottomRightX>546</BottomRightX> <Z>40001</Z>
|
|
15
|
+
<BottomRightY>397</BottomRightY> <TEXT translate="true">...</TEXT>
|
|
16
|
+
<Color> <NOTECLUSTERID>1-1</NOTECLUSTERID>
|
|
17
|
+
<Red>0</Red> </NOTE>
|
|
18
|
+
<Green>0</Green>
|
|
19
|
+
<Blue>0</Blue>
|
|
20
|
+
</Color>
|
|
21
|
+
<Filled>0</Filled>
|
|
22
|
+
<RECTCLUSTERID>1-1</RECTCLUSTERID>
|
|
23
|
+
</RECTANGLE>
|
|
24
|
+
|
|
25
|
+
`Color` is the outline; `Filled` decides whether the interior is painted with
|
|
26
|
+
it. Ellipses share the shape exactly, with `ELLIPSECLUSTERID`. The containers --
|
|
27
|
+
`RECTANGLES`, `ELLIPSES`, `LINES`, `NOTES` -- sit directly under the document
|
|
28
|
+
root, not inside a device.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import uuid as _uuid
|
|
34
|
+
import xml.etree.ElementTree as ET
|
|
35
|
+
|
|
36
|
+
# Cluster ids group annotations with a logical-workspace cluster. `1-1` is the
|
|
37
|
+
# root cluster, which is what an unclustered lab uses throughout.
|
|
38
|
+
ROOT_CLUSTER = "1-1"
|
|
39
|
+
|
|
40
|
+
# A small named palette, because "qirmizi" is easier to write than an RGB triple.
|
|
41
|
+
COLORS: dict[str, tuple[int, int, int]] = {
|
|
42
|
+
"black": (0, 0, 0),
|
|
43
|
+
"white": (255, 255, 255),
|
|
44
|
+
"red": (220, 60, 60),
|
|
45
|
+
"green": (60, 180, 90),
|
|
46
|
+
"blue": (70, 120, 220),
|
|
47
|
+
"yellow": (250, 215, 80),
|
|
48
|
+
"orange": (245, 150, 60),
|
|
49
|
+
"purple": (160, 100, 210),
|
|
50
|
+
"grey": (150, 150, 150),
|
|
51
|
+
"gray": (150, 150, 150),
|
|
52
|
+
"lightblue": (170, 170, 255),
|
|
53
|
+
"lightgreen": (190, 235, 200),
|
|
54
|
+
"lightyellow": (255, 245, 190),
|
|
55
|
+
}
|
|
56
|
+
# Azerbaijani names for the same palette, so a prompt can say `qirmizi cerceve`.
|
|
57
|
+
COLORS.update({
|
|
58
|
+
"qara": COLORS["black"], "ag": COLORS["white"], "qirmizi": COLORS["red"],
|
|
59
|
+
"yasil": COLORS["green"], "mavi": COLORS["blue"], "sari": COLORS["yellow"],
|
|
60
|
+
"narinci": COLORS["orange"], "benovseyi": COLORS["purple"], "boz": COLORS["grey"],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_color(name: str | tuple[int, int, int] | None) -> tuple[int, int, int]:
|
|
65
|
+
"""A colour name, an RGB triple, or black."""
|
|
66
|
+
if isinstance(name, (tuple, list)) and len(name) == 3:
|
|
67
|
+
return tuple(max(0, min(255, int(part))) for part in name) # type: ignore[return-value]
|
|
68
|
+
return COLORS.get(str(name or "").strip().lower(), COLORS["black"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _tag_with_uuid(parent: ET.Element, tag: str) -> ET.Element:
|
|
72
|
+
"""Create an annotation element carrying the identifier Packet Tracer uses.
|
|
73
|
+
|
|
74
|
+
Packet Tracer stamps every shape it saves with a braced version-4 `uuid`.
|
|
75
|
+
Ours opened without one, but matching what the application writes keeps a
|
|
76
|
+
generated file indistinguishable from a hand-drawn one after a round trip.
|
|
77
|
+
"""
|
|
78
|
+
node = ET.SubElement(parent, tag)
|
|
79
|
+
node.set("uuid", "{" + str(_uuid.uuid4()) + "}")
|
|
80
|
+
return node
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _container(root: ET.Element, tag: str) -> ET.Element:
|
|
84
|
+
"""The document-level container for a kind of annotation, created if absent."""
|
|
85
|
+
node = root.find(tag)
|
|
86
|
+
if node is None:
|
|
87
|
+
node = ET.SubElement(root, tag)
|
|
88
|
+
return node
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _filled_node(parent: ET.Element, filled: bool, outline: str | tuple[int, int, int] | None) -> None:
|
|
92
|
+
"""`Filled` also carries the outline, which is a separate colour.
|
|
93
|
+
|
|
94
|
+
Packet Tracer writes `<Filled OUTLINECOLOR="#000000" OUTLINED="false">1</Filled>`,
|
|
95
|
+
so a painted shape can have a border in a different colour -- something the
|
|
96
|
+
first version of this file had no way to express.
|
|
97
|
+
"""
|
|
98
|
+
node = ET.SubElement(parent, "Filled")
|
|
99
|
+
node.text = "1" if filled else "0"
|
|
100
|
+
if outline is not None:
|
|
101
|
+
red, green, blue = resolve_color(outline)
|
|
102
|
+
node.set("OUTLINECOLOR", f"#{red:02x}{green:02x}{blue:02x}")
|
|
103
|
+
node.set("OUTLINED", "true")
|
|
104
|
+
else:
|
|
105
|
+
node.set("OUTLINECOLOR", "#000000")
|
|
106
|
+
node.set("OUTLINED", "false")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _color_node(parent: ET.Element, color: tuple[int, int, int]) -> None:
|
|
110
|
+
node = ET.SubElement(parent, "Color")
|
|
111
|
+
for channel, value in zip(("Red", "Green", "Blue"), color):
|
|
112
|
+
ET.SubElement(node, channel).text = str(int(value))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def add_rectangle(
|
|
116
|
+
root: ET.Element,
|
|
117
|
+
top_left: tuple[float, float],
|
|
118
|
+
bottom_right: tuple[float, float],
|
|
119
|
+
*,
|
|
120
|
+
color: str | tuple[int, int, int] = "black",
|
|
121
|
+
filled: bool = False,
|
|
122
|
+
outline: str | tuple[int, int, int] | None = None,
|
|
123
|
+
) -> ET.Element:
|
|
124
|
+
"""A frame. `filled` paints the interior; `outline` borders it separately."""
|
|
125
|
+
rectangles = _container(root, "RECTANGLES")
|
|
126
|
+
rectangle = _tag_with_uuid(rectangles, "RECTANGLE")
|
|
127
|
+
ET.SubElement(rectangle, "TopLeftX").text = str(top_left[0])
|
|
128
|
+
ET.SubElement(rectangle, "TopLeftY").text = str(top_left[1])
|
|
129
|
+
ET.SubElement(rectangle, "BottomRightX").text = str(bottom_right[0])
|
|
130
|
+
ET.SubElement(rectangle, "BottomRightY").text = str(bottom_right[1])
|
|
131
|
+
_color_node(rectangle, resolve_color(color))
|
|
132
|
+
_filled_node(rectangle, filled, outline)
|
|
133
|
+
ET.SubElement(rectangle, "RECTCLUSTERID").text = ROOT_CLUSTER
|
|
134
|
+
return rectangle
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def add_ellipse(
|
|
138
|
+
root: ET.Element,
|
|
139
|
+
top_left: tuple[float, float],
|
|
140
|
+
bottom_right: tuple[float, float],
|
|
141
|
+
*,
|
|
142
|
+
color: str | tuple[int, int, int] = "black",
|
|
143
|
+
filled: bool = False,
|
|
144
|
+
outline: str | tuple[int, int, int] | None = None,
|
|
145
|
+
) -> ET.Element:
|
|
146
|
+
ellipses = _container(root, "ELLIPSES")
|
|
147
|
+
ellipse = _tag_with_uuid(ellipses, "ELLIPSE")
|
|
148
|
+
ET.SubElement(ellipse, "TopLeftX").text = str(top_left[0])
|
|
149
|
+
ET.SubElement(ellipse, "TopLeftY").text = str(top_left[1])
|
|
150
|
+
ET.SubElement(ellipse, "BottomRightX").text = str(bottom_right[0])
|
|
151
|
+
ET.SubElement(ellipse, "BottomRightY").text = str(bottom_right[1])
|
|
152
|
+
_color_node(ellipse, resolve_color(color))
|
|
153
|
+
_filled_node(ellipse, filled, outline)
|
|
154
|
+
ET.SubElement(ellipse, "ELLIPSECLUSTERID").text = ROOT_CLUSTER
|
|
155
|
+
return ellipse
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def add_line(
|
|
159
|
+
root: ET.Element,
|
|
160
|
+
start: tuple[float, float],
|
|
161
|
+
end: tuple[float, float],
|
|
162
|
+
*,
|
|
163
|
+
color: str | tuple[int, int, int] = "black",
|
|
164
|
+
) -> ET.Element:
|
|
165
|
+
"""A straight line.
|
|
166
|
+
|
|
167
|
+
A line does not use the rectangle's corner fields. Writing it that way
|
|
168
|
+
produced lines that never appeared: Packet Tracer read the file, rewrote
|
|
169
|
+
every one into `StartX/StartY/EndX/EndY`, and dropped them from the view.
|
|
170
|
+
The real shape came from a lab where the drawing palette had been used.
|
|
171
|
+
"""
|
|
172
|
+
lines = _container(root, "LINES")
|
|
173
|
+
line = _tag_with_uuid(lines, "LINE")
|
|
174
|
+
ET.SubElement(line, "StartX").text = str(start[0])
|
|
175
|
+
ET.SubElement(line, "StartY").text = str(start[1])
|
|
176
|
+
ET.SubElement(line, "EndX").text = str(end[0])
|
|
177
|
+
ET.SubElement(line, "EndY").text = str(end[1])
|
|
178
|
+
_color_node(line, resolve_color(color))
|
|
179
|
+
ET.SubElement(line, "LINECLUSTERID").text = ROOT_CLUSTER
|
|
180
|
+
return line
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def add_note(root: ET.Element, position: tuple[float, float], text: str, *, z: int = 40001) -> ET.Element:
|
|
184
|
+
"""A text note. `Z` sits above the devices so the note stays readable.
|
|
185
|
+
|
|
186
|
+
The container name is misleading: notes shown on the *logical* workspace are
|
|
187
|
+
stored under `PHYSICALWORKSPACE/NOTES`. Written at the document root instead,
|
|
188
|
+
Packet Tracer moved them and emptied the text, leaving notes parked at the
|
|
189
|
+
50000,50000 sentinel with nothing in them.
|
|
190
|
+
"""
|
|
191
|
+
workspace = root.find("PHYSICALWORKSPACE")
|
|
192
|
+
if workspace is None:
|
|
193
|
+
workspace = ET.SubElement(root, "PHYSICALWORKSPACE")
|
|
194
|
+
notes = workspace.find("NOTES")
|
|
195
|
+
if notes is None:
|
|
196
|
+
notes = ET.SubElement(workspace, "NOTES")
|
|
197
|
+
note = _tag_with_uuid(notes, "NOTE")
|
|
198
|
+
ET.SubElement(note, "X").text = str(position[0])
|
|
199
|
+
ET.SubElement(note, "Y").text = str(position[1])
|
|
200
|
+
ET.SubElement(note, "Z").text = str(z)
|
|
201
|
+
text_node = ET.SubElement(note, "TEXT")
|
|
202
|
+
text_node.set("translate", "true")
|
|
203
|
+
text_node.text = text
|
|
204
|
+
ET.SubElement(note, "NOTECLUSTERID").text = ROOT_CLUSTER
|
|
205
|
+
return note
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def clear_annotations(root: ET.Element) -> None:
|
|
209
|
+
"""Drop every annotation, so a regenerated lab does not inherit stale ones."""
|
|
210
|
+
for tag in ("RECTANGLES", "ELLIPSES", "LINES", "POLYGONS"):
|
|
211
|
+
node = root.find(tag)
|
|
212
|
+
if node is not None:
|
|
213
|
+
root.remove(node)
|
|
214
|
+
workspace = root.find("PHYSICALWORKSPACE")
|
|
215
|
+
if workspace is not None:
|
|
216
|
+
notes = workspace.find("NOTES")
|
|
217
|
+
if notes is not None:
|
|
218
|
+
workspace.remove(notes)
|