Skip to content

plexctl audit

plexctl audit is the CLI surface over the audit chains. The audit aggregate maintains two chains, and every subcommand selects one with exactly one of --domain <UUID> or --platform:

  • The per-Domain chain (--domain <UUID>) records authorization decisions a single Domain owns, against /v1/domains/{domainId}/audit/*.
  • The platform-residency chain (--platform) records actions no single Domain owns, against /v1/platform/audit/*. Cross-Domain list reads such as domain list and project list stamp their granted rows here, so those rows are visible only through --platform.

The parent command groups four operations, each wire-symmetric across both chains:

  • plexctl audit entries list — page through the chain in seq order (GET …/audit/entries).
  • plexctl audit entries get — read one row plus its hash-chain proof (GET …/audit/entries/{seq}).
  • plexctl audit verify — recompute the sha256(prev_hash ‖ sha256(canonical_bytes)) chain (POST …/audit/verify).
  • plexctl audit erase-identity — irreversibly purge the pseudonym ↔ identity mapping (POST …/audit/erase-identity).

The verify contract: verify returns exit 0 on a clean chain and exit 1 on tampering, so an operator script can distinguish a clean chain from a divergent one without parsing stdout. erase-identity requires an explicit --confirm flag — --yes is intentionally not honoured because the operation is forensically irreversible.

--domain and --platform are mutually exclusive, and exactly one is required; supplying both, or neither, is a flag-parse error (exit 2).

Synopsis

shell
plexctl audit entries list   (--domain <UUID> | --platform) [--limit <N>] [--cursor <token>] [--all]
plexctl audit entries get    (--domain <UUID> | --platform) --seq <N>
plexctl audit verify         (--domain <UUID> | --platform) [--from-seq <N>] [--to-seq <N>]
plexctl audit erase-identity (--domain <UUID> | --platform) --identity <UUID> --confirm

Invocation

In every flag table below, --domain and --platform are governed by the one-of contract: supply exactly one. The --platform row is omitted from each table for brevity and documented once here — it is a boolean that, when set, targets the platform-residency chain in place of a Domain.

plexctl audit entries list

Lists rows from the selected chain in seq order. The --all flag follows next_cursor until exhausted; the loop is capped at 100 000 items so a runaway server cannot exhaust the CLI's heap.

Flags

FlagTypeRequiredDefaultDescription
--domainUUIDone-ofOwning Domain UUID. Mutually exclusive with --platform; exactly one is required.
--platformboolone-offalseTarget the platform-residency chain instead of a Domain.
--limitintnoserver defaultMaximum items per page.
--cursorstringnoContinuation token from a previous call's next_cursor. Mutually exclusive with --all.
--allboolnofalseFollow next_cursor until empty (capped at 100 000 rows).

Persistent flags inherited from root: see plexctl.md.

plexctl audit entries get

Reads a single row plus its hash-chain proof. The seq value is the chain's monotonic sequence number starting at 1; the CLI rejects --seq < 1 at flag-parse time so a malformed value never hits the server.

Flags

FlagTypeRequiredDefaultDescription
--domainUUIDone-ofOwning Domain UUID. Mutually exclusive with --platform; exactly one is required.
--platformboolone-offalseTarget the platform-residency chain instead of a Domain.
--seqint64yesMonotonic sequence number, >= 1.

Persistent flags inherited from root: see plexctl.md.

plexctl audit verify

Recomputes the chain from --from-seq (defaults to 1) up to --to-seq (defaults to chain head) and asserts every row's entry_hash = sha256(prev_hash ‖ sha256(canonical_bytes)). The server returns HTTP 200 regardless of outcome with a {ok, segment_from, segment_to, divergent_seq, expected_hash, observed_hash} body; on ok=false the CLI returns a sentinel error so the dispatcher maps the exit to 1. A clean run exits 0.

Flags

FlagTypeRequiredDefaultDescription
--domainUUIDone-ofOwning Domain UUID. Mutually exclusive with --platform; exactly one is required.
--platformboolone-offalseTarget the platform-residency chain instead of a Domain.
--from-seqint64no1Inclusive lower bound on seq.
--to-seqint64nochain headInclusive upper bound on seq.

Persistent flags inherited from root: see plexctl.md.

plexctl audit erase-identity

Drops the audit_subject_pii row that maps the chain's pseudonym back to the underlying identity UUID, satisfying the right-to-erasure requirement. The hash chain is preserved — only the plaintext binding is purged. The operation is forensically irreversible: once the row is dropped the pseudonym ↔ identity mapping is gone for good. --confirm is mandatory and is not substituted by the persistent --yes flag.

Flags

FlagTypeRequiredDefaultDescription
--domainUUIDone-ofOwning Domain UUID. Mutually exclusive with --platform; exactly one is required.
--platformboolone-offalseTarget the platform-residency chain instead of a Domain.
--identityUUIDyesIdentity whose PII mapping is purged.
--confirmboolyesfalseExplicit confirmation; --yes does not substitute.

Persistent flags inherited from root: see plexctl.md.

Exit codes

CodeReachable fromMeaning
0every subcommandSuccess — for verify this means the chain is clean.
1every subcommandRuntime / API error. For verify this is the tamper-detection signal: chain divergence at the seq named in stderr.
2every subcommandFlag-parse / misconfiguration (neither --domain nor --platform set, both set together, malformed UUID, --seq < 1, missing --confirm on erase-identity, --all and --cursor set together, …).
3every subcommandMissing or insecure credentials (no token resolved, InsecureModeError).
4every subcommandPermission denied (HTTP 403 from the server — caller lacks the domain#auditor relation for --domain, or the platform#auditor relation for --platform).
64noneAudit family is fully wired; not a deferred command.

Examples

List the first page of audit entries

shell
plexctl audit entries list \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a1 \
  --limit 50

In --output text the table prints SEQ | OCCURRED_AT | REASON | RELATION | OBJECT_TYPE | OBJECT_ID | CORRELATION_ID. In --output json the wire shape is preserved verbatim, including the optional next_cursor token used to paginate.

List the platform-residency chain

Cross-Domain list reads such as domain list and project list stamp their granted audit rows onto the platform anchor, so those rows are reachable only with --platform:

shell
plexctl audit entries list \
  --server "${PLEXSPHERE_URL}" \
  --platform \
  --output json

The rows carry relation: "domain.list" (or project.list) and an object of type platform. On the wire each platform row's domain_id field carries the reserved platform anchor UUID (00000000-0000-0000-0000-706c6174666d), not a real Domain — the text table never prints it, but --output json includes it, so do not treat it as a Domain identifier.

Verify a clean chain (exit 0)

shell
plexctl audit verify \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a1
echo "exit=$?"   # exit=0

Verify a tampered chain (exit 1)

shell
plexctl audit verify \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a1 \
  --from-seq 1 --to-seq 1000
# stderr: plexctl: audit chain divergence at seq 742 (segment 1..1000)
echo "exit=$?"   # exit=1

Erase an identity from the audit chain

shell
plexctl audit erase-identity \
  --server "${PLEXSPHERE_URL}" \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a1 \
  --identity 0190a8b9-1234-7a0a-8a0a-a0a0a0a0a0a2 \
  --confirm

The response carries the per-Domain pseudonym (safe to print) and the erased_at timestamp. Calling the command again is a no-op at the server (the row is already gone) — the operation is idempotent on subject_pseudonym.

Refusal without --confirm (exit 2)

shell
plexctl audit erase-identity \
  --domain 0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a1 \
  --identity 0190a8b9-1234-7a0a-8a0a-a0a0a0a0a0a2
# stderr: Error: audit erase-identity: --confirm required for irreversible operation
echo "exit=$?"   # exit=2

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 source of truth. The per-Domain operations (ListAuditEntries, GetAuditEntry, VerifyAuditChain, EraseIdentityFromAudit) and the wire-symmetric platform operations (ListPlatformAuditEntries, GetPlatformAuditEntry, VerifyPlatformAuditChain, EraseIdentityFromPlatformAudit) share the AuditEntry, AuditEntryList, AuditEntryProof, AuditChainVerifyRequest, AuditChainVerifyResult, AuditEraseIdentityRequest, AuditEraseIdentityResponse schemas.
  • ../../contexts/audit/access.md — read-access surface: the domain#auditor and platform#auditor ReBAC gates and the eight OpenAPI operations they guard.
  • ../../contexts/audit/index.md — bounded-context reference: ubiquitous language, canonical-byte encoder pin (PXA1 magic), hash-chain state machine, residency rules for both chains, retention matrix, threat model.
  • ../../../cmd/plexctl/commands/audit.go — source of truth for the CLI dispatch, the verify exit-code contract, and the --confirm gate on erase-identity.
  • plexctl.md — root command reference (persistent flags, profile resolution, shared exit-code taxonomy).