Commit Graph
218 Commits
Author SHA1 Message Date
YueGuobin 4de6758e2f refactor(mcp): drop the raw drawing_create tool
The templated drawing_create_text/_rectangle/_ellipse tools cover the
LLM-relevant creation paths, and line/image/curve creation is not
needed from MCP or copilot for now. The management tools stay, so
existing drawings (including images and lines created in the Web UI)
remain fully manageable; drawing_update's raw svg parameter remains
the escape hatch for edits.
2026-10-09 11:12:18 +08:00
YueGuobin 5e8272fdd8 feat(agent): share Web UI compatible drawing builders between MCP and copilot
The text drawing tool was MCP-only; the copilot needs the same
capability and its area-annotation helper carried the same Web UI
compat bugs. Split into three layers with a single implementation:

- gns3server/utils/drawings.py: pure SVG builders (text, rectangle,
  ellipse) with full input validation (ValueError) — the Web UI
  format knowledge lives in core utils next to picture.py
- api_handlers.py: shared REST handlers (create_text_drawing_handler
  moved here, create_rectangle/ellipse_drawing_handler added) — the
  layer both consumers already share
- thin shells: MCP tools (drawing_create_text unchanged, plus new
  drawing_create_rectangle/_ellipse) and three LangChain tools
  (tools_v2/gns3_create_drawing.py) registered in both copilot mode
  lists

Shape builder details:

- rectangle: solid fill or "none", stroke block emitted only when a
  stroke is given, rx corner radius, dasharray validated against the
  Web UI's Qt remap table (QtDasharrayFixer rewrites those patterns)
- ellipse: bounding-box API; cx/cy derived as offsets inside the
  drawing box (the Web UI treats them as box-relative, not canvas
  coordinates); odd sizes round down and the actual size is returned

gns3_drawing_utils now delegates to the shared builders, fixing the
px-blind sizing, the ignored text-anchor/x/y centering and the
missing XML escaping in the area-annotation path.
2026-10-09 11:12:18 +08:00
YueGuobin 73cbb0387c feat(mcp): add drawing_create_text tool with Web UI compatible SVG assembly
The Web UI re-parses drawing SVG into its own element model instead of
rendering it raw, which makes hand-assembled SVG error-prone for LLM
callers. The new tool assembles the SVG server-side, mirroring what the
Web UI itself serializes for text drawings:

- XML-escaped text content, validated color (#RGB/#RGBA/#RRGGBB/
  #RRGGBBAA) and font family (attribute-safe charset)
- multi-line support: literal newlines render as stacked lines in the
  Web UI (tspan split), width/height computed from the longest line
- pt-aware sizing: the Web UI renders font-size in pt (1pt = 4/3 px),
  so width uses a 0.8/char factor for monospace (1.0 proportional) and
  line height 1.9 x font_size, instead of the px-based 0.62 estimate
- ET.fromstring pre-check so bad input fails loudly instead of hitting
  the controller's silent drop of unparseable 500+ char SVG
- computed width/height/line_height returned for label layout

Also correct the raw drawing_create docstring: root width/height are
required (the parser reads them for the selection box), x/y on rect are
ignored, <g> is unsupported, <path> is re-interpreted as a freehand
curve, and the old examples lacked the mandatory root dimensions.
2026-10-09 11:12:18 +08:00
YueGuobin 1e0d123f4b Merge branch '3.1' into fix/duplicate-device-configs-merge
Resolve conflicts against the upstream wave (PRs #2952-#2965, incl. the
typed-response/schema hardening and the drop of Python 3.10):

- controller/node.py: keep upstream's _transitioning/_confirm_status
  lifecycle guard; the kernel-anchor link wiring (node_started) runs
  after the transition completes.
- controller/link.py: keep upstream's add_node rollback (release_ports
  on create failure) and delete via release_ports; release_ports now
  resolves ports live (same staleness reason as _bind_members) and
  discards the node link unconditionally.
- schemas/controller/links.py: extend LinkFilterType with the eight
  kernel-datapath filter types and LinkFilterParameter.type with "str"
  so the newly typed /available_filters response validates; Link.filters
  stays a free-form dict — the vocabulary is capability-gated per link
  and validated by the controller (409), not by a strict request schema.
- schemas/controller/topology.py: drop the type: ignore made redundant
  by the dict filters field.
- tests: route-level filter validation now expects the controller's 409
  (unknown types, out-of-range values); packet_loss correlation is a
  valid kernel parameter (positive test added); OpenAPI assertions match
  the free-form filters property.
2026-10-09 10:03:50 +08:00
grossmj 2642345b0a fix: import errors 2026-10-08 15:33:53 +02:00
grossmj 811b45f863 fix: drop Python 3.10 and add Python 3.15 support 2026-10-08 15:14:34 +02:00
YueGuobin d640050d4b docs(features): quantify the kernel netem correl bias that collapses small rates
The kernel's correlated-random generator (get_crandom, sch_netem.c) truncates
instead of wrapping, starving both edges of the stationary distribution: an
event threshold below the correlation value is almost never reached. Measured
with stock iproute2 on this kernel — uBridge's netlink encoding is identical:
loss 1% + correl 25% drops nothing over 2000 pings (expected ~2%), 10%/25%
measures ~1.8% one way, only the 50% symmetry point survives. duplicate and
reorder correl share the same comparison structure and collapse the same way;
gemodel is immune (Markov chain over plain uniform draws) and is the honest
tool for bursty loss at a chosen mean rate.

Documented in the packet-filters correlation-bias bullet, and surfaced in the
two machine-facing descriptions (MCP link_update packet_loss note, controller
filter schema) so agents and clients stop walking into the trap.
2026-10-08 19:42:18 +08:00
YueGuobin c3c025ab2a fix: resolve the code-review findings on the kernel datapath
Compute:
- Track the anchor that owns uBridge's single AF_PACKET capture slot: a
  second kernel port's capture is refused with a clear error (its flag
  rolled back), a failed start no longer wedges the port, and only the
  owning port's stop issues the process-wide `capture stop_kernel`
  (docker/qemu/iou/dynamips/switch, via the mixin's slot helpers).
- Unwind failed starts instead of leaking uBridge and the anchor TAPs:
  QEMU's stop body runs under try/finally, QEMU and IOU start the datapath
  inside the launch try, Dynamips undoes a half-attached port (the retry
  used to be short-circuited into a silent black hole), and Docker deletes
  the veth pair when the namespace move fails.
- Restore the relay filter guard on IOU (kernel-only filters raise
  NodeError, an uncompilable BPF line degrades to a warning), validate the
  eBPF/quota/window/bpf filter shapes before touching the bridge (short or
  non-numeric values surfaced as 500s after partial state), and refuse a
  second NIO on an occupied Docker/QEMU port (the guard IOU and Dynamips
  already had).
- Probe fixes: `_probe_bridge_tap` survives a failing uBridge start, and
  the probe caches key on ctime too, so a chmod/setcap remedy re-probes.

Controller:
- Parse GNS3_UNIX_SOCKET_NIO like the compute does (line-anchored, truthy
  value) and mirror the compute's class selection in kernel_anchor_type.
- Read partial capability reports defensively; make BridgeNIO.bridge
  required-but-nullable so an omitted key is a 422 rather than a KeyError.
- Strip the packet_loss correlation slot even when it is zero: the relay
  filter takes exactly one argument and rejected `packet_loss 10 0`.

Ethernet switch: skip ports whose absorbed anchor does not exist yet (the
join is deferred by design) and roll the ports_mapping back when the VLAN
reconcile fails, so a retry still diffs against the old mapping.

Agent: reject device config entries without a device_name instead of
silently dropping them (a malformed batch used to execute partially while
reporting success).

Also split the netem builder to clear C901 and drop the two per-file ruff
ignores the PR had added, keeping the remove-only policy.
2026-10-06 19:55:21 +08:00
YueGuobin 7d04d08dba fix: repair imports lost in the 3.1 rebase and meet the new lint baseline
The rebase onto upstream 3.1 (mypy + ruff I001/F401 waves) left the tree
short of the newly enforced baselines in three ways:

* contextlib / UbridgeError imports dropped as false duplicates during
  conflict resolution left NameErrors in iou_vm and qemu_vm (F821);
  the Dynamips router grabbed the compute-level NIOTAP instead of the
  hypervisor one, breaking TAP anchoring (test failure).
* unused imports (KERNEL_UNSUPPORTED_FILTERS, UbridgeError, List) and
  unsorted blocks left behind where the stack's later commits moved the
  last usage elsewhere — ruff I001/F401/RUF022 now enforce these.
* mypy, now baseline-free: cache dict annotations in tc_probe, the
  locators candidate list, the resolved-params None branch in the MCP
  runner, and validate_bpf_syntax keeps the upstream Dict[str, Any]
  return (the strict Optional[str] was wrong for the "valid" bool entry).

The e2e raw-frame probe resolves its compiler via shutil.which (S607)
and zips with strict= (B905); the remaining subprocess call is S603 by
nature and gets a per-file ignore like other test files.

ruff check/format clean, mypy at upstream parity (no new errors),
full unit suite 2167 passed.
2026-10-06 00:21:11 +08:00
YueGuobin e13e5f3804 fix(tools): merge same-device entries before device command execution
A batch carrying the same device twice lost all but the last entry's
commands: the per-device command map kept only the last entry while
result processing iterated every original entry, so each row reported
the last entry's output under its own command list.

c3c78f99a consolidated the merge into the MCP template renderer, but
that only runs when a template is passed — the MCP direct-commands
path and gns3-copilot's direct _run path were left uncovered.
Normalize once in each tool's _run (single point covering all entry
paths, restoring the c42b3a59b coverage) and keep the renderer merge
as boundary normalization. Blocked-command info now also accumulates
across same-device entries instead of last-winning in the per-device
map.
2026-10-06 00:21:11 +08:00
YueGuobin 6633304e97 fix(mcp): let link_list return kernel_datapath
The tool descriptions have always advertised kernel_datapath as a
requestable field, but the handler's whitelist did not contain it: asking
for it was rejected as an invalid field, so the MCP clients could not see
which links run on kernel interfaces at all.

Also record what seeding a netem experiment on IOU costs: IOL emits its own
CDP/keepalive frames and they draw from the netem RNG stream, so a
seeded run only reproduces after `no cdp run` (verified bit-identical pcaps).
2026-10-06 00:21:09 +08:00
YueGuobin 187201a08c feat(controller): offer QEMU the kernel datapath, gated per compute
Kernel eligibility becomes "both endpoints on one compute, each able to
anchor": Docker always (adapters are born as veth pairs), QEMU when that
compute's uBridge can create the persistent TAPs its adapters anchor on. The
question is asked per compute, because the link needs both ends anchored --
so a mixed or half-upgraded deployment degrades to the relay instead of
failing an attach.

probe_tap_support() creates and deletes one throwaway persistent TAP through a
scratch uBridge, cached by binary identity like the tc report; /capabilities
exposes `ubridge_tap` (absent = unknown, which keeps links on the relay).

The compute route for a QEMU adapter NIO took UDPNIO only, so a kernel link
was rejected with 422: its body and response now accept
Union[UDPNIO, BridgeNIO] exactly like Docker's.

Stale "Docker-to-Docker" wording in the config, link and MCP descriptions now
talks about anchors, and docs/features/qemu-kernel-datapath.md documents the
datapath -- verified end-to-end on a real instance: 32/32 kernel-datapath
checks and 29/29 on the relay variant.
2026-10-06 00:21:08 +08:00
YueGuobin 1db9f171ca fix(controller): re-arm window_drop and expose kernel_datapath
Three fixes from live-calibrating the kernel impairment filters on an
isolated instance (Docker-to-Docker veth link, per-sequence evidence):

- update_filters skipped an unchanged filters dict as a no-op, so a
  one-shot window_drop outage that had already passed could never be
  re-armed: re-applying the same values produced no datapath command at
  all. A PUT still carrying window_drop now reconciles, restarting the
  schedule from the apply as documented. Measured: identical re-apply
  moves the outage window from seq 50-70 to 70-91 (arm at +8s of the
  new apply).

- gemodel parameter semantics were documented backwards in the
  validation rules, available_filters, the MCP/copilot descriptions and
  the feature docs. Measured against the kernel (per-direction counters,
  offered = sent + dropped): p is the good-to-bad transition chance, r
  the bad-to-good transition, 1-h the loss chance in the bad state (the
  good state loses nothing) - mean one-way loss p/(p+r)*(1-h), e.g.
  [10,30,100] -> 24.8%/25.7% measured vs 25% predicted, [5,45,100] ->
  10.8% both ways vs 10%.

- kernel_datapath was silently stripped from every REST/MCP link payload
  by the response model (LinkBase carried no such field), so clients
  could not tell a kernel link from a relay one.

Also verified on the same setup: the 3-argument window_drop form works
exactly as specified ([3000,5000,100] -> a single seq 31-55 outage,
[20000,6000,100] -> seq 115-144), and both generations of the command
builder emit the parameters unpadded.

Adds an API test for the field and controller tests for the no-op skip
and the window_drop re-arm exemption.
2026-10-06 00:21:08 +08:00
YueGuobin 5ac1521217 feat(mcp): accept project/node names as tool locators
LLM agents transcribe UUIDs unreliably — a 2-character slip invents an
id that never existed and is invisible to the eye. Tool parameters now
accept names wherever the server guarantees uniqueness: project names
among live projects, node names within a project. Links, drawings,
appliances and snapshots have no names and stay UUID-only.

Resolution sits in the single dispatch choke point (_run_handler_sync):
UUID-shaped values pass through untouched, names resolve with one list
call, and an unknown name fails fast listing the known names instead of
a confusing downstream 404. As a backstop for residual UUID typos,
error replies carrying "<UUID> doesn't exist" are matched against the
in-scope ids; a single candidate within edit distance 2 earns a
"Did you mean ...?" hint.
2026-10-06 00:21:08 +08:00
YueGuobin 935c00802f docs(mcp): refresh filter vocabulary in the MCP/copilot tool descriptions
link_update, link_available_filters and the copilot packet-filter tool
still advertised the pre-kernel-datapath five types. List the full
thirteen with positional parameter formats, ranges and kernel-only
marking, point at link_available_filters as the per-link authority
(topology + compute capability gated), and document window_drop's
apply-relative schedule restart. Also add kernel_datapath to the link
field lists.

Description-only: handlers pass filters through unchanged.
2026-10-06 00:21:08 +08:00
Mark Paronyan b622ccae7b refactor: declare explicit public API in package __init__ modules 2026-10-03 19:43:59 +03:00
Mark Paronyan 306d8fbde4 refactor: remove unused imports 2026-10-03 19:43:58 +03:00
Mark Paronyan 61caa20524 style: sort imports in agent and remaining compute modules 2026-10-02 12:43:26 +03:00
Mark Paronyan 06899c92fc refactor: use iterable unpacking instead of concatenation 2026-10-01 19:57:46 +03:00
Mark Paronyan 2dc7479d11 style: remove f-strings without placeholders 2026-10-01 19:57:46 +03:00
Mark Paronyan c9416d2569 style: remove quotes from type annotations 2026-10-01 19:57:45 +03:00
Mark Paronyan 396407827e style: drop redundant open mode arguments 2026-10-01 19:57:44 +03:00
Mark Paronyan d356a68987 refactor: replace aliased OSError subclasses 2026-10-01 19:57:44 +03:00
Mark Paronyan 0416ad8228 style: sort __all__ declarations 2026-10-01 19:57:42 +03:00
Mark Paronyan c5b5091f5e refactor: use explicit conversion in f-strings 2026-10-01 19:57:41 +03:00
Mark Paronyan 9e5ab02147 style: remove unused noqa directives 2026-10-01 19:57:40 +03:00
Jeremy Grossmann 726da8383a Merge branch '3.1' into mypy-agent-gns3_copilot-plus 2026-09-29 13:20:27 +02:00
Jeremy Grossmann 468ad4e85f Merge branch '3.1' into mypy-agent 2026-09-29 13:19:30 +02:00
Jeremy Grossmann 4bcaf596df Merge pull request #2910 from markparonyan/mypy-agent-copilot-utils
fix(typing): resolve mypy errors in agent.gns3_copilot.utils
2026-09-29 12:48:03 +02:00
Jeremy Grossmann b73128d791 Merge branch '3.1' into mypy-agent-copilot-tools-analysis 2026-09-29 12:45:07 +02:00
Jeremy Grossmann 77a323e1e8 Merge branch '3.1' into mypy-agent-copilot-tools-nornir 2026-09-29 12:38:40 +02:00
Jeremy Grossmann 7b40064658 Merge branch '3.1' into mypy-agent-copilot-skills 2026-09-29 12:30:40 +02:00
Jeremy Grossmann eb9a9ae19a Merge pull request #2904 from markparonyan/mypy-agent-web-wireshark
fix(typing): resolve mypy errors in agent.web_wireshark
2026-09-29 12:27:28 +02:00
Jeremy Grossmann 6aac6c8e34 Merge pull request #2906 from markparonyan/mypy-agent-copilot-client
fix(typing): resolve mypy errors in agent.gns3_copilot.gns3_client
2026-09-29 12:21:33 +02:00
Mark Paronyan 780b7f2ab6 fix(typing): resolve mypy errors in agent node lifecycle tools 2026-09-29 09:46:32 +03:00
Mark Paronyan bc27c4814f fix(typing): resolve mypy errors in agent.gns3_copilot.gns3_client.api_handlers 2026-09-29 09:46:31 +03:00
Mark Paronyan 9882eba088 fix(typing): resolve mypy errors in agent.gns3_copilot.chat_sessions_repository 2026-09-29 09:46:31 +03:00
Mark Paronyan 07560b0026 fix(typing): resolve mypy errors in agent 2026-09-29 09:46:31 +03:00
Mark Paronyan c154d1c41e fix(typing): resolve mypy errors in agent.web_wireshark.docker_client 2026-09-29 09:23:01 +03:00
Mark Paronyan d25cb5cdca fix(typing): resolve mypy errors in agent.mcp.appliances 2026-09-29 09:23:01 +03:00
Mark Paronyan 2d712e0a90 fix(typing): resolve mypy errors in agent.gns3_copilot.utils.custom_netmiko.scripts.list_netmiko_telnet_devices 2026-09-29 09:23:01 +03:00
Mark Paronyan 4ae6b5338d fix(typing): resolve mypy errors in agent.gns3_copilot.utils.parse_tool_content 2026-09-29 08:52:38 +03:00
Mark Paronyan 5cb1b8e3d2 fix(typing): resolve mypy errors in agent.gns3_copilot.utils.message_converters 2026-09-29 08:52:37 +03:00
Mark Paronyan cd7f5ab8e9 fix(typing): resolve mypy errors in agent.gns3_copilot.agent.model_factory 2026-09-29 08:52:33 +03:00
Mark Paronyan a251ca73ac fix(typing): resolve mypy errors in agent.gns3_copilot.agent.context_manager 2026-09-29 08:52:33 +03:00
Mark Paronyan f8144494f6 fix(typing): resolve mypy errors in agent.gns3_copilot.tools_v2.vpcs_tools_netmiko 2026-09-29 08:48:02 +03:00
Mark Paronyan d2fd238383 fix(typing): resolve mypy errors in agent.gns3_copilot.tools_v2.gns3_packet_filter 2026-09-29 08:48:02 +03:00
Mark Paronyan 67f68c9676 fix(typing): resolve mypy errors in agent.gns3_copilot.tools_v2.packet_analysis_tool 2026-09-29 08:48:02 +03:00
Mark Paronyan d6ae597f36 fix(typing): resolve mypy errors in agent.gns3_copilot.skills.manager 2026-09-29 08:48:01 +03:00
Mark Paronyan 35b360ca75 fix(typing): resolve mypy errors in agent.gns3_copilot.tools_v2.display_tools_nornir 2026-09-29 08:48:01 +03:00