trishul-snmp-suite

MIB Schema Integration Plan

Status: planning complete, phased for 2.1.0 Date: 2026-10-01 Owner: backend/frontend maintainers

This plan integrates the trishul-smi 0.5.3 JSON IR capabilities into the suite. It consolidates two prior analyses (the schema delta review and the earlier feasibility study). Where they disagreed — the earlier study claimed constraints was new in 0.5.3 — the corrected finding stands: constraints predates 0.5.2 (it was contract-pinned there, along with a BITS fix), while enums and units are genuinely new.

1. Scope and background

trishul-smi 0.5.3 (with trishul-snmp 0.6.2) emits an additive, richer MIB JSON IR at schema_version 1.1 (unchanged — bumping is reserved for breaking changes per the trishul-smi bundle compatibility policy).

Object-level IR delta fields in the 0.4.5 → 0.5.3 window:

Field Since Meaning
enums 0.5.2 Ordered label → number map for INTEGER/BITS inline constraints (e.g. ifOperStatus → {"up": 1, ...})
units 0.5.2 SMIv2 UNITS clause string (e.g. "bits/second")
enterprise 0.4.7 TRAP-TYPE ENTERPRISE ref, emitted as {module, object}
trap_number 0.4.7 TRAP-TYPE assignment subidentifier

Supporting facts, with evidence:

What already works with no suite changes (verified against the real bundle backend/data/bundles/sets/run-000001-if-mib-ianaiftype-mib/, producer 0.5.3):

2. Current state

Serializer drop-sites (all confirmed by review; enums/units never reach the frontend through these paths):

Site Evidence Drops
_serialize_varbind backend/app/services/runtime.py:1521-1542 VarBind.enum_label, VarBind.units (enum survives only inside the display_value string, line 1528)
_extract_value backend/app/services/walker_service.py:23-30 prefers raw inner value over the enriched display
trap history varbind loop backend/app/services/traps_service.py:234-247 uses the raw to_display_string() (raw_val["display"]), not display_value; drops enum_label/units
_node_to_record backend/app/services/browser_service.py:40-60 passes constraints (line 59) but not enums/units

Five constraints-fallback implementations (the redundancy this plan eliminates):

  1. backend/app/services/mibs_service.py:111-126 (_enum_values_from_constraints)
  2. backend/app/services/simulator_service.py:154-166 (_first_enum_value)
  3. backend/app/services/browser_service.py:321-338 (_member_entry inline constraints→enum_values derivation — the actual backend producer for the trap picker via get_trap_catalog → /api/mibs/traps)
  4. frontend/js/modules/traps.js:757 (oid.enum_values || oid.constraints fallback in the varbind picker — dead code from the picker path, since _member_entry output carries enum_values but no constraints key)
  5. trishul-snmp mib/render.py (library-side, pinned 0.6.2, VarBind display boundary)

Other gaps:

Phase 0 (completed 2026-10-01):

3. Work packages

P1 — Serializer plumbing, reproducible bundles, recompile prompts

Goal: carry enum_label/units through every serialization path; make compiles deterministic and bundle identity content-addressed; tell operators when their bundle predates a capability.

Changes:

  1. backend/app/services/runtime.py:1521-1542 (_serialize_varbind): add two keys after display_value: "enum_label": varbind.enum_label, "units": varbind.units. Both exist on VarBind (trishul-snmp mib/render.py:56-63); they are None on non-enriched varbinds. Additive keys — old frontend ignores them.
  2. backend/app/services/walker_service.py:
    • add "enum_label": entry.get("enum_label") and "units": entry.get("units") to _walk_item (:33-42, both use_mibs branches).
    • split value extraction: introduce _raw_value(entry) containing today’s _extract_value body (:23-30); change _extract_value to prefer entry.get("display_value") and fall back to _raw_value. _walk_compat_items spans :79-135; change the _extract_value call at line 110 to _raw_value — this single edit keeps both compat label values and metrics plain numerics, not "up(1)" display strings. _metric_value (:61-76) never calls _extract_value (its input is the line-110 value); no change needed there.
    • _walk_line (:45-47) keeps using _extract_value (display preferred).
  3. backend/app/services/traps_service.py:234-247: value preference becomes vb.get("display_value") → raw_val.get("display") → raw_val.get("value"); append "enum_label": vb.get("enum_label") and "units": vb.get("units") to each varbind row. Decision: the display_value preference is gated on effective_resolve_mibs — users with MIB resolution off keep seeing raw values, matching the toggle’s meaning. History rows come from stored event JSON, which serializes via _serialize_varbind, so the fields are present in stored data once change 1 lands.
  4. backend/app/services/browser_service.py:40-60 (_node_to_record): add "enums": dict(node.enums) if node.enums else None and "units": node.units.
  5. Reproducible compiles and content-hash identity:
    • backend/app/services/bundles.py:191-197: add reproducible=True to the CompilerConfig(...) call. Module JSON, manifest.json, and oid_index.json become byte-identical across recompiles of the same sources (trishul-smi pins generated_at to epoch — trishul_smi/output/json_ir.py:17-22).
    • backend/app/models/bundles.py (BundleSet): add a nullable content_hash text column (Alembic migration required).
    • bundles.py (compile_bundle, after :296 commit): compute sha256 over the sorted manifest-declared module files, and record it on the BundleSet. Expose it in _bundle_summary (:876-901) and in the compile-run payload. Do not enforce uniqueness — identical content across runs is expected; the diff view (:386-470) uses it as an “identical to left bundle” fast path and exactness proof.
  6. Producer-aware recompile prompts:
    • bundles.py:513-521 (_manifest_summary): add a capability_floor = "0.5.2" constant; emit recompile_recommended: bool (true when producer_version < 0.5.2) and missing_capabilities: ["enums", "units"] derived from a static PRODUCER_CAPABILITIES = {"enums": "0.5.2", "units": "0.5.2"} mapping.
    • Data flow: list_state/get_bundle have no HTTP routes today. Extend mibs_service.get_status (which already reads the active bundle summary via get_effective_bundle_summary) to surface the active bundle’s producer_version, recompile_recommended, and missing_capabilities through the existing /api/mibs/status response.
    • frontend/js/modules/mibs.js: read the flag from the existing /api/mibs/status fetch and render a dismissible banner on the MIB Manager page: “This bundle was compiled by an older MIB compiler and lacks enum/units metadata. Recompile?” — the banner action invokes POST /api/mibs/reload (the existing recompile path via mib_mutations); if reload’s semantics do not fit, add a dedicated route.

Old-bundle compatibility: all API additions are additive keys. For 0.4.5-era bundles, enum_label/units are None (the library fallback never produced units and the suite never re-derives it); enum display still arrives through display_value via the library path. No behavior regressions.

Verification:

Effort: M (3-4 days). Files touched: 5 service files + 1 model + 1 migration + mibs.js (8 total). Tests added: ~14 cases across 5 files.

Risks: changing _extract_value display preference could alter metric extraction for enum-valued gauges — mitigated by the _raw_value split and a regression case over the IF-MIB walk fixture. reproducible=True changes generated_at in new bundles — harmless (informational field; the diff engine never compares it).

P2 — Enums/units end-to-end UI + fallback consolidation

Goal: render the new metadata everywhere; reduce four constraints-fallback implementations to exactly one suite-side fallback.

Canonical source: the schema enums field via node.enums / VarBind.enum_label / VarBind.units.

Fallback architecture (decision recorded 2026-10-01):

Explicit deletion list (four implementations):

  1. backend/app/services/mibs_service.py:111-126 (_enum_values_from_constraints) — delete. _notification_member_payload (:129-166) uses enum_values(node) from mib_metadata; the enum_values attach at :163-165 stays.
  2. backend/app/services/simulator_service.py:154-166 (_first_enum_value) — delete. _default_value_for_syntax (:168+) takes the enum default via first_enum_value(node).
  3. backend/app/services/browser_service.py:321-338 (_member_entry) — delete the inline constraints→enum_values derivation; _member_entry uses enum_values(node) from mib_metadata.
  4. frontend/js/modules/traps.js:757 — delete the || oid.constraints half; the picker consumes only the backend-provided enum_values.

UI changes:

Old-bundle compatibility: old bundles get identical UI behavior — enum_map falls back to constraints, so the trap picker, simulator defaults, and browser table work for pre-0.5.2 bundles. units is simply absent (nothing renders).

Verification:

Effort: M (2-3 days). Files touched: 1 new module + 6 modified (7 total). Implementations deleted: 3. Tests added: ~10 cases (4 files).

Risks: moving normalizeEnumValues to utils.js must keep the data-enum-values attribute encoding contract (traps.js:632-633) intact — ship as a pure move, no shape change.

P3 — Constraint validation, index-aware walking, oid_index serving, module metadata

Goal: use the schema for correctness (input validation, instance decoding) and speed (client-side OID search); surface provenance.

Changes:

  1. Constraint validation (trap sender + simulator):
    • backend/app/services/mibs_service.py (_notification_member_payload): add "constraint": node.constraints (range/size/union kinds).
    • frontend/js/modules/traps.js (buildVarbindValueControl, :640-666): for Integer inputs with a range constraint, show min/max hints and validate before submit; for String inputs with a size constraint, enforce length. Server-side: the pre-encode coercion sites — traps_service._varbind_to_runtime (traps_service.py:116) and runtime._parse_runtime_objects (runtime.py:653, invoked from send_trap at :639) — reject out-of-range integers and oversize strings with an actionable error before PDU encode. (Note: runtime.py:1729-1776 is the post-send stored-event builder, not the validation site.)
    • backend/app/services/simulator_service.py: _default_value_for_syntax (:168+) draws random values inside the declared range (respecting the MIN/MAX bound spellings); _coerce_custom_value (:80-116) validates bounds/sizes and returns an explicit error instead of silently skipping.
  2. Index-aware walker grouping:
    • backend/app/services/walker_service.py:79-135 (_walk_compat_items): when the walk root resolves to a column node, locate its row (parent OID), read the row’s index list, and decode the OID suffix against each index column’s syntax (integer vs octet-string escaping) instead of splitting symbolic names on ".". Heuristic fallback retained for unresolved roots. This fixes grouping for string-indexed tables (e.g. snmpCommunityTable).
    • Plumbing: _walk_compat_items and execute currently receive only varbinds and have no MIB metadata access. Obtain the active bundle via app.services.bundle_state.get_bundle() (the pattern traps_service uses) or pass it through execute, then resolve the root OID and read the row node’s index list.
  3. oid_index serving + client-side search:
    • New route in backend/app/api/routes/browser.py: GET /api/bundles/{bundle_set_id}/oid-index streaming the sidecar from BundleSet.oid_index_path (path already tracked in the DB). Note: streaming the sidecar from disk does not invoke the loader’s schema gate (that runs on the manifest at bundle load); the route 404s on a missing sidecar file. The route needs a bundle-service dependency — copy the _ctx() pattern from routes/mibs.py.
    • frontend/js/modules/browser.js: load once per bundle, use for instant OID-prefix search (type 1.3.6.1.2.1.2 → ifTable) without per-keystroke server round-trips.
  4. Module metadata panel:
    • There is no module-detail payload or endpoint today. Add module_metadata (already on MibModuleRecord — lastupdated, revisions[], organization, contactinfo, description) to the module rows built by mibs_service.get_status (:342-607), exposed through the existing /api/mibs/status response.
    • frontend/js/modules/mibs.js: build a module revision-history card from those rows (revision history with per-revision descriptions, organization, contact info).

Old-bundle compatibility: constraints and index data exist in 0.4.5-era bundles, so validation works for old and new bundles alike; module_metadata is unchanged since before 0.4.5.

Verification:

Effort: M-L (4-6 days). Files touched: ~7 backend/frontend files + route. Tests added: ~12 cases.

Risks: index decoding for exotic index syntaxes (OID-valued, implied) — scope to integer and octet-string indexes, keep the heuristic fallback, and log when decoding is skipped.

P4 — Deferred / optional

Not scheduled for 2.1.0; recorded so the decisions survive.

  1. display_hint value formatting (DateAndTime, MacAddress, etc.): requires an upstream trishul-snmp change (apply RFC 2579 hints in mib/render.py where MibTypeRecord.display_hint is already loaded). Suite-side cost drops to zero once the library renders it into display_value.
  2. SMIv1 generic-trap decoding via enterprise/trap_number: match incoming v1 trap PDUs to traptype nodes and use their members (VARIABLES) to label varbinds, mirroring the v2 declared_members pattern at runtime.py:1766-1776. Only matters once SMIv1 vendor MIBs are compiled.
  3. tsmi lint gate in compile_bundle: run the 0.5.x lint engine over the compiled set and attach findings to CompileRun.command_json; render in the compile-result panel. lint --fix as an explicit operator action on uploaded MIBs.
  4. SNMPv1 walker protocol support: same protocol-layer workstream as SNMPv3; see item 5.
  5. SNMPv3: out of scope. The prior feasibility study concluded SNMPv3 is a separate workstream (USM/authPriv engine work in trishul-snmp, securityName/context plumbing through the session/runtime layers), orthogonal to the IR changes in this plan — it is not re-derived here. The current FAQ position stands: the runtime surface is v2c.

4. Compatibility matrix

Feature Old bundle (0.4.5-compiled) New bundle (0.5.3-compiled)
Enum label in live responses (display_value) yes — via trishul-snmp constraints fallback (mib/render.py:195-244) yes — via enums field
Structured enum_label in API responses null present (P1)
Enum dropdowns (trap picker) yes — via mib_metadata.enum_map fallback (P2) yes — via enums
Browser enumerations table yes (derived from constraints) yes
Units never (field absent in IR) present when source has UNITS (P1/P2)
Constraint validation yes (constraints predate 0.5.2) yes
Recompile prompt shown (producer_version < 0.5.2) not shown
Content-hash identity computable (hash covers whatever is on disk) computable, byte-stable with reproducible=True
Bundle diffing (diff_bundles) unchanged unchanged; hash fast path when identical
Schema gate loads (1.1 <= 1.1) loads

5. Verification strategy

Per-phase test additions are listed in each work package. Overall:

6. Rollout

Target release: 2.1.0 (next planned minor per docs/roadmap.md). Everything in P1-P3 is additive — new API keys, new UI elements, one nullable DB column — and fits the 2.1.0 mandate of surfacing existing backend capability. No secondary API surface; all routes extend the current /api/... namespace.

Operator guidance (release notes):

Counts summary:

Phase Files touched Tests added Implementations deleted
P0 (done) requirements/venv 0 0
P1 8 ~14 0
P2 8 (1 new) ~10 4
P3 ~7 ~12 0
P4 deferred — —