feat: VCP robustness layer: observability, privacy enforcement, audit compliance, hooks config#23
feat: VCP robustness layer: observability, privacy enforcement, audit compliance, hooks config#23
Conversation
Adds vcp/metrics.py covering all four VCP layers (Identity, Transport, Semantics, Adaptation) plus hooks and audit. Uses optional prometheus_client with a _NoOpMetric fallback so the SDK stays dependency-free when Prometheus is not installed. Wired into the top-level vcp package. 29 new tests, 0 regressions.
Adds hooks/config.py with load_from_yaml, load_from_dict, and build_registry. Supports all four built-in hooks and custom callables via dotted Python paths. Includes reference YAML example and 31 new tests. 0 regressions.
Add python/src/vcp/privacy.py — Python port of privacy.ts. Core invariant: private_fields_exposed == 0, always. Private field VALUES never appear in FilteredContext; they influence only boolean ConstraintFlags (7 flags: time_limited, budget_limited, noise_restricted, energy_variable, schedule_irregular, mobility_limited, health_considerations). Three field tiers: PUBLIC_FIELDS — always shared (display_name, goal, role, ...) CONSENT_REQUIRED_FIELDS — shared only with explicit ConsentRecord PRIVATE_FIELDS — NEVER shared; drive boolean flags only Key functions: extract_constraint_flags() — private context → booleans filter_context_for_platform() — consent-aware context filter get_stakeholder_visible_fields() — stakeholder (manager/hr) visibility get_share_preview() — preview before consent granted 90 new tests in test_privacy.py, including exhaustive privacy guarantee tests that confirm no PRIVATE_FIELDS value leaks regardless of manifest or consent claims. Suite: 707 passed (was 617), 3 pre-existing redis failures unchanged.
…+ fix HybridStateTracker.clear() Addresses two items from the VCP Status Report "Next Steps" (docs/VCP_STATUS_REPORT.md) and the Prometheus metrics spec (docs/VCP_INTEGRATION.md §Prometheus Metrics): 1. **Prometheus metrics instrumentation** — The metrics module (metrics.py, commit 6baebe5) defined 13 counters/histograms/gauges but none were called from application code. This commit wires every metric to its corresponding operation: Layer 1 (Identity): - vcp_token_lookups_total: Token.parse() ok/error + LocalRegistry.resolve() ok/miss - vcp_registry_size: gauge updated on LocalRegistry.register() Layer 2 (Transport): - vcp_bundle_verifications_total: Orchestrator.verify() labeled by result - vcp_bundle_verify_duration_seconds: verification latency histogram Layer 3 (Semantics): - vcp_csm1_parses_total: CSM1Code.parse() ok/error - vcp_compositions_total: Composer.compose() ok/conflict Layer 4 (Adaptation): - vcp_context_encodes_total: ContextEncoder.encode() - vcp_transitions_total: StateTracker.record() labeled by severity - vcp_active_sessions: gauge inc on first record, dec on clear Hooks: - vcp_hook_executions_total: per hook type + status - vcp_hook_duration_seconds: per hook type Audit: - vcp_audit_events_total: AuditLogger.log_verification() 2. **Bug fix: HybridStateTracker.clear()** — HybridStateTracker had no clear() method, so calling clear() only cleared in-memory state, leaving Redis stale. Added clear() that clears both backends + 2 tests (with/without Redis). All 712 tests pass.
…DPR purge - Enforce audit tier boundaries in AuditEntry.to_dict(): minimal level now only exposes bundle hash + verification result, not timestamps/ sessions/issuer/version (previously leaked at all levels) - Add AuditLogger.log_privacy_filter() for auditing context filtering operations with counts only (private_fields_exposed always 0) - Add AuditLogger.purge_by_session() for GDPR right to erasure - Bump vcp_audit_version to 1.1 per Amendment J spec - Add 33 tests covering tier boundaries, GDPR purge, privacy filter audit events, and hash privacy helper
…tering) High priority: - Add ALLOWED_IMPORT_PREFIXES allowlist to hook config _import_dotted_path to prevent arbitrary code execution via YAML action fields - Scope GDPR purge_by_session to in-memory only with tombstone receipt, document limitations for disk/callback/Redis persistence layers Medium priority: - Remove constraints dict from get_field_value search sources to prevent private data leaking through key collisions with public fields - Fix schedule detection false positive: only flag irregular/shift/rotating schedules, not regular ones like "9-5" - Use single "resolved" status label for token lookups to prevent metrics oracle that could distinguish existing vs non-existing tokens - Redact session_id_hash from STANDARD tier privacy-filter audit events - Wire up vcp_context_encode_duration_seconds in ContextEncoder.encode() Low priority: - Make PRIVATE_FIELDS a frozenset to prevent accidental mutation - Fix test assertion: use != (equality) instead of is not (identity) - Guard active session gauge against double-clear with _session_counted flag
…utc) datetime.utcnow() is deprecated since Python 3.12. Replace all occurrences across orchestrator, trust, injection, bundle, and adaptation modules with timezone-aware datetime.now(timezone.utc).
c34f129 to
4deab4d
Compare
|
Hi Nell, I have addressed all the items from your review: High priority
Medium priority
Low priority
The branch has been rebased onto current main. Let me know if anything needs further adjustment. |
…are datetimes) - Audit purge tests now check tombstone dict instead of int return value - Hook config tests use vcp.* imports to match allowlist, add test for rejected non-vcp imports - Integration tests use timezone-aware datetimes to match updated trust.py
Move _IRREGULAR_SCHEDULE_KEYWORDS and _is_irregular_schedule out of extract_constraint_flags to module level, fixing ruff N806 (uppercase variable in function) and E501 (line too long).
- audit.py: extract purge_id to local variable to avoid dict access typing issue (arg-type error on bundle_id_hash) - metrics.py: add type: ignore[assignment] for _prom = None fallback when prometheus_client is not installed
|
@NellInc This is ready for your review whenever you have a chance. All CI checks are passing. |
- Remove token parse metrics from Token.parse() that created an enumeration oracle undermining bloom filter anti-enumeration defense - Salt _hash_for_privacy with per-process random bytes so hashes are not reversible from known inputs - Add threading.Lock to AuditLogger for thread-safe GDPR purge - Remove session_id_hash from purge tombstone (the hash itself is PII) - Narrow hook config ALLOWED_IMPORT_PREFIXES from "vcp." to "vcp.hooks." and remove the bypassable allowed_prefixes parameter - Freeze PUBLIC_FIELDS and CONSENT_REQUIRED_FIELDS as tuples to prevent accidental mutation of the privacy boundary - Strengthen privacy guarantee test to assert on both result.public and result.preferences Co-Authored-By: Claude <noreply@anthropic.com>
NellInc
left a comment
There was a problem hiding this comment.
Adversarial Review
Strong work, Elena. The privacy architecture is well-designed, the spec references are thorough, and the test coverage is excellent (~1,500 lines). These findings are the kind that only surface under targeted adversarial analysis — none of them indicate carelessness.
I've pushed a commit (a9313e3) that addresses all of the below. 204/204 affected tests pass.
Fixed (in commit)
1. Token enumeration oracle via metrics (CRITICAL)
Token.parse() emitted vcp_token_lookups_total with status="ok" / status="error", while Registry.resolve() deliberately used a uniform status="resolved" label to avoid revealing whether a token exists. These two call sites on the same counter undermined each other — an attacker could diff parse-success vs resolve counts to infer token existence, defeating the bloom filter anti-enumeration defense.
Fix: Removed metric calls from Token.parse(). Parsing is format validation, not a lookup operation.
2. Unsalted _hash_for_privacy (HIGH)
Plain SHA-256 without a salt meant anyone who could guess the input (session IDs, platform IDs) could verify the hash. GDPR purge tombstones included the session_id_hash, making them reversible.
Fix: Added per-process random salt (os.urandom(32)) to _hash_for_privacy. Hashes remain deterministic within a process (required for purge matching) but are not reversible from known inputs across process restarts.
3. Tombstone leaked session_id_hash (HIGH)
The purge tombstone returned the target hash, which is itself PII if reversible.
Fix: Removed session_id_hash from tombstone. Only purge_id identifies the operation now.
4. ALLOWED_IMPORT_PREFIXES too broad + bypassable (HIGH)
The ("vcp.",) allowlist permitted importing any callable in the entire VCP package. The _import_dotted_path function also accepted allowed_prefixes as a parameter, so any caller could pass ("",) to import anything.
Fix: Narrowed to ("vcp.hooks.",). Removed the allowed_prefixes parameter — the module constant is now the sole source of truth. Added a test confirming vcp.audit.* is correctly rejected.
5. Mutable privacy boundary constants (HIGH)
PUBLIC_FIELDS and CONSENT_REQUIRED_FIELDS were plain list[str], while PRIVATE_FIELDS was correctly a frozenset. An accidental .append() at runtime would silently widen the fields shared with all platforms.
Fix: Changed both to tuple[str, ...].
6. GDPR purge race condition (MEDIUM)
purge_by_session was not thread-safe — concurrent log_verification calls could append entries between the length check and list rebuild.
Fix: Added threading.Lock around _entries mutation in purge.
7. Privacy test gap (MEDIUM)
test_private_field_blocked_even_if_in_public_profile only asserted pf not in result.preferences but not pf not in result.public. The invariant was half-checked.
Fix: Added the missing result.public assertion.
Not fixed (tech debt for follow-up)
verify()/_verify_inner()split: Method extraction solely for metrics wrapping.track_durationalready handles this inline. Consider a decorator instead.- Metrics scattered as inline calls: 8 modules import metrics directly into business logic. A decorator/middleware pattern would keep business logic metric-free as the SDK grows.
"private_fields_exposed:0"hardcoded: The audit log asserts zero leakage as a string constant rather than computing it. Worth deriving from actual filter output in a future pass.- No file size limit on YAML config load: A defensive
stat()check beforeyaml.safe_loadwould prevent multi-GB files from being loaded into memory.
Approving with the fixes already applied. Nice work on the Amendment J implementation and the privacy filtering architecture in particular — the "AI knows THAT constraints apply, never WHY" design is exactly right.
Co-Authored-By: Claude <noreply@anthropic.com>
NellInc
left a comment
There was a problem hiding this comment.
Re-approving after lint fix. All CI checks green.
Closes persistence-layer gaps from PR #23 code review and adds the PDP plugin enforcement that was flagged as unbuilt in the SDK. GDPR/audit fixes: - purge_by_session() now scrubs exported JSON files (not just in-memory) - Tombstone receipt includes file_entries_removed and files_purged - export_json() tracks paths for later purge propagation - Tests cover file purge, missing file handling, path tracking datetime.utcnow() deprecation: - Fix last 2 instances in identity/registry.py (default_factory) - All source files now use datetime.now(timezone.utc) PDP enforcement module (vcp/enforcement.py): - PDPPlugin abstract interface for enforcement plugins - PDPEnforcer orchestrator with priority ordering and fail-closed - RefusalBoundaryPlugin: bundle verification enforcement - AdherenceLevelPlugin: minimum adherence level gating - BundleExpiryPlugin: expired bundle blocking - 23 tests covering all plugins and edge cases Co-Authored-By: Claude <noreply@anthropic.com>
Context
The MDPI paper (Watson et al., "Value Context Protocol: A Standard for Inter-Agent Value Communication") defines VCP as a four-layer protocol stack (I-T-S-A) with conformance levels from VCP-Minimal through VCP-Enterprise. The paper specifies that VCP-Enterprise requires "multi-party signatures, append-only audit logs, and regulatory reporting" (§2.7), and the v1.1 Amendments (Junto Mastermind Critique) identified Amendment J: Audit Privacy Controls as a medium-priority gap.
The reference implementation shipped with the v3.1 extensions (PR #14) covered the core protocol: identity, transport, semantics, and adaptation. But it lacked the production infrastructure needed for VCP-Enterprise deployments: no observability, no privacy field filtering at the adaptation boundary, no configurable audit tiers, and hooks required hardcoded activation rules.
This PR closes those gaps.
What's in this PR
1. Prometheus Observability (vcp/metrics.py: 248 lines)
The paper's Section 2.3 (VCP/T) mandates audit logging for all bundle operations, but the spec says nothing about operational observability. In practice, any production deployment needs to answer: how many tokens are being resolved? What's the p99 latency on bundle verification? Are hooks firing as expected?
This adds counters, histograms, and gauges across all four layers: identity (registry lookups), transport (bundle verification), semantics (composition), and adaptation (state transitions). Uses a no-op fallback when prometheus_client is not installed, so there's zero runtime cost for lightweight deployments.
202 lines of tests.
2. Privacy Field Filtering (vcp/privacy.py: 541 lines)
The paper's three-layer model (§2.8-2.9) draws a hard boundary: "Personal state modulates expression, never boundaries." But the spec doesn't enforce what leaves the SDK. A Layer 4 context encoding contains personal dimensions (cognitive state, emotional tone, energy level, body signals) that should never leak to untrusted recipients without consent.
This implements field-level privacy filtering at the adaptation boundary. It extracts privacy constraints from active constitutions, redacts sensitive fields before context transmission, and integrates with the audit trail so redaction decisions are logged. This directly addresses the paper's normative constraint that Layer 3 data "MUST NOT be represented as medical, psychological, or clinical information" (§2.8, Layer 3 normative constraints): the filtering ensures these fields don't propagate where they shouldn't.
665 lines of tests.
3. Amendment J Audit Tiers + GDPR Purge (vcp/audit.py: 111 lines added)
Amendment J (§J in VCP v1.1 Amendments) identified audit privacy as a medium-priority gap: audit logs that record every bundle operation can themselves become a privacy liability. The amendment calls for privacy-preserving audit controls.
This implements tiered audit levels (from minimal metadata to full content logging) and GDPR-compliant purge capability that can remove audit records by retention policy while preserving the cryptographic chain integrity referenced in the paper's VCP/T audit logging spec (§2.3): "Audit logs are append-only and cryptographically chained for tamper-evidence."
255 lines of tests.
4. Hook Deployment YAML Config (vcp/hooks/config.py: 335 lines)
The paper's Section 2.10 defines three hook tiers: constitutional (creed-authored deterministic rules), situational (context-triggered activation), and personal (prosaic threshold triggers). The spec notes that situational and personal hooks are "currently hardcoded" in the plugin and "should be configurable."
This adds a declarative YAML-based configuration system for hook deployment, allowing operators to define hook rules, activation conditions, and execution order without modifying code. Includes an example config (examples/hooks_deployment.yaml) demonstrating all three hook tiers as described in the paper.
339 lines of tests.
5. HybridStateTracker Redis Fix
Fixed clear() method in the Redis-backed state tracker (vcp/adaptation/redis_state.py): the method wasn't properly flushing Redis keys, which could leave stale personal state signals in the cache. This matters because the paper's Context Lifecycle model (§2.11) specifies that signals transition through SET -> ACTIVE -> DECAYING -> STALE -> EXPIRED, and a broken clear() prevents proper lifecycle reset.
38 lines of regression tests.
By the numbers
What this does NOT include
Test plan