Policy Data Handling

This document records the operator-facing privacy contracts that policy and compliance packs rely on when they reference release evidence.

It is the binding reference for:

Definition of PII (binding)

The following data classes are forbidden inside immutable capsule manifests, DSSE payloads, append-only audit rows, and any field whose bytes feed a canonicaliser used for signing:

  1. Display names / personal namesfirstName, lastName, displayName, fullName, anything intended to identify a natural person to a human reader.
  2. Email addresses — any string matching ^[^@\s]+@[^@\s]+\.[^@\s]+$.
  3. IP addresses captured during admin actions — IPv4 and IPv6, structured (inet) or string forms.
  4. User-Agent strings — full browser/client UA strings (they fingerprint individuals when combined with other signals).
  5. Free-form usernames that contain @— covered by the existing ValidateOpaqueReference precedent on regulatory audit events.
  6. Telephone numbers, postal addresses, employee IDs from external HR systems — anything correlatable to a natural person via an external directory.
  7. Tenant display names when the tenant maps to a single natural person (sole-trader case). Tenant UUIDs are not PII; tenant slugs are PII-by-association only when the tenant is a single natural person.

Allowed inside capsules:

The write-time enforcer is CapsulePiiGuard (sprint 20260518_051 / TASK-051-03); it runs on every canonicalised manifest before the signer touches the bytes, on regulatory audit events, on evidence-hold reasons, and on decision_capsules.assembly_inputs. Violations raise HTTP 400 and emit an evidence.capsule.pii_rejected audit event containing the path of the offending field but not the offending value.

EDPB-aligned Pseudonymisation Pattern

A SHA-256 of an email address is pseudonymous data, not anonymous data. If the side-table mapping actor_ref -> email is purged but the hash remains in a sealed capsule, the hash is still personal data until the salt is destroyed (EDPB Guidelines 04/2019 on Pseudonymisation, §28).

Stella Ops therefore:

Erasure therefore deletes the cleartext mapping from shared.actor_identity; the capsules referencing actor_ref continue to verify because the canonical bytes they signed never contained any PII.

Side-Table Contract

Table: shared.actor_identity (tenant_id, actor_ref) primary key.

Columns: display_name, email, ip_first_seen, ip_last_seen, user_agent_last, external_subject, region_tag, created_at, updated_at, erased_at, erased_by, erasure_audit_id.

Operations are exposed through Platform.ActorIdentity.Client:

Read-path Redaction Shape

When a capsule read, audit projection, or SAR endpoint surfaces an actor reference, it must call Platform.ActorIdentity.Client.GetAsync and render one of these two shapes through ActorIdentityRedactor.Project:

// Active actor
{
  "actorRef": "01HXYZ...",
  "displayName": "Operator",
  "email": "operator@example.test",
  "externalSubject": "idp|subject",
  "regionTag": "eu-west-1",
  "erased": null
}

// Erased actor — `erased` discriminator is required (never absent)
{
  "actorRef": "01HXYZ...",
  "displayName": null,
  "email": null,
  "externalSubject": null,
  "regionTag": "eu-west-1",
  "erased": { "at": "2026-05-18T12:34:56Z", "by": "01HXYZ-OPERATOR" }
}

The actorRef is always preserved (it is opaque and so not PII), which keeps audit graphs navigable: an operator can still see that an action happened after the natural-person mapping was purged.

Subject Access Request Bundle

Platform exposes the subject access foundation at:

GET /api/v1/operator/sar/{actorRef}

The endpoint is tenant scoped and requires the platform:sar:read scope. The request tenant is resolved through the existing Platform request-context resolver, and both the tenant id and actorRef must be UUID values.

The current bundle shape is:

{
  "sarVersion": "1.0.0",
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "actorRef": "00000000-0000-0000-0000-000000000000",
  "generatedAt": "2026-05-18T12:34:56Z",
  "generator": "stellaops-platform",
  "regionFilter": "eu-only",
  "actorIdentity": {
    "actorRef": "00000000-0000-0000-0000-000000000000",
    "externalSubject": "idp|subject",
    "displayName": "Operator",
    "email": "operator@example.test",
    "regionTag": "eu-only",
    "erasedAt": null
  },
  "services": {
    "timeline": {
      "service": "timeline",
      "status": "deferred",
      "count": 0,
      "reason": "timeline_join_pending"
    },
    "authority": {
      "service": "authority",
      "status": "available",
      "count": 2,
      "authority": {
        "user": {
          "userId": "00000000-0000-0000-0000-000000000000",
          "externalSubject": "idp|subject",
          "username": "operator",
          "status": "active"
        },
        "roleAssignments": [
          { "role": "release-approver", "assignedAt": "2026-05-18T11:15:00Z" }
        ]
      }
    },
    "findings": {
      "service": "findings",
      "status": "available",
      "count": 2,
      "findings": {
        "events": [
          {
            "eventId": "11111111-1111-1111-1111-111111111111",
            "findingId": "finding-1",
            "eventType": "finding.created",
            "actorType": "operator",
            "occurredAt": "2026-05-19T07:00:00Z"
          },
          {
            "eventId": "22222222-2222-2222-2222-222222222222",
            "findingId": "finding-1",
            "eventType": "finding.status_changed",
            "actorType": "operator",
            "occurredAt": "2026-05-19T07:05:00Z"
          }
        ]
      }
    },
    "evidenceLocker": {
      "service": "evidenceLocker",
      "status": "deferred",
      "count": 0,
      "reason": "evidence_locker_join_pending"
    },
    "notify": {
      "service": "notify",
      "status": "deferred",
      "count": 0,
      "reason": "notify_join_pending"
    }
  },
  "missingServices": [
    "timeline",
    "evidenceLocker",
    "notify"
  ],
  "signature": {
    "alg": "ed25519",
    "keyId": "stella-ed25519-sar",
    "value": "<base64 detached Ed25519 signature>",
    "payloadType": "application/vnd.stellaops.sar.v1+json",
    "payload": "<base64 canonical signature-stripped bundle>",
    "publicKey": "<base64 raw Ed25519 public key>"
  }
}

The Platform aggregator declares typed projection sources for every service, so an operator always sees an explicit per-service entry rather than a silent omission.

The signature field is a real Ed25519 DSSE detached signature (Sprint SPRINT_20260519_079 / TASK-079-01). The producer canonicalises the signature-stripped bundle (JsonIgnoreCondition.WhenWritingNull, record property order preserved), embeds it as the base64 payload, computes the DSSE pre-authentication encoding (PAE) over payloadType + payload, and signs the PAE with the ExportCenter Ed25519 key wired through compose (Export:Attestation:Ed25519:PrivateKeyBase64; the SAR key id defaults to stella-ed25519-sar). The raw public key is echoed in publicKey for convenience. It is offline-verifiable via:

# Authoritative: verify against a local trust root (Ed25519 public key PEM,
# raw base64 SubjectPublicKeyInfo, or raw 32-byte base64 key).
stella verify sar path/to/bundle.json --trust-root path/to/sar-public-key.pem

# Integrity-only fallback (no trust root): verifies against the self-described
# public key in the bundle.
stella verify sar path/to/bundle.json

(See the stella verify sar command in src/Cli/StellaOps.Cli/Commands/VerifyCommandGroup.cs and the signer in src/Platform/StellaOps.Platform.WebService/Services/SubjectAccessRequestSigner.cs.)

Data Minimisation Boundary

Evidence capsules must contain opaque actor references, content hashes, and operational metadata only. Clear actor identity belongs in shared.actor_identity and is read by Platform (and by any other consumer through Platform.ActorIdentity.Client) at render time. Erasure can therefore remove mutable identity fields without rewriting sealed capsule bytes, and the DSSE signature of every legacy capsule remains valid after Art. 17 erasure.