Files
ScadaBridge/docs/requirements/Component-CLI.md
T
Joseph Doherty 41a5ecf14a feat(external-systems): deploy per-system retry policy to sites + CLI/management surface (register row 27)
An external system's MaxRetries/RetryDelay existed on the central entity but never
reached a site and had no management surface, so every ExternalSystem.CachedCall
buffered with the S&F engine default (DefaultMaxRetries 50 x DefaultRetryInterval
30s ~= 25 min to park) whatever central had configured. The sibling
DatabaseConnectionArtifact had always carried its equivalents.

The consumption end was already wired: ExternalSystemClient.CachedCallAsync already
read system.MaxRetries/RetryDelay and passed them to EnqueueAsync with the 0-as-unset
convention. Only the transport and persistence were missing.

Commons
- ExternalSystemArtifact gains MaxRetries/RetryDelay as optional positional params
  (0 / TimeSpan.Zero = unset), matching DatabaseConnectionArtifact rather than going
  nullable, so it maps 1:1 onto the entity and the proto3 scalars.
- Create/UpdateExternalSystemCommand gain nullable MaxRetries/RetryDelay, following
  the existing TimeoutSeconds "omit to preserve" convention. Both additive.

Central
- ManagementActor create maps both; update preserves-if-null.
- ArtifactDeploymentService projects them onto the artifact.

Wire
- site_command.proto ExternalSystemArtifactDto gains max_retries=7 /
  retry_delay=8 (google.protobuf.Duration), mirroring DatabaseConnectionArtifactDto.
  Additive: a pre-existing site decodes them as 0/absent, which IS the unset value,
  so old behaviour is preserved. Vendored C# regenerated via docker/regen-proto.sh
  sitecommand; csproj Protobuf item re-commented, no message types added or removed.

Site
- external_systems gains max_retries / retry_delay_ms via AddColumnIfMissing,
  DEFAULT 0 = unset. Storage upsert, repository SELECT/mapping (new ordinals 6/7
  appended so existing ones are untouched), apply path, and the legacy migrator's
  explicit column list all carry them.

Replicated-table decision: external_systems IS in SiteLocalDbSetup.ReplicatedTables,
so this was checked against the AckTime/native_alarm_state precedent. Real columns
are correct here, not a JSON bag: the table has no general-purpose config column
(auth_configuration and method_definitions are both semantically specific),
timeout_seconds already set the additive-column precedent on this exact table, and
sibling database_connections stores the same two values as plain columns. Verified in
LocalDb source that RegisterReplicated re-reads PRAGMA table_info every boot and its
install script is DROP-then-CREATE, so capture triggers rebuild over the new column
set automatically, and SiteStorageSchema.Apply runs before registration on the same
boot. The only cost is the ReplicatedTable.ComputeDigest change, which the sync
handshake compares fail-closed - a mixed-version pair refuses to sync loudly instead
of diverging silently. Both-nodes-together is already the standing site-pair rule, so
no new operational constraint; recorded in topology-guide.md.

Also fixed: ArtifactDiff.CompareExternalSystem never compared MaxRetries/RetryDelay,
so a bundle import that changed a system's retry budget previewed as "Identical" -
harmless while the values were inert, a live behaviour change now.

No EF migration: the central ExternalSystemDefinitions columns already exist
(InitialSchema). Central UI already had both form fields.

Tests: site storage round-trip + unset-default, site apply path, central artifact
projection, proto round-trip (via the shared sample), management create/preserve,
CLI option parse + alias, and diff reporting. Fail-before verified on the two core
tests (expected 7 got 0 on the read path; expected 9 got 0 on the apply path).

Docs: Component-ExternalSystemGateway, Component-StoreAndForward, Component-CLI,
components/StoreAndForward, components/SiteRuntime, deployment/topology-guide,
CLI README (which was also missing --timeout-seconds entirely).
2026-08-15 12:56:12 -04:00

456 lines
28 KiB
Markdown

# Component: CLI
## Purpose
The CLI is a standalone command-line tool for scripting and automating administrative operations against the ScadaBridge central cluster. It connects to the Central Host's HTTP Management API (`POST /management`), which dispatches commands to the ManagementActor. Authentication and role resolution are handled server-side — the CLI sends credentials via HTTP Basic Auth. The CLI provides the same administrative capabilities as the Central UI, enabling automation, batch operations, and integration with CI/CD pipelines.
## Location
Standalone executable, not part of the Host binary. Deployed on any machine with HTTP access to a central node.
`src/ZB.MOM.WW.ScadaBridge.CLI/`
## Responsibilities
- Parse command-line arguments and dispatch to the appropriate management operation.
- Send HTTP requests to the Central Host's Management API endpoint with Basic Auth credentials.
- Display structured responses from the Management API.
- Support both JSON and human-readable table output formats.
## Technology
- **Argument parsing**: `System.CommandLine` library for command/subcommand/option parsing with built-in help generation.
- **Transport**: HTTP client connecting to the Central Host's `POST /management` endpoint. Authentication is via HTTP Basic Auth — the server performs LDAP bind and role resolution.
- **Serialization**: Commands serialized as JSON with a type discriminator (`command` field). Message contracts from Commons define the command types.
## Authentication
The CLI sends user credentials to the Management API via HTTP Basic Auth:
1. The user provides credentials via `--username` / `--password` options.
2. On each request, the CLI encodes credentials as a Basic Auth header and sends them with the command.
3. The server performs LDAP authentication, group lookup, and role resolution — the CLI does not communicate with LDAP directly.
4. Credentials are not stored or cached between invocations. Each CLI invocation requires fresh credentials.
## Connection
The CLI connects to the Central Host via HTTP:
- **Management URL**: The URL of a central node's web server (e.g., `http://localhost:9001`). The management API is served at `POST /management` on the same host as the Central UI.
- **Failover**: For HA, use a load balancer URL in front of both central nodes. The management API is stateless (Basic Auth per request), so any central node can handle any request without sticky sessions.
- **No Akka.NET dependency**: The CLI is a pure HTTP client with no Akka.NET runtime.
## Command Structure
The CLI uses a hierarchical subcommand structure mirroring the Management Service message groups:
```
scadabridge <group> <action> [options]
```
All entities are identified by their integer **ID** (via `--id`, `--template-id`,
`--site-id`, etc.), not by name. Create/update commands take individual flags — there
is no `--file` option. The authoritative, always-current reference is the in-repo
`src/ZB.MOM.WW.ScadaBridge.CLI/README.md`; the command lists below mirror the implemented command
tree at the time of writing.
### Template Commands
```
scadabridge template list [--detail] [--skip <n>] [--take <n>]
scadabridge template get --id <id>
scadabridge template create --name <name> [--description <desc>] [--parent-id <id>]
scadabridge template update --id <id> [--name <name>] [--description <desc>] [--parent-id <id>]
scadabridge template validate --id <id>
scadabridge template delete --id <id>
scadabridge template attribute add --template-id <id> --name <name> --data-type <type> [--value <value>] [--description <desc>] [--data-source <ref>] [--locked]
scadabridge template attribute update --id <id> [--name <name>] [--data-type <type>] [--value <value>] [--description <desc>] [--data-source <ref>] [--locked]
scadabridge template attribute delete --id <id>
scadabridge template alarm add --template-id <id> --name <name> --trigger-type <type> --priority <n> [--description <desc>] [--trigger-config <json>] [--locked] [--trigger-kind advisory|strict]
scadabridge template alarm update --id <id> --name <name> --trigger-type <type> --priority <n> [--description <desc>] [--trigger-config <json>] [--locked] [--trigger-kind advisory|strict]
scadabridge template alarm delete --id <id>
scadabridge template script add --template-id <id> --name <name> --code <code> --trigger-type <type> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--trigger-kind advisory|strict]
scadabridge template script update --id <id> --name <name> --code <code> --trigger-type <type> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--trigger-kind advisory|strict]
scadabridge template script delete --id <id>
scadabridge template composition add --template-id <id> --instance-name <name> --composed-template-id <id>
scadabridge template composition delete --id <id>
scadabridge template native-alarm-source add --template-id <id> --name <name> --connection <name> --source-ref <ref> [--filter <expr>] [--description <desc>] [--locked]
scadabridge template native-alarm-source list --template-id <id>
scadabridge template native-alarm-source remove --id <id>
```
### Instance Commands
```
scadabridge instance list [--site-id <id>] [--template-id <id>] [--search <term>] [--skip <n>] [--take <n>]
scadabridge instance get --id <id>
scadabridge instance create --name <name> --template-id <id> --site-id <id> [--area-id <id>]
scadabridge instance set-bindings --id <id> --bindings <json>
scadabridge instance set-overrides --id <id> --overrides <json>
scadabridge instance import-overrides --id <id> --file <path.csv>
scadabridge instance alarm-override set --instance-id <id> --alarm <name> [--trigger-config <json>] [--priority <n>]
scadabridge instance alarm-override delete --instance-id <id> --alarm <name>
scadabridge instance alarm-override list --instance-id <id>
scadabridge instance native-alarm-source set --instance-id <id> --source <name> [--connection <name>] [--source-ref <ref>] [--filter <expr>]
scadabridge instance native-alarm-source clear --instance-id <id> --source <name>
scadabridge instance set-area --id <id> [--area-id <id>]
scadabridge instance diff --id <id>
scadabridge instance deploy --id <id>
scadabridge instance enable --id <id>
scadabridge instance disable --id <id>
scadabridge instance delete --id <id>
```
`--bindings` is a JSON array of `[attributeName, dataConnectionId]` pairs, e.g.
`[["Speed", 5], ["Mode", 7]]`. `--overrides` is a JSON object of attribute name to
value, e.g. `{"Speed": "100", "Mode": null}`.
### Native Alarm Source Commands
The `native-alarm-source` subcommands manage the **read-only native alarm mirror** —
alarms surfaced from an alarm-capable data connection rather than evaluated by the
ScadaBridge alarm engine. Native alarm sources are declared on a template and may be
overridden per instance. The subcommands map to management commands that resolve via
`ManagementCommandRegistry`:
- `--connection` names an alarm-capable data connection (**OPC UA** or **MxGateway**).
- `--source-ref` is the connection-specific reference: an **OPC UA `SourceNode` nodeId**
or an **MxAccess object/area**.
- `--filter` is an optional connection-specific filter expression that narrows the
mirrored alarm set.
**Template-level** (defines the inherited native alarm sources):
| CLI command | Management command | Required role |
|-------------|--------------------|---------------|
| `template native-alarm-source add` | `AddTemplateNativeAlarmSourceCommand` | Design |
| `template native-alarm-source list` | `ListTemplateNativeAlarmSourcesCommand` | — |
| `template native-alarm-source remove` | `DeleteTemplateNativeAlarmSourceCommand` | Design |
`add` takes `--name`, `--connection`, and `--source-ref` (required), plus optional
`--filter`, `--description`, and `--locked` (a flag that prevents instance-level
override). `remove` targets a single native alarm source by its own `--id`.
**Instance-level** (per-instance overrides of an inherited source; upsert semantics):
| CLI command | Management command | Required role |
|-------------|--------------------|---------------|
| `instance native-alarm-source set` | `SetInstanceNativeAlarmSourceOverrideCommand` | Deployment |
| `instance native-alarm-source clear` | `DeleteInstanceNativeAlarmSourceOverrideCommand` | Deployment |
`set` is an **upsert** keyed by `--instance-id` and `--source` (the inherited source
name): a blank/omitted `--connection`, `--source-ref`, or `--filter` keeps the
inherited value, so only the supplied options are overridden. `clear` removes the
override and **reverts the instance to the inherited template value**.
### Site Commands
```
scadabridge site list
scadabridge site get --id <id>
scadabridge site create --identifier <id> --name <name> [--description <desc>] [--node-a-address <addr>] [--node-b-address <addr>] [--grpc-node-a-address <addr>] [--grpc-node-b-address <addr>]
scadabridge site update --id <id> [--name <name>] [--description <desc>] [--node-a-address <addr>] [--node-b-address <addr>] [--grpc-node-a-address <addr>] [--grpc-node-b-address <addr>]
scadabridge site delete --id <id>
scadabridge site area list --site-id <id>
scadabridge site area create --site-id <id> --name <name> [--parent-id <id>]
scadabridge site area update --id <id> --name <name>
scadabridge site area delete --id <id>
scadabridge site deploy-artifacts [--site-id <id>]
```
### Deployment Commands
```
scadabridge deploy instance --id <id>
scadabridge deploy artifacts [--site-id <id>]
scadabridge deploy status [--instance-id <id>] [--status <status>] [--page <n>] [--page-size <n>]
```
`deploy artifacts` (and its alias `site deploy-artifacts`): `--site-id` is now honored — supplying it deploys system-wide artifacts to exactly that site; omitting it deploys fleet-wide to every site. A site-scoped (non-Administrator) Deployer must supply an in-scope `--site-id`; fleet-wide deployment (no `--site-id`) requires a system-wide Deployer or Administrator (arch-review C2).
### Data Connection Commands
```
scadabridge data-connection list [--site-id <id>]
scadabridge data-connection get --id <id>
scadabridge data-connection create --site-id <id> --name <name> --protocol <protocol> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <n>]
scadabridge data-connection update --id <id> --name <name> --protocol <protocol> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <n>]
scadabridge data-connection delete --id <id>
```
#### Site-routed OPC UA operations (browse / search / verify-endpoint / certs)
CLI parity for the site-routed data-connection operations the ManagementActor dispatches
(arch-review C4). Each carries `--site` so the management API routes it to the target
site cluster. `browse`/`search`/`verify-endpoint` require the **Designer** role; `certs`
require **Admin**. `verify-endpoint` reads `ConfigJson` verbatim from `--config-file`;
`certs trust` base64-encodes `--der-file`.
```
scadabridge data-connection browse --site <identifier> --connection <name> [--node-id <nodeId>]
scadabridge data-connection search --site <identifier> --connection <name> --query <text> [--max-depth <n>] [--max-results <n>]
scadabridge data-connection verify-endpoint --site <identifier> --protocol <protocol> --config-file <path> [--connection <name>]
scadabridge data-connection certs list --site <identifier> [--connection <name>]
scadabridge data-connection certs trust --site <identifier> --connection <name> --der-file <path> --thumbprint <hex>
scadabridge data-connection certs remove --site <identifier> --thumbprint <hex> [--connection <name>]
```
### External System Commands
```
scadabridge external-system list
scadabridge external-system get --id <id>
scadabridge external-system create --name <name> --endpoint-url <url> --auth-type <type> [--auth-config <json>] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
scadabridge external-system update --id <id> [--name <name>] [--endpoint-url <url>] [--auth-type <type>] [--auth-config <json>] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
scadabridge external-system delete --id <id>
scadabridge external-system method list --external-system-id <id>
scadabridge external-system method get --id <id>
scadabridge external-system method create --external-system-id <id> --name <name> --http-method <verb> --path <path> [--params <json>] [--return <json>]
scadabridge external-system method update --id <id> [--name <name>] [--http-method <verb>] [--path <path>] [--params <json>] [--return <json>]
scadabridge external-system method delete --id <id>
```
### Notification Commands
```
scadabridge notification list
scadabridge notification get --id <id>
scadabridge notification create --name <name> --emails <comma-separated>
scadabridge notification update --id <id> [--name <name>] [--emails <comma-separated>]
scadabridge notification delete --id <id>
scadabridge notification smtp list
scadabridge notification smtp update --id <id> --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value>]
```
### Security Commands
```
scadabridge security api-key list
scadabridge security api-key create --name <name> --methods <comma-separated-method-names>
scadabridge security api-key update --key-id <id> --enabled <bool>
scadabridge security api-key set-methods --key-id <id> --methods <comma-separated-method-names>
scadabridge security api-key delete --key-id <id>
scadabridge security role-mapping list
scadabridge security role-mapping create --ldap-group <group> --role <role>
scadabridge security role-mapping update --id <id> [--ldap-group <group>] [--role <role>]
scadabridge security role-mapping delete --id <id>
scadabridge security scope-rule list --mapping-id <id>
scadabridge security scope-rule add --mapping-id <id> --site-id <id>
scadabridge security scope-rule delete --id <id>
```
### Configuration-Change Audit Commands
The `audit-config` group queries the configuration-change audit trail (the `IAuditService`
record of admin edits). `audit-log` is a deprecated alias that still works but emits a
warning; migrate scripts to `audit-config`.
```
scadabridge audit-config query [--user <username>] [--entity-type <type>] [--action <action>] [--from <date>] [--to <date>] [--page <n>] [--page-size <n>]
```
The response is an `AuditLogQueryResult` — `{ "entries": [ … ], "totalCount": <n> }`, where
`totalCount` is the unpaged match count. The result type is a named record specifically
because the handler must not return the repository's `(Entries, TotalCount)` ValueTuple:
`System.Text.Json` serializes properties only, so a tuple result renders as the literal
`{}` and every query looks empty regardless of how many rows matched. `ManagementActor.SerializeResult`
now rejects tuple results outright so the failure mode cannot recur silently.
The new centralized Audit Log component (#23) is exposed via the `scadabridge audit` group below.
### Centralized Audit Commands
The `scadabridge audit` group targets the centralized Audit Log component (#23) and
exposes the UI-equivalent operational audit surface. Permissions follow the same
read-vs-export split the Central UI uses (see Component-AuditLog.md, Security &
Tamper-Evidence, and Security & Auth #10): `audit query`, `audit tree`, and
`audit verify-chain` require the `OperationalAudit` permission; `audit export`
additionally requires `AuditExport`; `audit backfill-source-node` requires the
`Admin` role (maintenance path only). The server enforces permission checks and
returns HTTP 403 (CLI exit code 2) on denial.
```
scadabridge audit query [--since <t>] [--until <t>] [--channel <c>] [--kind <k>] [--status <s>] [--site <s>] [--target <t>] [--actor <a>] [--correlation-id <id>] [--execution-id <id>] [--parent-execution-id <id>] [--errors-only] [--page-size <n>] [--all]
scadabridge audit export --since <t> --until <t> --format csv|jsonl|parquet --output <path> [--channel <c>] [--kind <k>] [--status <s>] [--site <s>] [--target <t>] [--actor <a>]
scadabridge audit tree --execution-id <guid> [--format table|json]
scadabridge audit backfill-source-node --before <ISO-8601-UTC> [--sentinel <value>] [--batch <n>]
scadabridge audit verify-chain --month <YYYY-MM>
```
- `audit query` — filtered query against the central `AuditLog` table, matching the
Central UI Audit Log page filter set (time range, channel, kind, status, site,
instance/script, target, actor, correlation ID, errors-only). Results stream as
JSON (default) or table.
- `audit export` — server-side streaming export of the central `AuditLog` to the
requested format (`csv`, `jsonl`, `parquet`) written to `--output`. The server
streams rows rather than materializing them in memory; the CLI writes bytes
through to disk. Supports the same scoping filters as `audit query`.
- `audit tree --execution-id <guid>` (M5.3 T8) — renders the full execution-chain
tree for the given `ExecutionId`. The server resolves the root from any node in
the chain (walks `ParentExecutionId` to find the root, then traverses downward)
and returns all reachable executions with their summary row counts and first/last
occurred timestamps. Output format: `json` (default — structured tree suitable
for scripting) or `table` (human-readable indented tree). Requires
`OperationalAudit` permission. Backed by `GET /api/audit/tree?executionId=<guid>`.
- `audit backfill-source-node --before <ISO-8601-UTC>` (M5.6 T5) — sets
`SourceNode` to a sentinel value (`--sentinel`, default `"unknown"`) on pre-feature
rows where `SourceNode IS NULL` and `OccurredAtUtc < --before`, in batches
(`--batch`, default 5000). Admin-only maintenance command. Idempotent.
Backed by `POST /api/audit/backfill-source-node`.
- `audit verify-chain` — hash-chain verification for the named month.
**No-op in v1**: the command is defined so the command tree is stable, but
verification only becomes meaningful once the hash-chain ships (see
Component-AuditLog.md, Security & Tamper-Evidence). Until then, the server
responds with a "verification not yet available" status and the CLI exits 0.
### Health Commands
```
scadabridge health summary
scadabridge health site --identifier <site-identifier>
scadabridge health event-log --site <site-identifier> [--event-type <type>] [--severity <level>] [--keyword <term>] [--from <date>] [--to <date>] [--page <n>] [--page-size <n>] [--instance-name <name>]
scadabridge health parked-messages --site <site-identifier> [--page <n>] [--page-size <n>]
```
### Debug Commands
```
scadabridge debug snapshot --id <id>
scadabridge debug stream --id <id>
```
The `debug snapshot` command retrieves a point-in-time snapshot via the HTTP Management API.
The `debug stream` command streams live attribute values and alarm state changes in real-time using a SignalR WebSocket connection. The CLI connects to the `/hubs/debug-stream` SignalR hub on the central server, authenticates with Basic Auth, and subscribes to the specified instance. Events are printed as they arrive — JSON format (default) outputs one NDJSON object per event; table format shows streaming rows. Press Ctrl+C to disconnect.
Key behaviors:
- **Automatic reconnection**: Uses SignalR's `.WithAutomaticReconnect()` to re-establish the connection on loss.
- **Re-subscription**: Automatically re-subscribes to the instance after reconnection.
- **Traefik compatible**: Works through the Traefik reverse proxy — WebSocket upgrade is proxied natively.
- **Required role**: `Deployment`.
Unlike `debug snapshot` (which uses the HTTP Management API), `debug stream` uses `Microsoft.AspNetCore.SignalR.Client` as a dependency for its WebSocket transport.
### Shared Script Commands
```
scadabridge shared-script list
scadabridge shared-script get --id <id>
scadabridge shared-script create --name <name> --code <code> [--parameters <json>] [--return-def <json>]
scadabridge shared-script update --id <id> [--name <name>] [--code <code>] [--parameters <json>] [--return-def <json>]
scadabridge shared-script delete --id <id>
```
### Database Connection Commands
```
scadabridge db-connection list
scadabridge db-connection get --id <id>
scadabridge db-connection create --name <name> --connection-string <string>
scadabridge db-connection update --id <id> [--name <name>] [--connection-string <string>]
scadabridge db-connection delete --id <id>
```
### Inbound API Method Commands
```
scadabridge api-method list
scadabridge api-method get --id <id>
scadabridge api-method create --name <name> --script <code> [--timeout <seconds>] [--parameters <json>] [--return-def <json>]
scadabridge api-method update --id <id> --script <code> [--timeout <seconds>] [--parameters <json>] [--return-def <json>]
scadabridge api-method delete --id <id>
```
### Bundle Commands (Transport #24)
```
scadabridge bundle export --output <path> [--passphrase <phrase>] [--all] [--include-dependencies] [--templates <names>] [--shared-scripts <names>] [--external-systems <names>] [--db-connections <names>] [--notification-lists <names>] [--smtp-configs <names>] [--api-methods <names>] [--sites <ids|names>] [--instances <unique-names>] [--source-environment <env>]
scadabridge bundle preview --input <path> [--passphrase <phrase>]
scadabridge bundle import --input <path> [--passphrase <phrase>] [--on-conflict skip|overwrite|rename] [--map-site <src=dst> ...] [--map-connection <srcSite/srcName=dstName> ...] [--create-missing-sites] [--create-missing-connections]
```
`--sites` / `--instances` (M8/T18) pull site-scoped artifacts: a `--sites` token is a `SiteIdentifier` (preferred) or friendly name; a `--instances` token is a `UniqueName`. Selecting an instance also pulls its site and bound `DataConnection`s.
`bundle preview` prints the per-row diff plus a **required-mapping summary**: for each source site/connection it states either the auto-match (`auto-matches 'X'`) or the directive to supply `--map-site` / `--map-connection` or `--create-missing-*`.
On import, the mapping flags reconcile environment-specific identifiers. `--map-site` / `--map-connection` are repeatable; a token with no `=dst` (or `=` with an empty right-hand side) means **create-new** from the bundle payload, otherwise it binds the source to the named existing target. `--create-missing-sites` / `--create-missing-connections` create any still-unmapped source site/connection instead of aborting. Imported instances always land `NotDeployed`.
Inbound API keys are not transported between environments — re-create them on the destination via CLI or UI.
Bundle commands use a 5-minute timeout.
### Cached-Call Commands (Site Call Audit #22)
Retry or discard parked `ExternalSystem.CachedCall` / `Database.CachedWrite` operations
at a site. Both commands relay via the central `SiteCallAuditActor` → site relay and
require the **Deployer** role (same gate as the Central UI Site Calls page).
```
scadabridge cached-call retry --site-id <identifier> --tracked-operation-id <id>
scadabridge cached-call discard --site-id <identifier> --tracked-operation-id <id>
```
The `--tracked-operation-id` value corresponds to the `MessageId` field of the parked
`SiteCall` row (visible on the Central UI Site Calls page and in the `SiteCalls` audit
table).
### Secured-Write Commands (two-person MxGateway writes)
Query two-person (MxGateway) secured writes — the pending queue plus terminal history.
Read-only; gated any-of **Operator** / **Verifier** / **Administrator** (the history
exposes process-sensitive tag values, so it is not open to every authenticated user).
```
scadabridge secured-write list [--status <status>] [--site <identifier>] [--skip <n>] [--take <n>]
```
`--status` / `--site` filter the results (omit to match all). `--skip` / `--take` provide
offset paging (arch-review P3); omitting them uses the historical unpaged default
(`--skip 0 --take 200`). `--take` is clamped to `1..500` server-side. The result carries a
`totalCount` (rows matching the filters, ignoring paging) alongside the returned page.
Overdue `Pending` rows are swept to `Expired` opportunistically each time the list is
queried (server-side TTL, arch-review S2).
The `--format json|table` option is recursive and accepted on every command above.
## Configuration
Configuration is resolved in the following priority order (highest wins):
1. **Command-line options**: `--url`, `--username`, `--password`, `--format`.
2. **Environment variables**:
- `SCADABRIDGE_MANAGEMENT_URL` — Management API URL (e.g., `http://central-host:5000`).
- `SCADABRIDGE_FORMAT` — Default output format (`json` or `table`).
- `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD` — LDAP credentials. Preferred over
`--password` on the command line, which is visible in process listings and shell
history. Credentials are never read from the config file.
3. **Configuration file**: `~/.scadabridge/config.json` — Persistent defaults for management URL and output format only (never credentials).
### Configuration File Format
```json
{
"managementUrl": "http://central-host:5000"
}
```
## Output Formats
- **JSON** (default): Machine-readable JSON output to stdout. Suitable for piping to `jq` or processing in scripts. Errors are written to stderr as JSON objects with `error` and `code` fields.
- **Table** (`--format table` or `--table`): Human-readable tabular output with aligned columns. Suitable for interactive use.
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error (command failed, connection failure, or authentication failure) |
| 2 | Authorization failure (insufficient role) |
## Error Handling
- **Connection failure**: If the CLI cannot connect to the management URL (e.g., DNS failure, connection refused), it exits with code 1 and a descriptive error message.
- **Command timeout**: If the server does not respond within 30 seconds, the command fails with a timeout error (HTTP 504).
- **Authentication failure**: If the server returns HTTP 401 (LDAP bind failed), the CLI exits with code 1.
- **Authorization failure**: If the server returns HTTP 403, the CLI exits with code 2.
## Dependencies
- **Commons**: Message contracts (`Messages/Management/`) for command type definitions and registry.
- **System.CommandLine**: Command-line argument parsing.
- **Microsoft.AspNetCore.SignalR.Client**: SignalR client for the `debug stream` command's WebSocket connection.
- **Management Service (#18)**: The CLI hits the central cluster via the existing HTTP Management API (`POST /management`), which dispatches to the ManagementActor. The `scadabridge audit` command group rides a parallel REST surface on the same Host (`GET /api/audit/query` and `GET /api/audit/export`), sharing HTTP Basic Auth with `/management` but bypassing the actor for read-only, keyset-paged / streaming workloads.
- **Audit Log (#23)**: The `scadabridge audit query`, `audit export`, `audit tree`, and `audit backfill-source-node` subcommands target the centralized Audit Log component's REST endpoints (`GET /api/audit/query`, `GET /api/audit/export`, `GET /api/audit/tree`, `POST /api/audit/backfill-source-node`) on the Host's Management API surface; `audit verify-chain` is a client-side no-op today (hash-chain deferred to v1.x). Permission checks (`OperationalAudit`, `AuditExport`, `Admin`) are enforced server-side by `AuditEndpoints`.
## Interactions
- **Management Service (via HTTP)**: The primary runtime dependency. All operations except `debug stream` are sent as HTTP POST requests to the Management API endpoint on a central node, which dispatches to the ManagementActor.
- **Central Host**: Serves the Management API at `POST /management` and the debug stream SignalR hub at `/hubs/debug-stream`. Handles LDAP authentication, role resolution, and ManagementActor dispatch.
- **Debug Stream Hub (via SignalR WebSocket)**: The `debug stream` command connects to the `/hubs/debug-stream` hub on the central server for real-time event streaming. This is the only CLI command that uses a persistent connection rather than request/response.