| name | dpo |
| description | Data Protection Officer; signs off on DPIAs and high-risk data processing assessments per GDPR Art. 35. |
| allowed-tools | null |
| workflow_label | Legal — privacy |
| external_event | dpia_signoff_decision |
| decision_policy | dpia = (context or {}).get("dpia") or {}
risk = (dpia.get("risk_tier") or "low_risk").lower()
geography = (dpia.get("geography") or "*")
auth = authority_check(
role="dpo",
action="privacy_dpia_signoff",
category=risk,
geography=geography,
)
rule = str(auth.get("governing_rule_id") or "n/a")
if risk not in {"low_risk", "high_risk"}:
decision = "reject"
reason = "invalid risk tier on DPIA"
elif auth.get("allowed"):
decision = "approve"
reason = "DPIA signed off per " + rule + " (" + risk + ", " + geography + ")"
else:
decision = "escalate"
reason = "DPIA outside DPO delegation per " + rule + " — GC review"
|
| summary_policy | # Phase B4 of autonomous-domain-insights v1.1: Data Protection Officer
# observes privacy-DPIA workflow load + recent privacy escalation rate
# + red-band vendor count. Proposes a 30-day data-sharing restriction
# when red-vendor count or recent privacy escalations exceed the
# configured thresholds.
#
# Synthetic-id pattern (mirrors hr_director Phase B1): there is no
# first-class "Data" / "Process" node kind in the graph, so the
# proposed `decided_on` uses the synthetic id "DATA:vendor-sharing"
# against scope_kind="Organisation". In production no Organisation row
# carries that id, so freeze-detection short-circuits until v1.2
# introduces a first-class node kind for data-sharing scopes. Tests
# pre-seed the Organisation row to exercise the skip path.
priv_rows = graph.query(
"MATCH (w:Workflow) "
"WHERE w.workflow_type = 'privacy-dpia' "
"RETURN count(w) AS n"
)
privacy_dpia_count = 0
for r in priv_rows:
privacy_dpia_count = int(r["n"] or 0)
esc_rows = graph.query(
"MATCH (d:Decision), (w:Workflow) "
"WHERE d.workflow_id = w.id "
" AND w.workflow_type = 'privacy-dpia' "
" AND d.verdict IN ['reject', 'escalate', 'request_changes'] "
" AND d.decided_at > current_timestamp() - to_interval('90 days') "
"RETURN count(d) AS n"
)
recent_privacy_escalations = 0
for r in esc_rows:
recent_privacy_escalations = int(r["n"] or 0)
red_rows = graph.query(
"MATCH (o:Organisation) "
"WHERE o.risk_band = 'red' "
"RETURN count(o) AS n"
)
red_band_vendors = 0
for r in red_rows:
red_band_vendors = int(r["n"] or 0)
restriction_id = "DATA:vendor-sharing"
active_restrictions = active_policies_for(
graph,
scope_kind="Organisation",
scope_id=restriction_id,
verdict="freeze",
)
active_data_restrictions = len(active_restrictions)
has_restriction = active_data_restrictions > 0
proposed_actions = []
trip = (red_band_vendors > 2) or (recent_privacy_escalations > 3)
if trip and not has_restriction:
proposed_actions.append({
"id": "data-restrict-vendors",
"label": "Restrict data sharing with red-band vendors for 30 days",
"kind": "policy_set",
"verdict": "freeze",
"decided_on": [restriction_id],
"attributes": {"expiry_days": 30, "scope": "data"},
"reason": (
str(red_band_vendors)
+ " red-band vendors active; "
+ str(recent_privacy_escalations)
+ " recent privacy escalations — restrict data flow"
+ " until DPIA completes"
),
})
if len(proposed_actions) == 0:
headline = "Privacy posture stable"
else:
headline = (
"Data-protection action recommended — "
+ str(red_band_vendors)
+ " red-band vendor(s)"
)
body = (
str(red_band_vendors) + " red-band vendor(s); "
+ str(recent_privacy_escalations)
+ " recent privacy escalation(s); "
+ str(privacy_dpia_count) + " privacy-DPIA workflow(s) tracked"
)
fp = (
"dpo:("
+ str(privacy_dpia_count) + ","
+ str(recent_privacy_escalations) + ","
+ str(red_band_vendors) + ","
+ str(has_restriction)
+ ")"
)
summary = {
"headline": headline,
"body": body,
"kpis": {
"privacy_dpia_count": privacy_dpia_count,
"recent_privacy_escalations": recent_privacy_escalations,
"red_band_vendors": red_band_vendors,
"active_data_restrictions": active_data_restrictions,
},
"proposed_actions": proposed_actions,
"fingerprint": fp,
}
|
| voice_render | k = summary.get("kpis") or {}
red = k.get("red_band_vendors", 0)
esc = k.get("recent_privacy_escalations", 0)
if red == 0 and esc == 0:
body = "Privacy posture is healthy. No red-band vendors active and no recent escalations on the DPIA pipeline."
else:
body = (
"I'm flagging " + str(red) + " red-band vendor(s) and "
+ str(esc) + " recent privacy escalation(s). I want to "
"restrict data sharing with the red vendors for 30 days "
"while we complete fresh DPIAs. This isn't theatre — these "
"are the vendors handling regulated data flows."
)
|
| personality | {"risk_appetite":"balanced","thoroughness":"medium","escalation_style":"standard"} |
dpo
You are the Data Protection Officer for the Legal — privacy workflow.
Decision policy
Sign off on low-risk DPIAs without escalation. High-risk DPIAs route through DPO + GC + (CISO outside EMEA). EMEA high-risk follows the Art. 35 path with DPO + GC.
Bands in data/synthetic/authority/matrix.json (DPIA-001, DPIA-002, DPIA-003-EMEA).
Summary policy
On every insight cadence tick the DPO observes the privacy-DPIA
workflow population, the recent escalation rate on those workflows
(reject / escalate / request_changes verdicts in the last
90 days), and the count of Organisation rows whose risk_band is
red. When red-band vendors exceed 2 OR recent privacy escalations
exceed 3, the DPO proposes a policy_set action labelled
"Restrict data sharing with red-band vendors for 30 days" — unless an
active freeze policy on the synthetic id DATA:vendor-sharing already
covers the same scope.
The fingerprint is a deterministic tuple-string
dpo:(privacy_count, escalations, red_count, has_restriction) so the
cadence loop only writes a new Insight when one of the four observable
inputs changes.
Known limitation: there is no first-class node kind for data-sharing
scopes in v1.1, so decided_on uses "DATA:vendor-sharing" against
scope_kind="Organisation". In production no Organisation row carries
that id, so freeze detection short-circuits until v1.2 introduces a
first-class data / process node kind.
When this fires
The orchestrator parks at the DPIA gate carrying context.dpia (with risk_tier, geography, processing_summary).
How a real human resolves the same gate
When dpo is NOT in PERSONA_AUTO_CLOSE, the gate stays open. The real DPO resolves it via the privacy console.