@aristanetworks/eos-mcp-server 0.6.1-beta → 0.8.0-beta

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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +197 -44
  3. package/dist/cli.d.ts +4 -1
  4. package/dist/cli.js +63 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/inspectDiagnostics.d.ts +3 -0
  7. package/dist/commands/inspectDiagnostics.js +35 -0
  8. package/dist/commands/inspectDiagnostics.js.map +1 -0
  9. package/dist/commands/releaseDiagnostics.d.ts +9 -0
  10. package/dist/commands/releaseDiagnostics.js +87 -0
  11. package/dist/commands/releaseDiagnostics.js.map +1 -0
  12. package/dist/config/diagnostics.d.ts +60 -0
  13. package/dist/config/diagnostics.js +37 -0
  14. package/dist/config/diagnostics.js.map +1 -0
  15. package/dist/config/loadConfig.js +6 -1
  16. package/dist/config/loadConfig.js.map +1 -1
  17. package/dist/config/schema.d.ts +168 -8
  18. package/dist/config/schema.js +3 -0
  19. package/dist/config/schema.js.map +1 -1
  20. package/dist/diagnostics/concurrency.d.ts +33 -0
  21. package/dist/diagnostics/concurrency.js +157 -0
  22. package/dist/diagnostics/concurrency.js.map +1 -0
  23. package/dist/diagnostics/eapiRunner.d.ts +10 -0
  24. package/dist/diagnostics/eapiRunner.js +46 -0
  25. package/dist/diagnostics/eapiRunner.js.map +1 -0
  26. package/dist/diagnostics/executionError.d.ts +7 -0
  27. package/dist/diagnostics/executionError.js +10 -0
  28. package/dist/diagnostics/executionError.js.map +1 -0
  29. package/dist/diagnostics/executor.d.ts +32 -0
  30. package/dist/diagnostics/executor.js +115 -0
  31. package/dist/diagnostics/executor.js.map +1 -0
  32. package/dist/diagnostics/journal.d.ts +14 -0
  33. package/dist/diagnostics/journal.js +317 -0
  34. package/dist/diagnostics/journal.js.map +1 -0
  35. package/dist/diagnostics/journalTypes.d.ts +8 -0
  36. package/dist/diagnostics/journalTypes.js +2 -0
  37. package/dist/diagnostics/journalTypes.js.map +1 -0
  38. package/dist/diagnostics/ping.d.ts +7 -0
  39. package/dist/diagnostics/ping.js +131 -0
  40. package/dist/diagnostics/ping.js.map +1 -0
  41. package/dist/diagnostics/releaseEvidence.d.ts +502 -0
  42. package/dist/diagnostics/releaseEvidence.js +115 -0
  43. package/dist/diagnostics/releaseEvidence.js.map +1 -0
  44. package/dist/diagnostics/reservationSchema.d.ts +49 -0
  45. package/dist/diagnostics/reservationSchema.js +18 -0
  46. package/dist/diagnostics/reservationSchema.js.map +1 -0
  47. package/dist/diagnostics/statePath.d.ts +4 -0
  48. package/dist/diagnostics/statePath.js +25 -0
  49. package/dist/diagnostics/statePath.js.map +1 -0
  50. package/dist/diagnostics/types.d.ts +82 -0
  51. package/dist/diagnostics/types.js +2 -0
  52. package/dist/diagnostics/types.js.map +1 -0
  53. package/dist/eapi/client.d.ts +3 -1
  54. package/dist/eapi/client.js +17 -6
  55. package/dist/eapi/client.js.map +1 -1
  56. package/dist/eapi/types.d.ts +6 -0
  57. package/dist/eapi/types.js.map +1 -1
  58. package/dist/index.js +10 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/inventory/buildModel.js +1 -0
  61. package/dist/inventory/buildModel.js.map +1 -1
  62. package/dist/inventory/listInventoryView.d.ts +1 -0
  63. package/dist/inventory/listInventoryView.js +1 -0
  64. package/dist/inventory/listInventoryView.js.map +1 -1
  65. package/dist/inventory/policy.d.ts +1 -0
  66. package/dist/inventory/policy.js +3 -1
  67. package/dist/inventory/policy.js.map +1 -1
  68. package/dist/inventory/types.d.ts +1 -0
  69. package/dist/inventory/varValidation.js +1 -0
  70. package/dist/inventory/varValidation.js.map +1 -1
  71. package/dist/mcp/createServer.js +56 -4
  72. package/dist/mcp/createServer.js.map +1 -1
  73. package/dist/mcp/tools/getServerInfo.d.ts +2 -1
  74. package/dist/mcp/tools/getServerInfo.js +2 -2
  75. package/dist/mcp/tools/getServerInfo.js.map +1 -1
  76. package/dist/mcp/tools/ping.d.ts +38 -0
  77. package/dist/mcp/tools/ping.js +40 -0
  78. package/dist/mcp/tools/ping.js.map +1 -0
  79. package/dist/operations/readExecution.d.ts +3 -1
  80. package/dist/operations/readExecution.js +24 -16
  81. package/dist/operations/readExecution.js.map +1 -1
  82. package/dist/serverInfo/buildServerInfo.d.ts +2 -1
  83. package/dist/serverInfo/buildServerInfo.js +16 -2
  84. package/dist/serverInfo/buildServerInfo.js.map +1 -1
  85. package/dist/serverInfo/types.d.ts +15 -0
  86. package/dist/utils/privateFile.d.ts +3 -0
  87. package/dist/utils/privateFile.js +7 -0
  88. package/dist/utils/privateFile.js.map +1 -0
  89. package/docs/CHECKPOINT.md +11 -1
  90. package/docs/CODE_WALKTHROUGH.md +30 -6
  91. package/docs/DESIGN.md +17 -4
  92. package/docs/DIAGNOSTICS.md +218 -0
  93. package/docs/IMPLEMENTATION_PLAN.md +5 -4
  94. package/docs/QUICKSTART.md +13 -13
  95. package/docs/adr/0001-non-configuring-diagnostics.md +5 -0
  96. package/docs/adr/0002-fail-closed-diagnostic-reservations.md +7 -0
  97. package/docs/adr/0003-durable-diagnostic-ledger.md +9 -0
  98. package/docs/adr/0004-offline-operator-attested-release.md +5 -0
  99. package/docs/adr/0005-multi-destination-ping.md +9 -0
  100. package/docs/examples/diagnostic-release-evidence.template.json +26 -0
  101. package/docs/research/eos-diagnostic-lifecycle.md +84 -0
  102. package/package.json +1 -1
  103. package/dist/inventory/writePolicy.d.ts +0 -2
  104. package/dist/inventory/writePolicy.js +0 -36
  105. package/dist/inventory/writePolicy.js.map +0 -1
@@ -0,0 +1,218 @@
1
+ # Non-configuring network diagnostics
2
+
3
+ `eos_ping` executes bounded ping commands **on EOS**, not on the MCP host. It never changes configuration. It generates network traffic and is not a passive read. Traceroute and packet capture are future tools, not implemented capabilities.
4
+
5
+ ## Authorization and addressing
6
+
7
+ A **Target** is an inventory host or group executing the diagnostic. A **Destination** is a literal IPv4/IPv6 address being tested and need not appear in inventory. DNS names and inventory-reference shorthand are not accepted as destinations. One request may name several destinations; each is tested from every resolved host.
8
+
9
+ Diagnostic access defaults to denied. Opt in explicitly using inherited inventory variables:
10
+
11
+ ```yaml
12
+ vars:
13
+ ansible_network_os: eos
14
+ ansible_user: diagnostics-user
15
+ mcp_password_env: EOS_MCP_PASSWORD
16
+ mcp_diagnostics_allowed: true
17
+ hosts:
18
+ leaf1:
19
+ ansible_host: 192.0.2.11
20
+ ```
21
+
22
+ `mcp_read_allowed: false` also denies diagnostics, regardless of the opt-in. Every resolved host must be eligible and authorized; a mixed-permission group fails before any commands are dispatched. `eos_list_inventory` reports `diagnostics_allowed` per host. Group resolution deduplicates hosts and preserves the existing alphabetical result ordering.
23
+
24
+ Use EOS AAA to restrict the credential to the intended operational commands. Ping requires enable mode on the tested EOS version. There is no SSH, device-shell, or MCP-host-shell fallback. Explicit diagnostic opt-in authorizes probes to arbitrary supported destinations reachable from the selected routing context; it is not a destination allowlist. Use network policy/ACLs if additional destination restrictions are required.
25
+
26
+ Supported addresses are unicast IPv4/IPv6 literals, including loopback. Unspecified addresses, IPv4 0/8 and reserved/multicast ranges, limited broadcast, IPv6 multicast, and IPv6 link-local/scoped addresses are rejected. IPv4-mapped IPv6 cannot bypass IPv4 restrictions. A directed broadcast cannot reliably be identified from an address alone; no such detection is promised.
27
+
28
+ ## Tool inputs
29
+
30
+ ```json
31
+ {
32
+ "target": "leaf1",
33
+ "destinations": ["2001:db8::10", "2001:db8::20"],
34
+ "count": 3,
35
+ "vrf": "BLUE",
36
+ "source": "2001:db8::11",
37
+ "include_raw": false
38
+ }
39
+ ```
40
+
41
+ - `target` and `destinations` are required. A single ping is a one-element `destinations` array.
42
+ - `destinations` holds 1 to `maxDestinationsPerRequest` unique addresses. Duplicates are detected by canonical address, so different IPv6 spellings of one address (such as `::1` and `0:0:0:0:0:0:0:1`) count as duplicates, while an IPv4 address and its IPv4-mapped IPv6 form are distinct destinations. Every destination is validated before anything is dispatched, so one invalid or duplicate entry rejects the whole request. The former single `destination` field is rejected with guidance rather than ignored.
43
+ - `count`, `vrf`, and `source` apply to every destination.
44
+ - `count` defaults to the configured `defaultPingCount`; callers cannot exceed `maxPingCount`.
45
+ - `vrf` is optional, a safe single identifier of up to 64 characters. Omission uses the EOS default routing context.
46
+ - `source` is an optional IP literal in the same address family as every destination; mixing IPv4 and IPv6 destinations therefore requires omitting `source`. Source-interface selection is not supported.
47
+ - An invalid/unavailable VRF or source fails; it never falls back to another context.
48
+ - `include_raw` defaults to false and exposes original EOS text only when true, including unrecognized output returned as a device failure.
49
+ - Arbitrary CLI commands/options are not accepted. Pacing is explicitly fixed at one second; EOS packet `size` is fixed at 64. Flood/rapid modes, custom sizes, and DNS resolution are not exposed.
50
+
51
+ ## Settings and defaults
52
+
53
+ Add an optional `diagnostics` mapping to the existing settings YAML. Every omitted field retains its default:
54
+
55
+ ```yaml
56
+ diagnostics:
57
+ defaultPingCount: 3
58
+ maxPingCount: 10
59
+ maxProbesPerRequest: 100
60
+ maxDestinationsPerRequest: 16
61
+ deviceConcurrency: 5
62
+ pingReplyTimeoutMs: 1000
63
+ commandTimeoutMs: 30000
64
+ httpTimeoutMs: 35000
65
+ overallOperationTimeoutMs: 60000
66
+ ```
67
+
68
+ | Setting | Meaning |
69
+ | --- | --- |
70
+ | `defaultPingCount` | Number of probes when the caller omits `count` |
71
+ | `maxPingCount` | Maximum probes per device and destination |
72
+ | `maxProbesPerRequest` | Maximum resolved host count × destination count × requested probes |
73
+ | `maxDestinationsPerRequest` | Maximum destinations in one `eos_ping` call |
74
+ | `deviceConcurrency` | Ledger-scoped diagnostic capacity, shared by active and uncertain executions across calls and preserved across server restarts |
75
+ | `pingReplyTimeoutMs` | EOS per-reply timeout; a positive multiple of 1000 ms |
76
+ | `commandTimeoutMs` | Conservative device execution budget; see lifecycle below |
77
+ | `httpTimeoutMs` | Per-device diagnostic eAPI HTTP timeout, separate from `readTimeoutMs` |
78
+ | `overallOperationTimeoutMs` | Whole diagnostic call deadline, including waiting for global capacity |
79
+
80
+ Optional `diagnostics.stateFile` selects the durable SQLite ledger; relative paths resolve beside the settings file. Otherwise, the server uses `$XDG_STATE_HOME/eos-mcp-server/<inventory-identity>/diagnostics.sqlite`, falling back to `~/.local/state` when `XDG_STATE_HOME` is absent or not absolute. The identity hashes the inventory's canonical filesystem path, so symlink aliases share the default ledger. Keep the inventory identity and ledger location stable across restarts. Changing/copying inventory paths or selecting another state file does not migrate prior reservations and must not be used to bypass them.
81
+
82
+ Existing `maxReadTargets` (default 50) and `maxResponseSizeBytes` (default 1,048,576) also apply. Diagnostic concurrency and timeouts are independent of the corresponding read settings. Effective diagnostic settings are reported by `eos_get_server_info` under `limits.diagnostics`.
83
+
84
+ Numeric settings must be finite positive integers. The default count cannot exceed the maximum count; the maximum count cannot exceed the aggregate probe budget. The command budget must cover:
85
+
86
+ ```text
87
+ maxPingCount * (pingReplyTimeoutMs + 1000) + 1000 ms
88
+ ```
89
+
90
+ The HTTP timeout must exceed the command budget by at least 1000 ms. Individual settings must fit the supported timer range (2,147,483,647 ms); there is no combined reservation-expiry timer. Unknown fields and contradictory settings fail config validation. Increasing count/reply-timeout ceilings may require increasing both command and HTTP budgets. The overall deadline may intentionally be shorter than device execution; that cancels the call, not necessarily the remote command.
91
+
92
+ ## Execution and lifecycle
93
+
94
+ Each host/destination execution is one text-mode eAPI request containing `enable` followed by the generated ping command. The exact wire request ID is committed as `rpc_request_id` before dispatch. All eAPI responses, including read-tool responses, must use JSON-RPC 2.0, match the exact request ID, and contain exactly one `result` or `error` member. Missing/stale identity fails with `eapi_response_uncorrelated`; contradictory envelopes fail with `eapi_payload_invalid`. Neither is completion evidence, and an unrelated response is never exposed as the requested diagnostic's raw output. There are **no automatic ping retries**, including no JSON-to-text retry. Read-only auto fallback also requires a correlated error and uses a new ID for its subsequent request. Existing `eos_run_show` validation remains show-only.
95
+
96
+ On cEOS 4.34.3M, ping's `timeout` maps to a per-reply wait, not a whole-process deadline. The tool combines finite `repeat`, explicit one-second `interval`, and finite reply timeout; the conservative execution formula above must fit `commandTimeoutMs`. This budget is **not** a remote wall-clock kill timer. It assumes normal EOS command scheduling, not a stalled operating system or an indefinitely queued eAPI command. No remote process termination guarantee is made.
97
+
98
+ One admission controller is shared by all diagnostic calls using the server's ledger. The reservation is durably committed **before** the device runner can dispatch. Confirmed synchronous completion or a locally established rejection before dispatch releases capacity only after the removal commits. Unknown completion after transport loss, HTTP timeout, invalid eAPI payload (including correlated results with missing or non-text output), cancellation, or a post-dispatch RPC error becomes **uncertain and never expires automatically**. RPC interruption errors are not process-reaped certificates; even failed commands may conservatively retain capacity. Neither elapsed time nor HTTP abort proves that earlier work stopped or cannot start later. A late confirmed response can release its reservation only while the original owner still has the ledger open; there is no reset exposed through MCP.
99
+
100
+ The ledger uses Node's built-in SQLite (Node 24+, potentially marked experimental by the runtime), FULL synchronous commits, and a separate SQLite ownership database. Only one server may own a ledger; another owner fails with `diagnostic_state_in_use`. OS locking releases ownership when a process crashes, without PID-age guessing or deleting a stale lock. All unfinished records, including records persisted as active before a crash, reopen as uncertain. Corrupt, incompatible, inventory-mismatched, or half-missing state is rejected rather than initialized as fresh capacity. A full SQLite integrity check runs before restored capacity is accepted and during offline inspection; malformed indexes must not hide occupied reservations. State write failure blocks further diagnostics with `diagnostic_state_write_failed`; an abort listener does not throw if writing uncertainty fails, because the earlier committed record still preserves the reservation.
101
+
102
+ A request fans out into one execution per resolved host and destination. Executions run concurrently, up to `deviceConcurrency`, through the same admission controller as every other call; each takes and releases its own reservation exactly as a single ping does. Executions are scheduled host by host, in destination order. Queued executions wait for capacity inside the same overall deadline, so size multi-destination requests so that roughly `ceil(executions / available capacity)` successive waves of pings fit within `overallOperationTimeoutMs`; uncertain reservations reduce available capacity. The device can also limit overlap: on cEOS 4.34.3M, eAPI executed at most three commands concurrently per device, and further delivered requests waited on the device while holding their server reservations. A single-host request therefore gains at most that device's eAPI concurrency, even when more server capacity is free.
103
+
104
+ The overall deadline or MCP cancellation stops scheduling new executions, removes capacity waiters, and aborts in-flight HTTP work. Every in-flight execution of a cancelled multi-destination request becomes uncertain, so one cancellation can hold up to `deviceConcurrency` slots; queued executions are never dispatched. An overall timeout is a request-level `operation_timeout`, not a partial-results response. Already completed findings are not returned on that path. Do not blindly retry an interrupted diagnostic.
105
+
106
+ ## Uncertain execution and capacity visibility
107
+
108
+ `eos_get_server_info` returns a fresh `diagnostics.capacity` snapshot with:
109
+
110
+ - `scope: "state-file"` for the production durable ledger, `limit`, and `available_count`. A deliberately in-memory executor reports `server-instance`; production can use that mode only when no inventory host has diagnostic permission.
111
+ - `active_count`, `uncertain_count`, and `queued_count`.
112
+ - `reservations`: ID, inventory hostname, diagnostic kind, state, reservation timestamp, and (when uncertain) uncertainty timestamp and machine-readable reason code. New reservations also carry `rpc_request_id`, matching the journaled reservation ID and the actual wire ID. Legacy records without this field remain uncorrelated; no wire identity is invented for them. Destinations, endpoints, credentials, and original EOS output are not included.
113
+ - `storage_error_code` when journal I/O has failed; new work is blocked even if the numeric limit would otherwise leave space.
114
+
115
+ When every slot is uncertain, new requests fail immediately with `diagnostic_capacity_uncertain`. The MCP result has `isError: true` and structured `error_code`/`message`. Work already waiting for capacity is also rejected instead of waiting indefinitely; if uncertainty arises during group execution, those failures use the existing per-device envelope. If some healthy capacity remains, it can still execute diagnostics without freeing uncertain slots.
116
+
117
+ The standalone `print-server-info` CLI has no live executor and reports `diagnostics.capacity: null`, not fabricated available capacity. Inspect the running MCP server for actual reservation state.
118
+
119
+ ### Offline inspection and operator recovery
120
+
121
+ ```bash
122
+ eos-mcp-server inspect-diagnostics --config eos-mcp-server.yml --json
123
+ # Or derive the default ledger from the same inventory identity:
124
+ eos-mcp-server inspect-diagnostics --inventory inventory.yml --json
125
+ ```
126
+
127
+ Inspection opens existing state read-only, including while another server owns it; it does not resolve credentials, contact devices, take over ownership, create a missing ledger, or clear records. Integrity, schema, reservations, and audit are read in one transaction, so a concurrent commit cannot mix before/after release states. A competing writer may fail closed while this short read snapshot is held; inspect current state before retrying an administrative release. Inspection reports recorded state, not proof that remote work completed.
128
+
129
+ **Operational constraints:** use a persistent local filesystem that honors SQLite locking and sync, private owner-controlled state files (POSIX mode 0600), and an owner-controlled directory not writable by group/others. Network filesystems and ephemeral/container storage are not qualified durability mechanisms. Preserve the ledger, its `.lock.sqlite` ownership file, and any SQLite recovery files together. Do not delete, replace, copy, rotate, roll back, or change the ledger path to clear reservations. Loss of both ledger files cannot be distinguished locally from first initialization. Before upgrading from the earlier in-memory implementation, unresolved old executions require independent reconciliation; they cannot be recovered from a journal that did not yet exist.
130
+
131
+ Restarting with the same ledger retains uncertainty. Recovery is now available only as an **offline, audited Operator Release** backed by a **Fencing Attestation**. There is no MCP reset, release-all operation, native completion-token lookup, or automatic timeout-based release. Stopping the MCP process establishes local ownership transfer, not remote termination.
132
+
133
+ #### Operator release workflow
134
+
135
+ 1. Stop the server owning this ledger and prevent diagnostic resubmission. The release command refuses a live ledger owner; it does not stop one.
136
+ 2. Run `inspect-diagnostics --json` and retain the `inventory_identity`, exact selected reservation IDs, immutable host/admission timestamps, and recorded RPC IDs. Establish the original physical device and execution epoch from independent evidence/history, not just today's inventory mapping.
137
+ 3. Independently verify that the fence terminates running work, invalidates **accepted queued** work, and prevents later dispatch/replay. A timeout, empty process list, quiet counters, or repeated observations are insufficient.
138
+ 4. Prepare a private JSON attestation from [the deliberately invalid template](examples/diagnostic-release-evidence.template.json). Replace every placeholder with actual evidence. Change each assertion to true only after verification. Do not put credentials, raw packet contents, or EOS output in the file; use evidence references instead.
139
+ 5. Release only the exact covered IDs, then inspect the audit:
140
+
141
+ ```bash
142
+ # Prepare with private permissions; the template cannot authorize a release as-is.
143
+ umask 077
144
+ cp docs/examples/diagnostic-release-evidence.template.json ./evidence.json
145
+ # Human: complete the attestation from independently verified fencing evidence.
146
+ chmod 600 ./evidence.json
147
+
148
+ eos-mcp-server release-diagnostics --config eos-mcp-server.yml \
149
+ --reservation-id '<exact-id>' --evidence-file ./evidence.json \
150
+ --confirm-fenced --json
151
+ # Repeat --reservation-id only for other IDs covered by the same attestation.
152
+ eos-mcp-server inspect-diagnostics --config eos-mcp-server.yml --json
153
+ ```
154
+
155
+ `--confirm-fenced`, an evidence file, and explicit unique IDs are mandatory. Evidence is private, owner-controlled UTF-8 JSON with unique keys, at most 65536 bytes and 100 explicitly covered reservations. There is no wildcard selection. The command performs no credential resolution, device call, URI fetch, fencing action, or network retry. A state-only config with an explicit `diagnostics.stateFile` can be used if the original inventory is unavailable; the attestation still must match the ledger's stored inventory identity. If config declares an inventory path, its canonical identity must also match.
156
+
157
+ The attestation requires `evidence_version: 1`, a new UUID `attestation_id`, the exact `inventory_identity`, an accountable `verified_by`, a past UTC `verified_at`, a reason, and four literal-true assertions: `independent_evidence_verified`, `running_work_terminated`, `accepted_queued_work_cannot_execute`, and `replay_prevented`. Each covered reservation must match its exact ID, host and `reserved_at`, and its exact `rpc_request_id` (explicit JSON null for legacy records without one). It must also identify the physical device, fenced execution epoch, fencing time, and independent evidence reference. Fencing must follow reservation admission and precede verification. The selected CLI IDs and evidence scope must match exactly.
158
+
159
+ #### Audit and authority
160
+
161
+ The release audit and every selected deletion commit in **one FULL-synchronous SQLite transaction** under exclusive ledger ownership. A failed batch rolls back earlier deletions and its audit; unselected reservations remain held. The append-only-through-the-application audit records original reservation snapshots, the complete attestation, its canonical SHA-256 digest, an audit-content checksum, server recording time, and actual local OS username/UID. `verified_by` is a claimed accountable person, not authenticated identity; the OS account is collected independently. Local operators must be trusted and protected by OS access controls. This is not a cryptographic human-presence check, and an untrusted agent with the ledger owner's filesystem privileges is outside that protection.
162
+
163
+ `inspect-diagnostics --json` exposes `release_audit` to local operators; MCP server info does not expose the evidence or audit. Checksums detect accidental inconsistency, not deliberate tampering by the trusted filesystem owner. The program validates consistency and explicit authority; it **cannot establish the truth of external fencing evidence**. Releasing on false attestation can violate remote concurrency safety.
164
+
165
+ If CLI completion is lost, inspect the audit first. Repeating the exact committed attestation returns its original release ID with `already_committed: true` and makes no additional changes. Reusing an attestation UUID for changed evidence is rejected. This local administrative idempotency does not make EOS ping requests idempotent; never retry a ping just because its RPC ID is known.
166
+
167
+ Ledger schema 1 upgrades atomically to schema 2, preserving unresolved reservations and adding release history. Read-only inspection of schema 1 does not migrate it. A rejected release also rolls back any migration. Schema-2 ledgers with missing or invalid audit history fail closed, and older schema-1-only binaries must not be used to bypass the audit.
168
+
169
+ ### Native EOS timeout qualification
170
+
171
+ The installed cEOS implementation exposes a `runCmds.requestTimeout` execution-stage interruption timer. It was accepted in a bounded read request, and a later loopback experiment observed an interruption response after 273 ms with a 250 ms native timer (RPC code 1001 / error 408). No ping process appeared in the subsequent samples. This single normal-load result does not qualify a dispatch-relative or process-exit deadline, nor establish the public support contract. The product does not enable it or treat timeout-shaped RPC errors as completion. No durable token-bound status/cancel method was qualified. See [the primary-source investigation](research/eos-diagnostic-lifecycle.md) for evidence, citations, and limits.
172
+
173
+ ## Results
174
+
175
+ Results use the existing target metadata and envelope with one entry per host and destination, ordered by host and then by requested destination. `resolved_devices` lists hosts; `summary` counts host/destination executions, not devices. Each entry carries `inventory_hostname`, `resolved_endpoint`, `destination`, and `status`. `request` echoes normalized intent, including `destinations` and the effective count. Each successful entry's `finding` contains:
176
+
177
+ - `kind: "ping"`
178
+ - `reachability`: `reachable` (all replies), `partial` (some replies), or `unreachable` (no replies)
179
+ - `packets_sent`, `packets_received`, and calculated `packet_loss_percent`
180
+ - `rtt_ms: { min, avg, max }`, or null when unavailable
181
+
182
+ Execution `status: "success"` means ping executed and its output was understood. Zero replies are a successful execution with an `unreachable` finding, not a failed tool operation and not a diagnosis of the cause. Authentication/transport errors, invalid VRF/source, and malformed/inconsistent output are execution failures with machine-readable error codes, reported on that host/destination entry without failing the others. Unknown output never becomes an invented reachability or loss result.
183
+
184
+ ## Extension seam
185
+
186
+ `DiagnosticExecutor.execute` accepts typed diagnostic intent and owns authorization, aggregate budgets, fan-out, fail-closed global admission, cancellation, result envelopes, and response limits. `getCapacity()` exposes a detached snapshot; server-info obtains it at call time rather than caching startup state. `DiagnosticDeviceRunner.execute` accepts inventory identity and one prepared execution; the eAPI adapter owns connection resolution and a single command execution.
187
+
188
+ Each diagnostic kind supplies a `DiagnosticDefinition`: `prepare` validates the whole request before dispatch and returns the echoed request plus the executions every resolved host performs, `probeCount` prices one execution against the aggregate budget, and `interpret` turns observed output into a finding. The executor runs every prepared execution on every host and knows nothing kind-specific. Ping's definition, validation, compilation, and interpretation live in `src/diagnostics/ping.ts`; it prepares one execution per destination. Multiple destinations are therefore a property of ping's definition, not of the executor.
189
+
190
+ Admission queueing is separate from reservation creation and state transitions; shutdown, storage failure, and uncertain-capacity exhaustion share queue rejection. A queued admission failure does not invalidate an earlier command whose completion was already committed. The eAPI adapter separates connection validation from dispatch/error handling instead of maintaining a dispatch flag. `DiagnosticExecutionError` carries a typed `not_dispatched`, `completed`, or `unknown` classification; ordinary errors and malformed acknowledgements remain uncertain. Capacity is reusable only after the corresponding durable removal succeeds.
191
+
192
+ The request, observation, and finding types are discriminated by `kind`; only `ping` is implemented. Add other real operations at this seam without exposing arbitrary command strings. A destination-based operation such as traceroute can reuse multi-destination fan-out by preparing one execution per destination, although its much longer execution bound makes large requests hard to fit in the overall deadline. Packet capture needs a separate decision about transport, privilege, visibility, asynchronous lifetime, artifacts, and sensitive data; no capture-session abstractions are fabricated now.
193
+
194
+ ## Verification
195
+
196
+ Validated against the repository's cEOS 4.34.3M lab over HTTPS eAPI:
197
+
198
+ - IPv4/IPv6 loopback, explicit source and default VRF, and measured RTTs.
199
+ - Explicit one-second pacing, finite repeat count, and reply-timeout behavior.
200
+ - Group fan-out and 100% packet loss in the MGMT VRF.
201
+ - Multi-destination fan-out: six 10-packet pings from one host to its BGP peers completed in 18.6 s (about 56 s when issued as serial calls). A ledger sampler observed five simultaneous active reservations, and direct concurrent eAPI requests confirmed the device ran at most three commands at once.
202
+ - Invalid VRF and unavailable source fail without fallback.
203
+ - Overall HTTP cancellation can leave a short-lived device ping; observed processes exited after their finite probe sequence.
204
+ - MCP stdio execution loads configured defaults and returns real measurements.
205
+
206
+ Older EOS versions and physical platforms have not been qualified for this new tool; the existing read-path minimum does not establish ping compatibility.
207
+
208
+ The refactor was also checked against synthetic ledgers and release audits written by the pre-refactor code (`0e208ec`): inspection, checksum validation, exact committed retries, legacy absent RPC IDs, and remaining reservations stayed compatible. Audit snapshots use the shared reservation codec, and evidence/audit digests share the same JSON SHA-256 implementation; the serialized formats and schema versions are unchanged.
209
+
210
+ ```bash
211
+ npm test
212
+ npm run typecheck
213
+ npm run lint
214
+ npm run build
215
+ INTEGRATION=1 npm test
216
+ ```
217
+
218
+ Integration tests use the isolated `clab/testlab.clab.yaml` lab (admin/admin and lab-only TLS override). The read-path suite now defaults to that topology's unprefixed node names; `EOS_MCP_TEST_PREFIX=clab-testlab-` supports older prefixed deployments. All 13 read-path checks have been run alongside the ping suites, including the EOS code-1003 unconverted-command JSON-to-text read fallback; active ping still never retries. Optional `EOS_PING_TEST_HOST`, `EOS_PING_TEST_HOST_2`, and `EOS_PING_TEST_SILENT_HOST` override its device/silent-destination addresses. Unit tests additionally cover partial loss, input injection, mapped-address restrictions, fail-closed policy, invalid settings, concurrent calls, multi-destination ordering, concurrent destination dispatch, per-destination failure isolation, whole-request destination validation, multi-destination cancellation, uncertain reservations, cancellation, response-size enforcement, write-ahead ordering, blocked dispatch on storage failure, failed completion commits, private permissions, ownership exclusion, malformed/missing ledgers, actual child-process crash recovery, and offline non-mutating inspection. Operator-release tests use synthetic ledger records and synthetic attestations only: they cover exact scope, legacy null IDs, rejected/malformed evidence, private bounded file input, competing ownership, identity/timestamp mismatches, schema migration/rollback, partial deletion rollback, audit integrity and idempotent retry. They do not claim any real EOS execution was fenced or release live test reservations. Additional protocol regressions cover missing/stale IDs, invalid versions, contradictory result/error envelopes, no retry on an uncorrelated read error, persisted wire-ID ordering, and reservation survival across restart after an unrelated acknowledgement. The delayed-backend regression models a request timing out at 35 seconds and starting remotely after the former 65-second expiry: replacement work is now rejected, so modeled active execution stays within the configured ceiling. This is a deterministic fault model, not a claim to have reproduced live EOS queue overload. Live MCP integration also checks timeout-to-uncertain visibility and the structured capacity error before and after restarting with the same ledger.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Product boundary
4
4
 
5
- The server is a permanent read-only MCP service. Scope is limited to inventory inspection, device probing, validated EOS `show` commands, bounded logging, facts, and running-configuration retrieval.
5
+ The server is a permanent read-only MCP service. Scope includes inventory inspection, device probing, validated EOS `show` commands, bounded logging, facts, running-configuration retrieval, and explicitly authorized non-configuring diagnostics. Ping is implemented; traceroute and packet capture are future tools requiring separate capability/lifecycle work.
6
6
 
7
7
  ## Implemented work
8
8
 
@@ -11,8 +11,9 @@ The server is a permanent read-only MCP service. Scope is limited to inventory i
11
11
  - Strict inventory validation, inherited variables, EOS eligibility, and fail-closed read policy.
12
12
  - Inventory-authoritative host/group target resolution.
13
13
  - HTTPS eAPI client with TLS validation, custom CA support, credential validation, request cancellation, and response-size limits.
14
- - Seven MCP tools: server info, inventory, probe, show, logging, facts, and running configuration.
15
- - Local `validate-inventory`, `print-server-info`, and `probe` commands.
14
+ - Eight MCP tools: server info, inventory, probe, ping, show, logging, facts, and running configuration.
15
+ - Typed diagnostic execution with inherited opt-in, configurable budgets/timeouts, global admission, no retries, indefinite uncertain-execution reservations, and live sanitized capacity visibility. Write-ahead durable reservations, crash-safe ownership and offline inspection are implemented. Offline operator-attested release, atomic audit and schema migration are implemented. Machine-verified remote completion/deadline qualification remains follow-up work.
16
+ - Local `validate-inventory`, `print-server-info`, `probe`, read-only `inspect-diagnostics`, and offline attested `release-diagnostics` commands.
16
17
  - Bounded concurrency, target counts, command counts, logging messages, device responses, and aggregate tool responses.
17
18
  - Unit, MCP stdio smoke, package-policy, and optional cEOS integration coverage.
18
19
 
@@ -26,7 +27,7 @@ The server is a permanent read-only MCP service. Scope is limited to inventory i
26
27
 
27
28
  ## Completion criteria
28
29
 
29
- - Every registered MCP tool is read-only and inventory-scoped.
30
+ - Every registered MCP tool is non-configuring and inventory-scoped at the execution target; active diagnostics are explicitly authorized.
30
31
  - All device-facing paths use the shared target, connection, timeout, and result-envelope safeguards.
31
32
  - Documentation lists only supported tools and configuration fields.
32
33
  - `npm test`, `npm run typecheck`, and `npm run build` pass.
@@ -8,18 +8,16 @@ This guide gets you from the npm package to a working MCP server inside Claude C
8
8
  - Claude Code installed (`npm install -g @anthropic-ai/claude-code`)
9
9
  - One or more Arista EOS devices reachable via eAPI (HTTPS)
10
10
 
11
- ## 1. Install the package globally
11
+ ## 1. Check that the package runs
12
12
 
13
- ```bash
14
- npm install -g @aristanetworks/eos-mcp-server
15
- ```
16
-
17
- Verify it's on your PATH:
13
+ The server is published on npm as `@aristanetworks/eos-mcp-server`, and `npx` runs it without a separate install step:
18
14
 
19
15
  ```bash
20
- eos-mcp-server --help
16
+ npx -y @aristanetworks/eos-mcp-server --help
21
17
  ```
22
18
 
19
+ If you prefer a global install, run `npm install -g @aristanetworks/eos-mcp-server` and use `eos-mcp-server` in place of `npx -y @aristanetworks/eos-mcp-server` in the commands below.
20
+
23
21
  ## 2. Create an inventory file
24
22
 
25
23
  Create `inventory.yml` with your EOS devices. Replace the hostnames, IPs, and credentials to match your environment.
@@ -62,13 +60,13 @@ export EOS_MCP_PASSWORD='your-device-password'
62
60
  Validate the inventory:
63
61
 
64
62
  ```bash
65
- eos-mcp-server validate-inventory --inventory inventory.yml
63
+ npx -y @aristanetworks/eos-mcp-server validate-inventory --inventory inventory.yml
66
64
  ```
67
65
 
68
66
  Probe a device to confirm connectivity:
69
67
 
70
68
  ```bash
71
- eos-mcp-server probe --inventory inventory.yml --target leaf1
69
+ npx -y @aristanetworks/eos-mcp-server probe --inventory inventory.yml --target leaf1
72
70
  ```
73
71
 
74
72
  You should see:
@@ -82,7 +80,7 @@ Devices: 1/1 succeeded
82
80
  ## 5. Add the MCP server to Claude Code
83
81
 
84
82
  ```bash
85
- claude mcp add eos -- eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
83
+ claude mcp add eos -- npx -y @aristanetworks/eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
86
84
  ```
87
85
 
88
86
  Use absolute paths for the inventory file since Claude Code may launch the server from any working directory.
@@ -92,7 +90,7 @@ For additional options such as timeouts, request limits, or a default connection
92
90
  To make it available across all your projects, add `-s user`:
93
91
 
94
92
  ```bash
95
- claude mcp add -s user eos -- eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
93
+ claude mcp add -s user eos -- npx -y @aristanetworks/eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
96
94
  ```
97
95
 
98
96
  ### Passing the password to Claude Code
@@ -102,7 +100,7 @@ The simplest approach is to export `EOS_MCP_PASSWORD` in your shell profile (`~/
102
100
  Alternatively, add it inline when registering the server:
103
101
 
104
102
  ```bash
105
- claude mcp add -e EOS_MCP_PASSWORD=your-device-password eos -- eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
103
+ claude mcp add -e EOS_MCP_PASSWORD=your-device-password eos -- npx -y @aristanetworks/eos-mcp-server serve --inventory /absolute/path/to/inventory.yml
106
104
  ```
107
105
 
108
106
  ## 6. Verify in Claude Code
@@ -139,7 +137,9 @@ Once connected, Claude Code has access to these tools:
139
137
 
140
138
  ## Troubleshooting
141
139
 
142
- **`npm install -g` fails with permission errors** — Run with `sudo` or configure npm to use a user-writable prefix (`npm config set prefix ~/.npm-global`).
140
+ **`npm install -g` fails with permission errors** — Use `npx` as shown above, or configure npm to use a user-writable prefix (`npm config set prefix ~/.npm-global`).
141
+
142
+ **An old version runs after switching to npm** — Versions up to 0.6.1-beta were installed from a `.tgz` under the unscoped name `eos-mcp-server`. Remove that install with `npm uninstall -g eos-mcp-server`.
143
143
 
144
144
  **`Password env var ... is not set`** — Export `EOS_MCP_PASSWORD` before launching Claude Code, or pass it via `-e` when adding the server.
145
145
 
@@ -0,0 +1,5 @@
1
+ # Allow explicitly authorized, non-configuring diagnostics on EOS
2
+
3
+ Extend operational inspection with bounded diagnostics executed on inventory-selected EOS devices, starting with ping and leaving traceroute and packet capture for later design. Preserve the permanent prohibition on configuration changes, but do not equate non-configuring diagnostics with passive or harmless reads: probes generate traffic, while packet capture can expose sensitive data. Require explicit, inherited diagnostic authorization in inventory, denied by default and always vetoed by read denial; reject a whole group before execution if any member is unauthorized. Diagnostic destinations may be literal IP addresses outside inventory, because inventory identifies where operations execute rather than every address they may test.
4
+
5
+ Use eAPI for the initial ping implementation, without automatic SSH or shell fallback. Keep operation-specific inputs behind a shared typed diagnostic execution interface, not arbitrary command execution. Enforce both device-side bounds and server-wide execution limits; ending an HTTP request is not proof that a device command has stopped. This deliberately adds authorization and lifecycle controls rather than silently extending existing read permissions or relaxing the show-only tool. Future packet capture requires a separate privilege, transport, lifecycle, and data-handling decision.
@@ -0,0 +1,7 @@
1
+ # Retain uncertain diagnostic capacity instead of expiring it
2
+
3
+ Persistence follow-up: [ADR 0003](0003-durable-diagnostic-ledger.md) extends this first slice with a durable ledger; the safety-over-availability decision below remains in force.
4
+
5
+ A finite probe count and reply timeout do not establish when an already-delivered eAPI request will start. Releasing a reservation after a guessed command/HTTP window can therefore admit replacement work while the original command can still execute. Prefer safety over availability: retain unknown-completion reservations without automatic expiry, releasing them only after confirmed completion or definitive rejection before execution. When all capacity is uncertain, reject new and queued diagnostics with `diagnostic_capacity_uncertain`; expose sanitized live reservation metadata through server info rather than suggesting automatic retries or a larger cooldown.
6
+
7
+ This first implementation is scoped to one running server instance. It adds no SSH/shell fallback, remote kill timer, operator reset endpoint, or persistent journal. Restarting loses reservations and is not safe reconciliation. Durable recovery and qualification of any native EOS execution lifecycle/deadline require separate work; this change closes the timer-based capacity-reuse gap, not those remaining guarantees.
@@ -0,0 +1,9 @@
1
+ # Preserve diagnostic reservations across restart in a private SQLite ledger
2
+
3
+ Operator recovery follow-up: [ADR 0004](0004-offline-operator-attested-release.md) adds offline attested release and schema-2 audit; the persistence guarantees below remain in force.
4
+
5
+ In-memory fail-closed reservations are insufficient when an MCP process restarts while EOS work may still run or start later. Production diagnostic execution therefore uses Node's built-in SQLite on private persistent local storage: commit a reservation before dispatch, commit confirmed removal before capacity reuse, and restore unfinished records as uncertain. A separate SQLite ownership database holds an exclusive OS-managed lock for the server lifetime, so process crashes release ownership without guessing from PID or lock age. Prefer one active server per ledger over independent processes silently allocating fresh capacity. Existing state is validated and bound to the canonical inventory identity; corruption, missing companion files, incompatible schemas, and write failures block work rather than recreate empty capacity.
6
+
7
+ The default path is inventory-keyed beneath the user's XDG state directory; `diagnostics.stateFile` permits an explicit stable location. State files must stay together on a filesystem honoring SQLite locking/sync. Changing inventory identity, relocating/deleting all state, ephemeral volumes, and old in-memory executions are not automatic migrations or recovery mechanisms. Credentials, destinations, and EOS output are not journaled. `inspect-diagnostics` provides offline read-only inspection without device calls or ownership takeover, but no reset/release command is introduced.
8
+
9
+ This extends the persistence limitation in ADR 0002, not its requirement for positive completion evidence. Installed EOS exposes an execution-stage requestTimeout, but no dispatch-relative/process-exit bound or durable status token was qualified; see [the investigation](../research/eos-diagnostic-lifecycle.md). Post-dispatch RPC interruption errors remain uncertain. Native timer enablement, operator attestation, and out-of-band fencing/reconciliation require explicit qualification rather than inferring completion from silence.
@@ -0,0 +1,5 @@
1
+ # Allow offline operator-attested release with atomic audit
2
+
3
+ EOS completion remains unprovable from silence or an interruption error, but permanently retaining uncertain capacity leaves no operational recovery path. The user explicitly authorized offline operator authority grounded in independently verified fencing of running, accepted queued and replayable work. Permit only exact reservation IDs, matching immutable execution identities, a private bounded attestation file and explicit confirmation, under exclusive ledger ownership. This is not automatic proof of EOS completion, a fencing action, a wildcard clear, or an MCP tool.
4
+
5
+ Commit original reservation snapshots, the attestation, content checksums, recording time and actual OS account in the same FULL-synchronous SQLite transaction as every selected deletion. Keep the claimed verifier distinct from OS identity; neither a field label nor an explicit flag proves human presence. The trusted filesystem owner/OS access policy is the authority, and hashes detect accidental inconsistency rather than malicious owner forgery. Exact committed retries return the prior event without further mutation; changed reuse is rejected. Upgrade schema 1 transactionally to schema 2 so older binaries fail closed rather than ignore operator audit. Read-only inspection does not migrate state. False human evidence can violate remote concurrency safety; the program cannot certify external fencing truth.
@@ -0,0 +1,9 @@
1
+ # Accept multiple destinations in one ping request
2
+
3
+ Testing several destinations from the same target, such as every BGP peer of a switch, required one `eos_ping` call per destination. MCP clients do not reliably run those calls concurrently: measured on 2026-10-28, Claude Code dispatched six pings issued in one turn strictly one at a time, and Codex runs MCP calls serially unless the server connection opts into parallel calls. Labelling ping read-only to change client scheduling would misstate an active diagnostic that generates traffic and writes durable reservations. Instead, `eos_ping` takes `destinations` (one or more unique IP literals) and runs one execution per resolved host and destination concurrently inside the server, so every client gets the same behavior from one call.
4
+
5
+ Make this a breaking change rather than adding a second tool or an optional alias: one interface serves a single ping (a one-element array) and a sweep, and responses always contain one result per host and destination, each naming its destination. The removed `destination` field is rejected with guidance instead of being silently accepted. `summary` counts host/destination executions; `resolved_devices` still lists hosts.
6
+
7
+ Keep the safety model of ADRs 0001 to 0004 unchanged. Every execution is admitted, journaled, released, or retained as uncertain exactly like a single ping, under the existing shared `deviceConcurrency`. The whole request is validated before any dispatch, the aggregate probe budget counts hosts × destinations × count, and `maxDestinationsPerRequest` bounds request size. The cost is that one cancelled or timed-out request can now leave up to `deviceConcurrency` uncertain reservations from a single host target, as group targets already could. Large requests must fit the overall deadline in successive waves of available capacity. No up-front feasibility rejection or per-request concurrency cap is added: a conservative bound would reject deliberately short deadlines that existing integration tests and operators use, and a per-request cap would diverge from existing group fan-out.
8
+
9
+ Fan-out across destinations belongs to ping's `DiagnosticDefinition`, which prepares one execution per destination. The executor only runs prepared executions on resolved hosts, so a future destination-based diagnostic can reuse the pattern, and diagnostics without destinations such as packet capture are not forced into it.
@@ -0,0 +1,26 @@
1
+ {
2
+ "evidence_version": 1,
3
+ "attestation_id": "<new UUID for this independently verified attestation>",
4
+ "inventory_identity": "<exact inventory_identity from inspect-diagnostics --json>",
5
+ "verified_by": "<accountable operator>",
6
+ "verified_at": "<UTC ISO timestamp after independent verification>",
7
+ "reason": "<approved incident/recovery reason; do not include secrets>",
8
+ "assertions": {
9
+ "independent_evidence_verified": false,
10
+ "running_work_terminated": false,
11
+ "accepted_queued_work_cannot_execute": false,
12
+ "replay_prevented": false
13
+ },
14
+ "reservations": [
15
+ {
16
+ "reservation_id": "<exact selected reservation ID>",
17
+ "rpc_request_id": "<recorded wire ID; use JSON null only when absent in the ledger>",
18
+ "inventory_hostname": "<exact inventory hostname from the reservation>",
19
+ "reserved_at": "<exact reserved_at from the reservation>",
20
+ "physical_device_identity": "<independently established original device identity>",
21
+ "execution_epoch": "<original execution/queue epoch covered by the fence>",
22
+ "fenced_at": "<UTC ISO fencing time after reservation admission and before verification>",
23
+ "evidence_reference": "<independent evidence reference; no credentials or packet contents>"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,84 @@
1
+ # EOS HTTPS eAPI diagnostic lifecycle
2
+
3
+ ## Conclusion
4
+
5
+ **No dispatch-relative deadline, durable execution-token lifecycle, or definitive cancellation guarantee was qualified for ping.** However, EOS **does expose a native `runCmds.requestTimeout`**: saying eAPI has only a client-side timeout would be incorrect. Its installed implementation is an execution-stage interruption timer, not a verified admission-to-process-exit bound. Losing HTTP completion evidence must therefore remain an uncertain reservation, not become free capacity after a cooldown.
6
+
7
+ Scope: installed cEOS lab image `ceos-lab:4.34.3M`, inspected on `node1-1`; HTTPS checks against `172.20.20.11`, 2026-10-27 UTC. Repository context: `docs/DIAGNOSTICS.md` and ADRs `0001`/`0002`. Those documents describe requirements/current behavior, not independent proof of EOS capabilities.
8
+
9
+ ## Evidence and qualification
10
+
11
+ ### Measured in this investigation
12
+
13
+ - HTTPS `runCmds`, version 1, text format, commands `enable` and `show version`, with `requestTimeout: 5.0`, `ignoreDisconnect: false`, and `timestamps: true`, returned HTTP 200 and matching JSON-RPC ID in approximately **0.023 s**. EOS reported **4.34.3M-44102239.4343M (engineering build)**. This proves these parameters are accepted on this device, **not** that timeout expiry terminates ping.
14
+ - HTTPS `getCommandHelp` and enable-mode `getCommandCompletions` inspected ping syntax without executing ping. Completion after `ping 127.0.0.1 ` offered `repeat`, `timeout`, `interval`, size/source/interface/pattern/DF/record-route options; timeout accepted `<0-3600>` seconds. A `deadline` suffix produced no completions. This agrees with the installed grammar, but is not an exhaustive test of every possible CLI wrapper.
15
+ - **The initial source investigation emitted zero ping probes**; it performed no connection-loss experiment, forced timeout, process termination, configuration change, recovery/reset, SSH, capture, or load test. A later bounded expiry experiment is recorded separately below. HTTP calls had a 10-second client timeout and bounded response reads. Lab certificate verification was disabled only for these research calls; no credentials or configuration are included here.
16
+
17
+ ### Bounded native timeout-expiry follow-up
18
+
19
+ A later normal-load HTTPS experiment used text-mode `enable` plus `ping ip 127.0.0.1 repeat 4 timeout 1 interval 1 size 64`, with `requestTimeout: 0.25`, `ignoreDisconnect: false`, `timestamps: true`, version `latest`, and an eight-second client limit. The command could emit at most four loopback probes; no configuration change, SSH, capture, overload, recovery/reset, or external process kill was performed.
20
+
21
+ - The matching JSON-RPC response arrived after **273 ms**, HTTP 200, with error code **1001**, message ending in `unexpected error 408`, and an error entry `Command exceeded its allowed time limit`.
22
+ - No exact-name `ping` process was observed in samples at **274, 409, 940, and 4492 ms** after dispatch.
23
+ - This establishes native interruption behavior in one normal-load case on the installed engineering build. It **does not** establish a bound on admission delay, cleanup/reaping under faults, queued-request invalidation, disconnect propagation, other versions, or the public support contract.
24
+ - Sampled process absence and an interruption response are not durable fencing evidence. The product still does not enable this internal option or release uncertain reservations on native timeout errors. A regression fixture now covers the observed 1001/408 error shape.
25
+
26
+ Local raw observations and the bounded reproducer are in `.local-artifacts/diagnostics-native-timeout/observations.json` and `experiment.mjs`; credentials are not recorded in the evidence JSON. These are measured follow-up artifacts, not a substitute for a supported lifecycle guarantee.
27
+
28
+ ### Source-backed capabilities
29
+
30
+ Installed paths below are **inside `node1-1`**, under `/usr/lib/python3.9/site-packages/` unless otherwise stated. Findings paraphrase implementation rather than reproduce proprietary source.
31
+
32
+ | Evidence | What it establishes |
33
+ | --- | --- |
34
+ | `JsonCli.py:345-389`, especially `350`, `364-373`; forwarding at `137-162` | `runCmds` accepts `requestTimeout` in seconds, described as an internal CAPI/eAPI request-termination mechanism distinct from socket timeout. `ignoreDisconnect` defaults false; its description distinguishes operational commands from configuration-command continuation. These are real exposed parameters, not product inventions. |
35
+ | `CliApi.py:479-526` | The timer is created **after session update**, when the backend reaches `executeCommands`, before executing the command list. It is not an absolute timestamp carried from the client, nor a timer armed at client dispatch/HTTP admission. |
36
+ | `CliApi.py:105-143`, `411-477` | Expiry signals tracked child process groups with SIGINT where applicable and interrupts the command thread. The command loop handles interruption and terminates its session. This is a native attempt to interrupt the request, not merely abandonment of the HTTP response. |
37
+ | `CliServerLib.py:545-558`, `729-743`, `850-865`, `1023-1050` | EOS tracks child PIDs, attempts group signalling, and has disconnect/final-cleanup paths. SIGINT can skip marked children; signalling errors are caught. These paths do not supply an externally queryable process-reaped acknowledgement or a bounded signal-to-exit contract. |
38
+ | `CapiUwsgiServer.py:224-281`; `EapiClientLib.py:206-220`; `CliPatchSigint.py:186-208` | HTTPS command handling creates a stateless backend connection and closes it during cleanup; backend disconnect can cause interruption/child cleanup. This does **not** prove when an external HTTP loss is detected or propagated through the HTTP server and backend, or what the client can verify after losing the response. |
39
+ | `CliPlugin/NetworkToolsCli.py:208-257`, `287-373`, `388-411` | Enable-mode IPv4/IPv6 ping has finite-count/pacing/reply-wait options. EOS maps `repeat` to `-c`, `timeout` to **`-W`**, and `interval` to `-i`; the inspected handler supplies no `-w` whole-ping deadline. It waits using `communicate()` on the child. No ping deadline/job/status/cancel option occurs in this grammar. |
40
+ | `JsonCli.py:180-211`, `215-216`, `284-285`, `344-354`; `CliServerLib.py:593-620`; `JsonRpcBase.py:119-123`, `260-304`, `393-395` | The inspected backend exports completion/help/prompt, configuration operations, and `runCmds`; it exports no execution-token status/cancel method. JSON-RPC `id` is client-supplied, echoed, and cleared from request-local state. It is correlation, not demonstrated durable identity, deduplication, or a retrievable execution record. Configuration sessions are not diagnostic jobs. |
41
+ | `CliApi.py:495-497`, `596-626` | `timestamps` supplies command execution timing in returned results, not a lookup handle or deadline. |
42
+
43
+ The installed API explorer independently describes ordered command execution and an echoed request ID: `/usr/share/nginx/html/eapi/main.js:2`, protocol prose near character offsets 1,340,000–1,369,000. Its request builder on the same line, near offset 1,191,844, exposes ordinary execution options but omits `requestTimeout`/`ignoreDisconnect`. **Installed acceptance/internal documentation is established; a public support contract for these internal controls is not.**
44
+
45
+ Official public client sources corroborate the transport distinction:
46
+
47
+ - Arista [goeapi, pinned revision, request builder](https://github.com/aristanetworks/goeapi/blob/7090068b8735dc15c22444cbda080db4052ae8af/eapilib.go#L183-L190) sends `runCmds`; its [timeout implementation](https://github.com/aristanetworks/goeapi/blob/7090068b8735dc15c22444cbda080db4052ae8af/eapilib.go#L455-L461) sets an HTTP-client timeout, not a remote deadline.
48
+ - Arista EOS+ [pyeapi, pinned revision](https://github.com/arista-eosplus/pyeapi/blob/6816d9e498f06d773fd97121b805dec198c9f6e2/pyeapi/eapilib.py#L314-L364) builds the echoed client ID and filters allowed request parameters; this revision's allowlist does not forward `requestTimeout`/`ignoreDisconnect`. Its [HTTPS connection](https://github.com/arista-eosplus/pyeapi/blob/6816d9e498f06d773fd97121b805dec198c9f6e2/pyeapi/eapilib.py#L669-L686) passes timeout to the transport. Client-library omissions do not establish server incapability.
49
+
50
+ ## Answers
51
+
52
+ ### 1. Supported dispatch-relative or whole-process deadline without shell execution?
53
+
54
+ **Native execution-stage timer: yes. Qualified dispatch-relative/whole-process deadline: no.** `requestTimeout` can accompany the normal text-mode `enable` + ping request without a shell command. It applies to the backend command list, not to each ping reply. But backend/session/admission delay before timer creation is outside its measured interval; expiry uses asynchronous interruption, not a verified process-exit deadline. A stalled scheduler is also outside any demonstrated bound.
55
+
56
+ The supported ping grammar's `timeout` remains a reply wait. Finite repeat/interval/reply settings constrain normal work, not start time. Treat any count-based duration formula as an **assumption-dependent execution estimate**, not permission to release capacity at dispatch time plus that estimate. Hard-deadline behavior and public support for the internal request timer remain unqualified. The follow-up experiment above observes expiry/interruption under normal load, not an admission-to-process-exit guarantee.
57
+
58
+ ### 2. Lost-request lookup or cancellation using a durable token?
59
+
60
+ **Not established.** The inspected HTTPS dispatcher has no such exported method, and its JSON-RPC ID is not an execution token. `ignoreDisconnect: false` enables EOS interruption behavior; it is neither an explicit cancellation RPC nor an acknowledgement that pending work cannot later execute. Reusing an ID is not demonstrated idempotency and must not justify retrying a ping.
61
+
62
+ This is a scoped finding about inspected code/protocol, **not proof that every EOS release, extension, or separate management service lacks lifecycle facilities**.
63
+
64
+ ### 3. Positive evidence that can safely authorize capacity release?
65
+
66
+ For the exact reserved attempt, with trustworthy request/device correlation:
67
+
68
+ - **Normal final synchronous completion:** a complete EOS result after the ping handler's child wait returned. Reachability success is unnecessary; a normally completed zero-reply ping also qualifies.
69
+ - **Definitive rejection before diagnostic execution:** evidence from a known EOS processing path that the diagnostic was not dispatched and cannot remain queued. Generic HTTP/proxy errors, malformed payloads, or arbitrary JSON-RPC error labels are not enough.
70
+ - **Separately verified terminal/fencing evidence:** evidence that both executing work and any previously accepted/queued work for that attempt can no longer run. No native token-bound acknowledgement meeting this condition was qualified here.
71
+
72
+ Caution: `CliApi.py:387-390, 415-430, 459-477` initializes an interrupted command's response as request-timeout status and returns on `KeyboardInterrupt`. A timeout-shaped error is therefore **not by itself a process-reaped certificate**. Qualify the specific terminal/error path before interpreting it as completion.
73
+
74
+ Elapsed time, local abort, an empty process list, packet-counter silence, or availability of another HTTP request are not positive evidence against later dispatch. No delayed-queue fault was reproduced; its exclusion is simply not guaranteed by the evidence available.
75
+
76
+ ### 4. Purely observational automatic reconciliation?
77
+
78
+ **Not defensible from process snapshots/time alone.** Repeated absence of a running ping cannot rule out a request not yet started. A source-understood late final response could automatically reconcile its own reservation; that is positive completion evidence, not inference from silence.
79
+
80
+ Without such evidence or a qualified durable native lifecycle, recovery requires **operator attestation grounded in out-of-band evidence** covering the original device/execution epoch, running work, pending accepted requests, and prevention of later dispatch/replay. An operator merely waiting or observing no process is insufficient. A persistent local journal preserves uncertainty and correlation across restart; it cannot manufacture remote completion proof. New product reservations persist the exact `rpc_request_id` before dispatch and accept only a JSON-RPC 2.0 response matching that ID, with exactly one result/error member. Legacy journal entries without a wire ID remain unknown; no ID is invented for them. These are product correlation checks, not an EOS status-token facility. This research defines no recovery mechanism or new product tool.
81
+
82
+ ## Limits
83
+
84
+ The official [EOS Command API manual URL](https://www.arista.com/en/um-eos/eos-command-api) returned a “Client Challenge” page rather than manual content. That access failure is **not evidence of feature absence**. Public support guarantees, other releases/platforms, undisclosed/native lifecycle interfaces, expiry behavior under faults, and bounded HTTP-disconnect propagation remain unverified. The installed engineering-build findings should be confirmed with Arista before relying on them as a production lifecycle contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aristanetworks/eos-mcp-server",
3
- "version": "0.6.1-beta",
3
+ "version": "0.8.0-beta",
4
4
  "description": "MCP server for Arista EOS eAPI",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,2 +0,0 @@
1
- import type { InventoryVars, NormalizedInventory, ValidationContext } from "./internalTypes.js";
2
- export declare function validateWritePolicyContradictions(hostName: string, globalVars: InventoryVars, lineageGroups: Set<string>, hostVars: InventoryVars, inventory: NormalizedInventory, groupDepths: Map<string, number>, context: ValidationContext): void;
@@ -1,36 +0,0 @@
1
- import { readBoolean } from "../utils/value.js";
2
- export function validateWritePolicyContradictions(hostName, globalVars, lineageGroups, hostVars, inventory, groupDepths, context) {
3
- const falseDepths = [];
4
- const globalWrite = readBoolean(globalVars.mcp_write_allowed);
5
- if (globalWrite === false) {
6
- falseDepths.push(0);
7
- }
8
- for (const groupName of lineageGroups) {
9
- const groupWrite = readBoolean(inventory.groups.get(groupName)?.vars.mcp_write_allowed);
10
- if (groupWrite === false) {
11
- falseDepths.push(groupDepths.get(groupName) ?? 1);
12
- }
13
- }
14
- if (falseDepths.length === 0) {
15
- return;
16
- }
17
- if (readBoolean(hostVars.mcp_write_allowed) === true) {
18
- context.errors.push({
19
- code: "inventory_write_policy_contradiction",
20
- message: `Host ${hostName} sets mcp_write_allowed=true beneath an inherited false`,
21
- path: hostName
22
- });
23
- }
24
- for (const groupName of lineageGroups) {
25
- const groupWrite = readBoolean(inventory.groups.get(groupName)?.vars.mcp_write_allowed);
26
- const depth = groupDepths.get(groupName) ?? 1;
27
- if (groupWrite === true && falseDepths.some((falseDepth) => falseDepth < depth)) {
28
- context.errors.push({
29
- code: "inventory_write_policy_contradiction",
30
- message: `Host ${hostName} inherits mcp_write_allowed=true in group ${groupName} beneath an inherited false`,
31
- path: hostName
32
- });
33
- }
34
- }
35
- }
36
- //# sourceMappingURL=writePolicy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"writePolicy.js","sourceRoot":"","sources":["../../src/inventory/writePolicy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,UAAU,iCAAiC,CAC/C,QAAgB,EAChB,UAAyB,EACzB,aAA0B,EAC1B,QAAuB,EACvB,SAA8B,EAC9B,WAAgC,EAChC,OAA0B;IAE1B,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC9D,IAAI,WAAW,KAAK,KAAK,EAAE,CAAC;QAC1B,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,aAAa,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACxF,IAAI,UAAU,KAAK,KAAK,EAAE,CAAC;YACzB,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO;IACT,CAAC;IAED,IAAI,WAAW,CAAC,QAAQ,CAAC,iBAAiB,CAAC,KAAK,IAAI,EAAE,CAAC;QACrD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,sCAAsC;YAC5C,OAAO,EAAE,QAAQ,QAAQ,yDAAyD;YAClF,IAAI,EAAE,QAAQ;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,aAAa,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAE9C,IAAI,UAAU,KAAK,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,KAAK,CAAC,EAAE,CAAC;YAChF,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;gBAClB,IAAI,EAAE,sCAAsC;gBAC5C,OAAO,EAAE,QAAQ,QAAQ,6CAA6C,SAAS,6BAA6B;gBAC5G,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC"}