# GRCITSM tenant catalog

Tenant-operational reference for the standard GRCITSM tenant: the MCP tool catalog at a glance, the ticket-side CF id catalog, and the diagnostic workflow. Ticketing doctrine lives in `grcitsm-ticketing.md`; platform write/read semantics in the vendored `reference-docs/halo-platform/` corpus.

## Catalog at a glance

Names only — read the live MCP tool description for each one's contract. The catalog mixes SQL-backed report tools, runbook-backed write tools, and built-in tools.

**Ticket-side write (2):** `agent_note`, `update_ticket_fields`. `agent_note` carries billable-note semantics (action history entry, time tracking, charge rate, start/target dates) unique to the **Action POST** path (`POST /api/Actions`) — they are not replicated by `update_ticket_fields`. `update_ticket_fields` runs on **Update Ticket** (flat `POST /api/Tickets`) — snake_case native FKs (`sla_id`, `priority_id`, `status_id`, `parent_id`, etc.), `{id, value}` or `{name, value}` customfields with **omitted-PRESERVED** semantics, multi-select CFs writable via array shape (NOT CSV). All type-specific CF writes (Issue POA&M, Change Request, Validation check-definition, Vulnerability Deviation) go through `update_ticket_fields` with `customfields:[{id,value}]` (resolve ids via `ticket_type_fields`). The ticket **`todo_list[]`** checklist is also writable through this tool (bare passthrough — same array `get_ticket` returns), but as a **guarded, non-atomic diff**: NOT omit-preserved like `customfields[]` and NOT full-replace like `assets[]`. Read-merge-write the FULL union of items (each with its `id`); see § Generic write and reference-docs/halo-platform/write-semantics.md.

**Custom-table rows (2):** `create_validations` + `get_validations` — the `CTValidations` child-table surface (validation run history per check ticket). Write rides Halo's Update Custom Table action (an import-pipeline UPSERT; body `key` = the TICKET id; custom table 1025 / ticket CF 626 — standard GRCITSM tenant schema, like every field id cited in this skill): rows match on the Validation Key column (627) — new key INSERTS, matched key UPDATES in place (unsent columns preserved), unmentioned rows are never touched or deleted. Rows are **runs-only** — the check definition lives on the type-49 Validation ticket, not on the row. Columns map by FIELDINFO id only (`{name,value}` silently drops values). Deletes are UI-only; bulk repair via ad-hoc `update_ticket_fields` full-replace on the type-7 CF. See § Validation rows.

**Ticket-side structural write (5):** `set_ticket_parent` (writes `Faults.parentid` via body field `parent_id`), `set_ticket_assets` (writes the FAULTDEVICE junction via body `assets[]`, replace-semantics), `set_ticket_client_site` (writes `Faults.clientid` + `Faults.sitenumber` — cross-client reassignment re-scopes SLA and may orphan links; close-and-reopen is usually safer), `assign_agent` (writes `Faults.assignedtoint` — the **only** working path for ticket-level reassignment; the update tools do not expose an `agent_id` parameter because every attribution wire is auth-overridden under the runbook automation principal), and `set_ticket_sla_and_priority` — a narrow Update Ticket wrapper writing `Faults.slaid` + `Faults.seriousness` via body `{sla_id, priority_id}`. Priority is required; sla_id is optional (omit to adjust priority within the current SLA without rebinding). SLA + priority writes also work via the broader `update_ticket_fields` path; the dedicated tool gives a discoverable narrow surface.

**Pre-write validators (4):**

- `ticket_type_allowed_statuses` — call before any update to pick a `status_id` that's valid for the ticket's type.
- `ticket_type_allowed_categories` — call before any update that sets `category` to pick a path valid for the ticket's type. Pass the path string (not the ID) as the `category` param; Halo resolves to `Faults.categoryid2`.
- `ticket_type_fields` — call before any update to discover the live CF schema for the ticket's type. **The `[Allowed Values]` pipe-list is authoritative for static lookups only** — a dynamic-lookup field (framework CFs, baselines) resolves its values at runtime, so take value text from the per-family catalog tool instead (`reference-docs/halo-platform/write-semantics.md` § 3.11). The `[Field Type]` column is **not** authoritative for the underlying SQL type (Boolean CFs misclassify as `free-form` — see § Anti-patterns). Halo silently drops customfield writes that don't match the live lookup exactly (see § Lookup-value rules).
- `slas_and_priorities` — call before any update to resolve `priority_id` against the ticket's SLA. Priority labels are SLA-scoped (the same integer means different labels under different SLAs — e.g., Priority Level 3 can be "Moderate" under a Remediation SLA, "N4: LEV+IRV" under a 20X Low Remediation SLA, and "Medium" under an Incident SLA).

**Other lookups (5):** `agents` (resolves agent_id for `assign_agent`), `users` (end-user catalog — resolves a site's designated General User for requester writes), `charge_rates`, `categories`, `category_control_mappings`. The `statuses` catalog is the read-only ticket-status enum; for updates prefer `ticket_type_allowed_statuses`.

**Requester convention — the designated General User:** every client/site carries a designated "General User" end-user account (Display Name "General User", no email, `Is General User = Yes`), and tickets logged for a client/site carry it as the requester, not the agent who logged them. Resolve it at runtime via `users` (filter Client ID + Site ID + `Is General User = Yes`) — never hardcode user ids. Canonical create recipe: `grcitsm-ticketing.md` § Requester convention.

**Point-lookup control/requirement:** the compliance control catalog is normalized into FIVE custom tables (standard GRCITSM tenant schema) — `CTControlDetails` (canonical detail per ControlID), `CTControlBaselines` (pure membership), `CTControlParameters` (per-class overrides), `CTControlRequirements` (sub-parts), `CTBaselines` (baseline dimension) — and FedRAMP baselines use the 2026 Certification Class model. Point-lookup tools: `lookup_controls(control_id, baseline_name)` returns the `CTControlBaselines` membership row; `lookup_control_details(control_id)` returns the canonical detail row; `lookup_control_parameters(control_id, baseline_name)` returns per-class parameter overrides; `lookup_requirements(requirement_id, baseline_name)` returns the single `CTControlRequirements` row. The 11 baselines: FedRAMP Rev5 Class A/B/C/D, FedRAMP 20x Class A/B/C, CMMC L1/L2/L3, Drata (`baseline_name` examples: 'FedRAMP Rev5 Class C', 'FedRAMP 20x Class C', 'CMMC L2'). Use to **validate** that a name is a real catalog entry in a baseline, and to **read** the canonical control/requirement text — NOT to resolve row IDs for writes. Halo auto-resolves names server-side on both `update_ticket_fields` and `update_asset`: `customfields[].value=[{name:"X"}]` is the canonical multi-select shape (the `id` key is advisory and ignored regardless of value). **Persistence of manual ticket-side framework-CF writes depends on the `category-control-lookup` event runbook's configuration, not on an engine guarantee.** If misconfigured (a loosely-conditioned "Ticket Updated" subscription), the category auto-derive CAN re-fire on a CF-only payload and silently revert manual framework-CF writes within ~90 seconds — but this is not guaranteed: live-validated 2026-06-14 on this tenant, a CF-only framework-CF write was stable on re-read after 2+ minutes. Touching category, however, always re-derives. **Rule: before any bulk manual framework-CF write, pilot ONE write and re-read after 90s before fanning out.** Touch category and auto-derive re-fires and clobbers by design. **Re-categorize + manual override recipe:** if a workflow needs BOTH a category change AND a manual CF set, send TWO sequential `update_ticket_fields` calls — first the category change alone (let auto-derive settle into the new category's mapping), then a follow-up payload with the manual `customfields[]` and NO category. Combining them in one call lets auto-derive clobber the manual entries milliseconds after they land. Site-scope baseline filter applies to both surfaces: names outside the site's enabled `CFBaselines` silently drop. For per-baseline orientation use the `_summary` tools (`control_baselines_summary`, `control_requirements_summary`, `control_details_summary`, `control_parameters_summary`); for baseline metadata use `baseline_catalog`.

**Reads:** `get_ticket`, `search_tickets`, `ticket_summary`, `open_issues`, `closed_issues`, `vulnerability_deviations`, `active_inventory`, `system_information`, `system_pocs`, `control_baselines_summary`, `control_requirements_summary`, `control_details_summary`, `control_parameters_summary`, `baseline_catalog`, `client_details`, `halo_reports`. The per-baseline summary tools provide catalog orientation; full-catalog walks exceed the MCP relay cap. For per-row catalog detail, use `lookup_controls` / `lookup_control_details` / `lookup_control_parameters` / `lookup_requirements`.

**Point read vs bulk enumeration (ticket + asset):** there is no `ticket_details` / `asset_details` tool — both were retired. For a **single** ticket use `get_ticket(ticket_id)`; for a single asset use `get_asset(asset_id)`. To enumerate **many**, go through the scale-safe roll-up summaries, never a full-table walk: `ticket_summary` → loop `get_ticket` per id; `asset_summary` → loop `get_asset` per id. Both summaries return one row per grouping key (Client / Site / Type[/Status]) with pipe-delimited id lists; the asset side additionally carries `<asset id>:<inventory_number>` pairs, so an AFUUID→id resolution over the full population never truncates. `active_inventory` remains the one-call Cloud+Hardware bulk field set, but it **excludes the SSP-spine asset types** (Capability, Component, Implemented Requirement, Implementation Statement) — reach those via `asset_summary` → `get_asset`.

**`open_issues` / `closed_issues` are a fan-out — dedupe before counting.** Both return **one row per (Issue × linked asset)**: an Issue linked to three assets appears as three rows. For any Issue-level count or roster, **dedupe on `[Issue Ticket ID]`** before aggregating — a raw row count over-counts multi-asset Issues. SSP-spine assets are now excluded from this fan-out, so the asset dimension covers only non-spine linked assets.

**Asset reads (2):** `asset_summary` (scale-safe bulk enumeration — one row per Client/Site/Type[/Status] with pipe-delimited id lists and `<asset id>:<inventory_number>` pairs; drives AFUUID→id resolution over the full population without truncation) and `get_asset(asset_id)` (per-asset full Device row + all per-type Asset Fields + global Custom Fields). Standard bulk pattern: `asset_summary` → loop `get_asset` per id. `active_inventory` is the one-call Cloud+Hardware bulk field set but EXCLUDES the SSP-spine types (Capability, Component, Implemented Requirement (dtype 190), Implementation Statement (dtype 191)) — enumerate those via `asset_summary` → `get_asset`.

**Asset-side write (2) + read-back (1):** `create_asset` (Create Asset — minimal identity-only row), `update_asset` (Edit Asset — populates Asset Fields + Custom Fields), and `get_asset` (returns the full Device row, all per-type Asset Fields, and all global Custom Fields by `asset_id`). `get_asset` relays the full asset object only when Halo's "Pause all background services" advanced setting is UNCHECKED — see § Verifying writes for the read-back workflow.

**Asset-side pre-write validators (3):** `asset_types`, `asset_type_fields`, `asset_type_allowed_statuses`. Same role on the asset side as the four `ticket_type_*` lookups play on the ticket side.

**SSP spine reconcilers (6):** `reconcile_ssp_assets`, `reconcile_ssp_relationships_cap_component`, `reconcile_ssp_relationships_ci_ir`, `reconcile_ssp_relationships_ir_is`, `reconcile_ssp_relationships_ci_sp`, `reconcile_ssp_relationships_ir_sp` — zero-param reconcilers, run in that order (cap_component wires Capability→Component rel 6; the two Set Parameter reconcilers wire CI→SP rel 11 and IR→SP rel 12). The five relationship reconcilers are ALSO event-triggered on `Asset created` / `Asset updated`, so the MCP tools are the on-demand/backfill path, not the only path. See `[[ssp]]` § Deterministic spine reconcile and `reference-docs/ssp-spine-reference.md` for the ownership map.

**KSI validation scaffolder (1):** `create_validation_tickets` — zero-param, idempotent, diff-based sweep that stands up the FedRAMP 20x KSI validation tickets on every 20x Site missing them: one top-level Validation-type (49) KSI ticket per baseline-scoped KSI (Class A gets its 6, B/C/D their 46) under category `Validation>KSI`, each with the class-matched KSI Validation SLA, Machine Based priority, and its own-KSI custom field. **Use this for any KSI-ticket build or backfill instead of creating KSI tickets one by one** (agent assignment and IR↔ticket linkage are handled by their own runbooks). See § KSI processes A.

## CF id catalog (standard GRCITSM tenant schema)

Reference for `{id, value}` writes when you don't want to call `ticket_type_fields` (which doesn't expose Field ID integers anyway):

| CF | id | Type | Scope |
|---|---|---|---|
| CFjustification | 114 | free-form | CR |
| CFchangeType | 151 | lookup 122 | CR — Routine Recurring \| Adaptive \| Transformative \| Impact Categorization \| Interim Requirement \| Emergency. **REQUIRED on every CR (set in the create chain) despite `Mandatory: No` in the schema — see § Type-defining CFs.** |
| CFoutageRequired | 158 | **boolean** — pass `true`/`false`, NOT strings | CR |
| CFoutageDuration | 159 | free-form | CR |
| CFChangePlanMemo | 187 | memo HTML | CR |
| CFImpact | 424 | lookup | CR — Company Wide \| Multiple Users Affected \| Single User Affected |
| CFRisk | 425 | lookup | CR — Unknown \| Low RIsk \| Medium Risk \| High Risk |
| CFTypeofDR | 267 | lookup | Deviation/Issue — FP \| OR \| RA \| OR RA \| N/A |
| CFRequestedRiskRatingImpact | 268 | lookup | Deviation/Issue — Critical \| High \| Moderate \| Low \| N/A |
| CFAttackVector | 277 | CVSS env lookup | Deviation |
| CFPrivilegesRequired | 281 | CVSS env lookup | Deviation |
| CFUserInteraction | 283 | CVSS env lookup | Deviation |
| CFImpactMetricsConfidentiality | 285 | CVSS env lookup | Deviation |
| CFRemediationLevel | 291 | lookup | Deviation — Offical Fix \| Temporary Fix \| Workaround \| Unavailable |
| CFPOAMID | 337 | free-form | Deviation/Issue |
| CFDRNumber | 338 | free-form | Deviation |
| CFEvidenceDescription | 341 | narrative HTML | Deviation |
| CFAdditionalInformation | 351 | free-form | Deviation |
| CFDeviationRationale | 353 | narrative HTML | Deviation/Issue |
| CFFedRAMPControls | 378 | multi-select | global |
| CFCMMCControls | 380 | multi-select | global |
| CFFedRAMPKSIs | 381 | multi-select | global |
| CFFedRAMPRequirements | 391 | multi-select | global — write AFTER Controls (separate call) |
| CFCMMCRequirements | 392 | multi-select | global — write AFTER Controls (separate call) |
| CFDrataControls | 462 | multi-select | global |
| CFValidationMethod | 483 | lookup | Validation — Automated \| Manual |

Additional CR memo CFs (id not listed here): CFChangeTestPlanMemo, CFChangeImplementationPlanMemo, CFChangeBackoutPlanMemo, CFCommunicationPlan, CFSecurityImpactAnalysis — all use `{name, value}` shape successfully.

## Diagnostic workflow

When an MCP write call returns `"Task queued."` but the data doesn't appear on read-back, the single fastest first ask is:

> "Paste the Halo execution log for the affected Action."

Without the trace, every failure looks identical from the MCP-tool side ("call succeeded but data is wrong"). With the trace, the request body, response body, and any Halo-side exception are visible and root cause is usually obvious within minutes.

When triaging a silent write:

1. **First**: Halo execution log for the failing Action — request body, response body, any FormatException stack.
2. **Second**: if the log says "value resolved to display X" but the column is still blank, suspect propagation (multi-select lookup CFs, see § above).
3. **Third**: if the log shows a FormatException and the entire `customfields[]` rolled back, suspect a Boolean CF being passed `"Yes"`/`"No"`.
4. **Fourth**: confirm the read-side report is actually reading what you think (e.g., `Did` not `ddevnum`).
5. **Only then**: escalate to whoever maintains the tenant's runbook / MCP configuration.

Field-name note: Halo's Action POST body keys come from `FIELDINFO.actions_field_name` — not the SQL column name and not the UI label. Canonical example: `Faults.category2` (no underscore in SQL) → body key `new_category1` (UI label "Category (Category 1)"). See reference-docs/halo-platform/write-semantics.md.
