Architect grounding boundary: redaction/re-hydration privacy gate (v0.1.0:55)

Phase 1 Workstream D. Lets the Architect ground the thesis in REAL recurring LP
objections without any LP identity reaching the Claude API. Layered, defense-in-depth,
fail-closed by construction (docs/redaction-rehydration.md).

backend/redaction/:
- scrub.py: the leak-proof core. Drops Tier-1 (labelled/structured account/wire/SSN/
  IBAN/SWIFT/passport, separator-tolerant); tokenizes known LP entities (dictionary from
  the canonical layer, unicode-folded + hyphen-extended) and structured PII (emails,
  scheme-less/social URLs, intl+ext phones, currency-cued amounts, ISO/worded/numeric/
  quarter dates, addresses, bare long digit runs); pre-neutralizes injected [TYPE_N]
  strings; single-pass rehydrate; metadata-only audit logging (the pseudonym map is the
  de-anon key — local-only, never logged/sent). Hardened across THREE adversarial
  leak-hunts (worded/coded amounts, intl phones, NFD/ligature/zero-width names, slash/
  comma SSN, SWIFT, alpha-prefixed accounts, substance-preserving false-positive fixes).
- client.py: Boundary — one scrub/rehydrate contract, SCRUB_BACKEND=local (default) or
  gateway (Spark Control /scrub + /rehydrate). Fails closed (db_path required; dictionary
  build errors propagate; strict rehydrate returns tokenized-not-de-anon text).
- test_scrub_leak.py, test_reidentification.py: golden-file leak + re-identification
  suites (synthetic only, guardrail #9), regression-locking every leak-hunt vector.

backend/mcp/architect_grounding.py: the flow — retrieve (local) -> minimize-first
(local Qwen) -> scrub (+ local-Qwen NER backstop for unknown names) -> Claude over the
de-identified register only -> re-hydrate locally -> human review. FAILS CLOSED if the
local model is unreachable or a hallucinated token appears. test_grounding_boundary.py
proves nothing sensitive reaches Claude and the three fail-closed paths.

server.py: POST /api/architect/ground (admin) wires retrieval -> ground_objections.
docker_entrypoint.sh: SCRUB_BACKEND (default local). docs/spark-control-scrub-endpoints.md:
the gateway handover spec (Option 1 — caller supplies the entity dictionary).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Keysat
2026-06-05 17:06:29 -05:00
parent 300041a7ec
commit 2e70b34592
12 changed files with 1371 additions and 4 deletions
+5
View File
@@ -85,6 +85,11 @@ export CRM_DB_PATH="${CRM_DB_PATH:-$DATA_DIR/crm.db}"
export SPARK_CONTROL_URL="${SPARK_CONTROL_URL:-https://192.168.1.72:62419}"
export SPARK_CONTROL_VERIFY_TLS="${SPARK_CONTROL_VERIFY_TLS:-false}"
export QDRANT_URL="${QDRANT_URL:-http://192.168.1.87:6333}"
# Redaction boundary backend for the Architect's grounding step (Workstream D):
# local (default) = in-repo deterministic scrubber (backend/redaction/), map in-process.
# gateway = Spark Control POST /scrub + /rehydrate, once that ships.
# Flip to 'gateway' only after the Spark Control endpoints are live (same contract).
export SCRUB_BACKEND="${SCRUB_BACKEND:-local}"
# OPERATOR: how often (minutes) the background sync scheduler re-runs the
# incremental ingest sync to keep the Qdrant search index fresh. Default 60.
export CRM_INGEST_SYNC_INTERVAL_MIN="${CRM_INGEST_SYNC_INTERVAL_MIN:-60}"
+3 -2
View File
@@ -19,8 +19,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
// * 0.1.0:51 (entity-resolution fix: people double-count + duplicate queue)
// * 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door)
// * 0.1.0:53 (seed v5 thesis into the Architect Workshop)
// * Current: 0.1.0:54 (unification polish: LinkedIn in grid inline contact editor)
export const PACKAGE_VERSION = '0.1.0:54'
// * 0.1.0:54 (unification polish: LinkedIn in grid inline contact editor)
// * Current: 0.1.0:55 (Architect grounding boundary: redaction/re-hydration privacy gate)
export const PACKAGE_VERSION = '0.1.0:55'
export const DATA_MOUNT_PATH = '/data'
export const WEB_PORT = 8080
+3 -2
View File
@@ -15,8 +15,9 @@ import { v_0_1_0_51 } from './v0.1.0.51'
import { v_0_1_0_52 } from './v0.1.0.52'
import { v_0_1_0_53 } from './v0.1.0.53'
import { v_0_1_0_54 } from './v0.1.0.54'
import { v_0_1_0_55 } from './v0.1.0.55'
export const versionGraph = VersionGraph.of({
current: v_0_1_0_54,
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53],
current: v_0_1_0_55,
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54],
})
+22
View File
@@ -0,0 +1,22 @@
import { VersionInfo } from '@start9labs/start-sdk'
// The Architect grounding boundary (Phase 1 Workstream D): the redaction / re-hydration
// privacy gate so the Architect can ground its thesis in REAL LP feedback without sending
// any LP identity to Claude. A local-Qwen summary minimizes first, a deterministic scrubber
// (hardened across three adversarial leak-hunts) tokenizes residual identifiers, a local-Qwen
// NER pass backstops unknown names, Claude reasons only over the de-identified register, and
// the draft is re-hydrated locally for human review. Fails CLOSED if the local model is
// unreachable. SCRUB_BACKEND=local (default) or gateway (Spark Control /scrub + /rehydrate,
// once built). No data migration.
export const v_0_1_0_55 = VersionInfo.of({
version: '0.1.0:55',
releaseNotes: {
en_US: [
'Adds the Architect grounding boundary: the Architect can now pressure-test the thesis',
'against real recurring LP objections WITHOUT any LP name, email, amount, or other',
'identifier reaching Claude — everything is de-identified locally first and the draft is',
'restored on the server for your review. Sovereignty by construction; fails closed.',
].join(' '),
},
migrations: { up: async () => {}, down: async () => {} },
})