The waiting variant blocked on a fixed progress bar (up to 120s) even
when every start command had already failed (e.g. the uBridge >= 1.2.3
409), burying the error behind two minutes of silence. Drop it: the
single start_gns3_node now sends the commands and returns per-node
results immediately (GNS3StartNodeQuickTool kept as an alias). Boot
waiting moves to a dedicated wait_seconds tool the agent calls
explicitly between start and status checks, with a 600s ceiling and
liveness logging every 5s.
The tool descriptions still advertised the removed "short-lived JWT"
and gave no guidance against URL reconstruction. Align them with
node_console's wording: run the returned curl_command verbatim, never
rebuild the URL or copy the ticket by hand, re-call the tool once the
10-minute ticket has expired.
link_capture_download and get_symbol embedded a 10-min JWT in the
Authorization header of the curl command they return; LLM clients
retyping that command corrupted the long token — the same failure
class as the console WebSocket URLs fixed in the previous commit.
Generalize the ticket store (console_tickets.py -> access_tickets.py,
ConsoleTicketService -> AccessTicketService): a ticket now binds to
either a node's console endpoints (WebSocket, matched against route
path params) or one exact REST resource path (capture file, symbol
image). The two binding modes are isolated — a node-bound ticket
cannot authenticate a REST resource and vice versa.
get_user_from_token redeems path-bound tickets through the existing
token parameter / Bearer header, matched exactly against
request.url.path, then reuses the shared user lookup and token_version
revocation checks. Download URLs embed ?token=<ticket> and the curl
commands no longer carry a Bearer header.
LLM clients transcribing the console WebSocket URL into shell commands
reliably corrupted the ~200-char JWT embedded in it (dropped header
segment -> "MissingAlgorithmError: Missing 'alg' value in header" on
every connection attempt). The node_console tool now mints a short
random ticket ("gns3t_" + 16 urlsafe chars, 10 min TTL, multi-use)
stored server-side and bound to the node's console endpoints:
- new ConsoleTicketService (gns3server/services/console_tickets.py),
in-memory store with lazy expiry sweeps
- get_current_active_user_from_websocket redeems tickets through the
existing "token" query parameter, gated on websocket.path_params so a
ticket only authenticates the console/ws and console/vnc routes of
the node it was minted for; the JWT path is unchanged
- redemption reuses the existing user lookup, token_version revocation
and is_active checks, so logging out invalidates outstanding tickets
- vnc_url no longer embeds the full session JWT
- the tool docstring now tells clients to run the returned command
verbatim instead of reconstructing the URL
The compute_get/compute_images MCP tools typed compute_id as a UUID, so
passing 'local' (the actual id of the built-in compute, which the
compute_images description itself pointed to) was rejected by schema
validation. Both tools now take a string defaulting to 'local', and the
compute_get REST route resolves 'local' through the controller since the
local compute has no database entry.
The device tools reported failures in three shapes: topology-level
entries with only an error key, per-device entries with status 'error'
plus the reason under output (VPCS tool only), and raw exceptions
leaking out of template rendering. Every in-band error entry now
carries status 'failed' and an error message, and invalid Jinja2
templates are reported in-band instead of escaping the handler.
VPCS syntax typed into another node's console is silently discarded
(IOS answers % Invalid input) while the tool still reports success.
get_device_ports_from_topology now carries the GNS3 node type through
to callers, and VPCSCommands fails device preparation with a per-device
error unless the node type is vpcs.
install_appliances_from_image relied on the name+version pair check in
TemplatesService, so the same appliance reached through a second image
(the CSR1000v case) created a second template sharing the name. The auto
path now skips when any template with the same name exists, whatever the
version, and returns a manifest of created and skipped candidates;
POST /images/install replies 200 with that manifest instead of an empty
204, and the image_install MCP tool surfaces it.
POST /appliances/{id}/install replied 204 with an empty body, so the MCP
appliance_install tool crashed with 'Expecting value: line 1 column 1'
while the template had actually been created. The route now returns the
created template (201, response_model=schemas.Template), _create_template
propagates it, and the MCP handler parses the body defensively so an
empty reply degrades to a plain success message.
The controller assigns default names (R-1, R-2, ...) and console ports
in request arrival order. A parallel batch fan-out lets thread scheduling
decide that order, so the first submitted node could end up as R-2.
Batches that rely on default naming (any node without a name) are now
created sequentially; batches with explicit names stay parallel. The
node_create tool description documents the ordering semantics and tells
callers to correlate nodes by node_id.
Splitting the file with keepends=False and rejoining with newlines
dropped the trailing newline of the last line (and every \r of CRLF
files), so returned content was shorter than the file on disk and did
not round-trip. Split with keepends=True and join the selected lines
verbatim; pagination semantics are unchanged.
Batch node/link creation collected results with as_completed, so the
response order followed completion rather than the submitted array and
callers could not correlate entries. Collect in submission order via
pool.map, and report batch deletes as status=success like every other
batch action (the message still says what was deleted).
Per-link markers on serial links need the WAN encapsulation (e.g.
DLT_C_HDLC) so the BPF compiles against the right link layer — the
REST API already accepts it, but the MCP tool never forwarded it.
Create passes it through; update ignores it (changing it would
invalidate the capture file), matching the REST schema semantics.
Symbol tools (symbol_list/get/dimensions/defaults/upload/delete) require
a vision-capable model to be genuinely useful — they shuttle SVG
content, which a text-only LLM cannot inspect or produce. The tool
registrations and imports are commented out (handlers stay in
symbols.py); revisit when multimodal support is worked out.