Skip to content

feat: VCP robustness layer: observability, privacy enforcement, audit compliance, hooks config#23

Merged
NellInc merged 17 commits intomainfrom
elena/backend-v3
Apr 4, 2026
Merged

feat: VCP robustness layer: observability, privacy enforcement, audit compliance, hooks config#23
NellInc merged 17 commits intomainfrom
elena/backend-v3

Conversation

@elenaajayi
Copy link
Copy Markdown
Collaborator

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

Source Tests
Files changed 16 5 new
Lines added ~1,400 ~1,500
Total tests 745 passing, 0 failures

What this does NOT include

  • Distributed registry backends (PostgreSQL/Redis): tracked separately
  • Real cryptographic signatures: still using simplified Ed25519
  • Content Safety Attestation (Amendment A): CRITICAL priority, separate effort
  • Key rotation lifecycle (Amendment F): separate effort

Test plan

  • All 745 tests pass (0 failures, 4 intentional skips for external services)
  • Metrics no-op fallback verified without prometheus_client
  • Privacy filtering tested with nested field redaction + constitution constraint extraction
  • Audit purge tested with retention policies + chain integrity
  • Hook config tested with all three tiers (constitutional, situational, personal)
  • Merge conflict with main resolved (messaging + competence types merged with metrics + privacy imports)

@elenaajayi elenaajayi requested a review from obliviance March 24, 2026 02:39
obliviance
obliviance previously approved these changes Mar 26, 2026
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).
@elenaajayi
Copy link
Copy Markdown
Collaborator Author

Hi Nell, I have addressed all the items from your review:

High priority

  • Hook config loader now has an ALLOWED_IMPORT_PREFIXES allowlist (defaults to vcp.* only) so action fields can no longer trigger arbitrary imports
  • GDPR purge_by_session now returns a tombstone receipt with purge ID, timestamp, and entry count. The docstring explicitly states this only covers in-memory entries and that callers are responsible for propagating purges to disk exports, log callbacks, and external stores

Medium priority

  • Removed the constraints dict from get_field_value search sources so private-derived values cannot leak through key collisions with public fields
  • Schedule detection now checks for specific keywords (shift, rotating, night, variable, irregular, on-call) instead of treating any non-None value as irregular
  • Token lookup metrics use a single "resolved" status label regardless of outcome, so the /metrics endpoint no longer reveals whether a token exists
  • Privacy-filter audit events redact session_id_hash at STANDARD tier, only including it at FULL and above
  • vcp_context_encode_duration_seconds is now wired up in ContextEncoder.encode()

Low priority

  • PRIVATE_FIELDS is now a frozenset
  • Fixed the test_no_private_value_leaks assertion to use equality (!=) instead of identity (is not)
  • Added a _session_counted flag to prevent the active session gauge from going negative on double clear
  • Replaced all datetime.utcnow() calls with datetime.now(timezone.utc) across the codebase

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
@elenaajayi
Copy link
Copy Markdown
Collaborator Author

@NellInc This is ready for your review whenever you have a chance. All CI checks are passing.

@elenaajayi elenaajayi requested a review from NellInc April 3, 2026 06:56
- 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
NellInc previously approved these changes Apr 3, 2026
Copy link
Copy Markdown
Contributor

@NellInc NellInc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_duration already 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 before yaml.safe_load would 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>
Copy link
Copy Markdown
Contributor

@NellInc NellInc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving after lint fix. All CI checks green.

@NellInc NellInc merged commit b28533a into main Apr 4, 2026
8 checks passed
@NellInc NellInc deleted the elena/backend-v3 branch April 4, 2026 00:00
NellInc added a commit that referenced this pull request Apr 6, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants