Skip to content

feat: enforce purpose-limited PII event protection - #803

Merged
seonghobae merged 22 commits into
mainfrom
codex/pii-purpose-encryption
Aug 25, 2026
Merged

feat: enforce purpose-limited PII event protection#803
seonghobae merged 22 commits into
mainfrom
codex/pii-purpose-encryption

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add AES-256-GCM field encryption for explicitly declared PII fields in audit and analytics event details.
  • Resolve encryption keys only through the existing KV credential registry; missing, malformed, or tampered protected data fails closed.
  • Map authenticated inference/admin roles to message_delivery, operator_read, and audit_replay purposes, and record secret-free authorization decisions.
  • Keep credential redaction and raw PII response usability intact; no blanket PII masking or automatic detector was added.
  • Add ADR 0024, library/research grounding, APA 7 references, and hash-locked cryptography dependency metadata.

Verification

  • Final exact-head commit: ec3b3c8
  • Full suite: 1446 passed
  • PII focused suite: 36 passed
  • pii_protection.py branch coverage: 100% (125 statements, 46 branches)
  • interrogate: 100%
  • pip-audit -r requirements.lock: no known vulnerabilities
  • compileall and git diff --check: passed

Scope note

Callers must pass pii_fields when an audit or analytics detail contains PII. This explicit declaration avoids unreliable PII detection and preserves usable content. Authorized admin replay decrypts protected fields; ordinary internal reads retain ciphertext. Key metadata supports retaining older KV keys during rotation.

This PR does not self-approve or bypass protected merge requirements. It awaits independent review and terminal GitHub Checks.


Open in Devin Review

Summary by CodeRabbit

  • 새 기능

    • 지정된 개인정보 필드를 AES-256-GCM으로 암호화합니다.
    • 관리자 감사 재생 권한이 있을 때만 보호된 상세 정보를 복호화합니다.
    • 역할·목적 기반 접근 제어와 인가 결정 감사 기록을 제공합니다.
    • 패스프레이즈 기반 키 생성과 명시적 키 형식을 지원합니다.
    • 감사·인가·분석 기록을 비동기적으로 저장하고 최신 256개까지 관리합니다.
    • 관리자 상태 및 감사·인가 기록 조회 필터를 제공합니다.
  • 문서

    • 목적 제한형 개인정보 보호 정책과 키 관리 기준을 문서화했습니다.
  • 버그 수정

    • 잘못된 권한, 키, 암호화 데이터가 평문을 노출하지 않고 안전하게 거부됩니다.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

AES-256-GCM 기반 PII 필드 보호를 추가했다. 감사 및 분석 이벤트는 지정 필드를 암호화해 저장한다. 관리자 감사 재생만 복호화한다. 관리자 경로는 scope별 purpose를 검증하고 인가 결정을 기록한다. 이벤트 스트림은 비동기로 저장하며 스트림별 보존 한도를 적용한다.

Changes

목적 제한형 PII 보호

Layer / File(s) Summary
PII 암호화 핵심
contextual_orchestrator/pii_protection.py, contextual_orchestrator/orchestrator.py, docs/planning/adrs/..., docs/library_research.md, tests/test_pii_protection.py
명시적 키 형식과 scrypt 패스프레이즈 파생을 지원한다. 지정 필드를 AES-256-GCM으로 암호화하고 암호화 봉투를 검증한다. 키 오류와 변조 데이터를 거부한다.
이벤트 저장 및 보존
contextual_orchestrator/orchestrator.py, tests/test_persistence.py, tests/test_pii_protection.py
감사, authorization 및 analytics 이벤트를 별도 스트림에 저장한다. 스트림 저장은 최대 2048개 대기열과 백그라운드 worker를 사용한다. 스트림별 보존 한도는 256이다.
목적 기반 관리자 인증
contextual_orchestrator/server.py, contextual_orchestrator/orchestrator.py, tests/test_pii_protection.py, docs/planning/adrs/...
SecurityConfig가 scope별 purpose를 검증하고 반환한다. 관리자 경로는 audit_replay 또는 operator_read purpose를 선택한다. 인가 결과와 인증된 role 및 purpose를 상태에 전달한다.
런타임 및 검증 지원
pyproject.toml, fuzz/*, .github/workflows/*, CLAUDE.md, conductor/tech-stack.md, docs/*, tests/*
cryptography 런타임 및 퍼징 잠금을 갱신했다. PII 키 경계 퍼징과 속성 테스트를 추가했다. 관련 문서와 테스트 설치 설명을 수정했다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to f646b

This change encrypts declared PII and alters event persistence behavior, but the current implementation can drop audit events, hang after save failures, and accept or expose improperly protected data. These availability, integrity, and confidentiality risks must be fixed or explicitly accepted before merging.

Sequence Diagram(s)

sequenceDiagram
  participant AdminClient
  participant SecurityConfig
  participant TaskOrchestrator
  participant PiiFieldEncryptor
  participant SQLite

  AdminClient->>SecurityConfig: scope 및 purpose 전달
  SecurityConfig->>SecurityConfig: purpose 검증
  SecurityConfig->>TaskOrchestrator: role 및 purpose 반환
  AdminClient->>TaskOrchestrator: PII 이벤트 기록
  TaskOrchestrator->>PiiFieldEncryptor: 지정 필드 암호화
  PiiFieldEncryptor-->>TaskOrchestrator: 암호화 봉투 반환
  TaskOrchestrator->>SQLite: 이벤트 스트림 비동기 저장
  AdminClient->>TaskOrchestrator: audit_replay 요청
  TaskOrchestrator->>PiiFieldEncryptor: 감사 이벤트 복호화
  PiiFieldEncryptor-->>AdminClient: 복호화된 이벤트 또는 unavailable 반환
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.23% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 65 functions across 8 files. (3 skipped: 3 unsupported.) Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 감사 및 분석 이벤트에 목적 제한 PII 보호를 추가하는 PR의 주요 변경 사항을 명확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/pii-purpose-encryption

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Reverification after review fixes (current HEAD 5e0611439972f93e86c8047ab239c0a35310fe21):\n\n- uv run --with pytest pytest -q: 1447 passed in 547.03s\n- focused PII/security/persistence suite: 37 passed\n- pii_protection.py: 100% branch coverage (125 statements, 46 branches)\n- interrogate: 100%\n- pip-audit: no known vulnerabilities\n- compileall and diff check: passed\n\nThe follow-up fixes cache KV-loaded encryptors, hide key bytes from dataclass repr, and record authorization denials plus successful sensitive audit replays only. Routine successful traffic continues through the existing analytics path. The PR remains open and protected; independent approval and terminal Checks are still required.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head remediation record: base e226e11, head 33f312c. The source security finding is fixed by bounding durable audit retention to 256 rows; CI runtime locks now include cryptography and greenlet with hashes. Local evidence on this exact tree: 1448 passed in 527.54s, 38 focused persistence/security tests passed, hash-locked install plus editable install succeeded, actionlint/compileall/diff-check passed. Devin and CodeRabbit dispositions are recorded; all review threads are resolved. Hosted checks are rerunning and currently pending, so this remains a normal protected merge candidate, not a guarded-force candidate.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head remediation follow-up (HEAD 5c51c3a9, parent 33f312c7).

  • Devin 3828428696: fixed at the root. Authorization decisions now use a separate bounded in-memory _authorization_events stream and durable authorization SQLite kind, so denial or replay-auth churn cannot evict substantive PII audit events. Both streams retain 256 newest records and reload independently.
  • Devin 3828428819: fixed. Authorized replay now degrades only an undecryptable/malformed row to its ciphertext envelope plus the non-sensitive marker __pii_protection_error__: unavailable; other audit rows and the admin payload remain available. No exception/key material is returned.
  • Devin 3828429001: disposition unchanged. Caching is by key name intentionally; ADR 0024 represents rotation by introducing a new key name and retaining old names until expiry/re-encryption.
  • Devin 3828429147: canonical docs now describe the stdlib HTTP/core plus the selected cryptography runtime dependency; the existing hash-locked requirements and CI install paths are retained.
  • Devin 3828429257: raw workflow/access/evaluation traces and /admin/state remain audit_replay surfaces by design. The stream-separation fix removes their authorization-decision noise from the substantive PII audit stream.
  • Devin 3828429448: fail-closed behavior remains only for sensitive audit_replay authorization auditing; ordinary operator-read routes do not write a successful replay decision.
  • Devin 3828429642 and 3828429788: no source change warranted; analytics envelopes are intentionally write-only, and the current GET path authorizes before consuming the per-request role/purpose.

Exact-head local evidence after this remediation:

  • uv run --with pytest pytest -q: 1451 passed.
  • focused PII/persistence/admin suite: 23 passed.
  • coverage: pii_protection.py 100% statement/branch; repository aggregate 90% statement with 146 partial branches (existing uncovered areas remain, so the user-requested global 100% gate is not claimed).
  • interrogate: 95.9% repository aggregate, configured threshold 80%.
  • pip-audit -r requirements.lock: no known vulnerabilities.
  • Semgrep: 0 findings; actionlint, compileall, and git diff --check: passed.

Hosted evidence for 5c51c3a9 is newly running and must be re-read at this exact head. This remains a normal protected-merge candidate; no emergency bypass is requested.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head remediation follow-up (HEAD , parent ).\n\n- Devin : fixed at the root. Authorization decisions now use a separate bounded in-memory stream and durable SQLite kind, so denial or replay-auth churn cannot evict substantive PII audit events. Both streams retain 256 newest records and reload independently.\n- Devin : fixed. Authorized replay now degrades only an undecryptable/malformed row to its ciphertext envelope plus the non-sensitive marker ; other audit rows and the admin payload remain available. No exception/key material is returned.\n- Devin : disposition unchanged. Caching is by key name intentionally; ADR 0024 represents rotation by introducing a new key name and retaining old names until expiry/re-encryption.\n- Devin : canonical docs now describe the stdlib HTTP/core plus the selected runtime dependency; the existing hash-locked requirements and CI install paths are retained.\n- Devin : raw workflow/access/evaluation traces and remain surfaces by design. The stream-separation fix removes their authorization-decision noise from the substantive PII audit stream.\n- Devin : fail-closed behavior remains only for sensitive authorization auditing; ordinary operator-read routes do not write a successful replay decision.\n- Devin and : no source change warranted; analytics envelopes are intentionally write-only, and the current GET path authorizes before consuming the per-request role/purpose.\n\nExact-head local evidence after this remediation:\n- ........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 19%]
........................................................................ [ 24%]
........................................................................ [ 29%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 44%]
........................................................................ [ 49%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 64%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 89%]
........................................................................ [ 94%]
........................................................................ [ 99%]
........... [100%]
1451 passed in 528.59s (0:08:48): 1451 passed.\n- focused PII/persistence/admin suite: 23 passed.\n- coverage: 100% statement/branch; repository aggregate 90% statement with 146 partial branches (existing uncovered areas remain, so the user-requested global 100% gate is not claimed).\n- interrogate: 95.9% repository aggregate, configured threshold 80%.\n- : no known vulnerabilities.\n- Semgrep: 0 findings; actionlint, compileall, and : passed.\n\nHosted evidence for is newly running and must be re-read at this exact head. This remains a normal protected-merge candidate; no emergency bypass is requested.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head follow-up for 606eb3788681bf04928c5be9325f2ca499412069 (base e226e1197bdfc890c9d8e5b9b648c78857d7e465):

  • The latest Devin rate-limit finding was checked against the current implementation. Authorization decisions use the separate authorization stream; _StateStore._STREAM_LIMITS["authorization"] is 256, and each durable insert prunes that stream to the newest 256 rows. It cannot grow the audit table without bound or evict substantive audit events.
  • Added only tests/test_persistence.py::test_durable_authorization_retention_is_bounded as a regression proof. No additional production change was warranted for this finding.
  • Focused PII/persistence/security suite: 43 passed in 3.69s.
  • Full suite: 1453 passed in 523.18s.
  • pip-audit -r requirements.lock: no known vulnerabilities.
  • Semgrep Python scan: 0 findings; actionlint, compileall, and git diff --check passed.
  • Interrogate: 95.9% against the repository's 80% minimum. PII protection module statement/branch coverage remains 100%; aggregate repository coverage remains 90% statement with partial branches recorded honestly.
  • Existing findings were dispositioned as fixed, stale, or intentional against prior exact heads; please review this new head again. No formal approval is present, and hosted required checks are still pending.

Decision remains WAIT_AND_REMEDIATE: normal protected merge only after exact-head hosted checks and required independent approvals are terminal.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head update: pushed 9f8b094b to codex/pii-purpose-encryption after removing the unused os import that made the security test contract fail Ruff. The two current denial-retention findings were rechecked against the preceding exact tree and are stale: authorization decisions already use a separate bounded authorization stream (256 durable rows) and cannot evict substantive PII audit events. Focused PII/persistence/security tests: 43 passed; Ruff and git diff --check pass. Hosted checks and an independent approval must be re-established for the new head; this remains WAIT_AND_REMEDIATE, with no bypass.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head verification update

  • Exact current head: 1f836527a718374585c5fde7838148cfa22765b2
  • Exact base: e226e1197bdfc890c9d8e5b9b648c78857d7e465
  • Head tree: a3bff9a5d6b4255ce8953b73da2985af9953ea0a
  • Scope since the prior reviewed head: one documentation line correcting the runtime cryptography dependency description; normal feature-branch push, no rewrite.
  • Focused PII/security tests: 34 passed
  • Full suite: 1453 passed
  • Configured statement/branch coverage: 90% aggregate; pii_protection.py 100%
  • Public docstring coverage: 95.9% (interrogate)
  • SAST: Semgrep 0 findings
  • Dependency audit: pip-audit -r requirements.lock reported no known vulnerabilities
  • Workflow/compile/diff checks: actionlint, compileall, and git diff --check passed
  • Packaging: uv build and isolated wheel install/import smoke passed
  • Full/coverage notes: three existing SQLite ResourceWarnings; no test failures. gitleaks and Trivy are not installed in this local environment; hosted Security remains authoritative.

Review disposition:

  • The runtime-dependency documentation finding was valid and fixed in this head.
  • The analytics-retention-256 suggestion is not supported by ADR 0024 or the existing 512-entry analytics contract; it was not changed.
  • Prior authorization-stream, bounded-retention, and undecryptable-replay findings have exact-head dispositions in the review history.

Live gate remains protected and incomplete: open, non-Draft, mechanically mergeable but blocked; current head has 7 skipped and 15 queued check-runs, with no formal approval. This is normal WAIT_AND_REMEDIATE state, not D1-D5 deadlock. No merge or bypass was performed.

@seonghobae
seonghobae enabled auto-merge (squash) August 21, 2026 16:49
@opencode-agent
opencode-agent Bot disabled auto-merge August 21, 2026 20:27
@seonghobae

Copy link
Copy Markdown
Contributor Author

Review repairs pushed at exact head 35692d5. The analytics stream and durable analytics retention now both use the 256-event contract. PII key parsing now rejects unprefixed raw 32-byte strings, preserves explicit base64:/hex: generated-key formats, and supports passphrase: through stdlib scrypt with key-name domain separation; tests cover deterministic derivation and rejection. Updated ADR 0024 and the research register with APA-style RFC 7914 evidence. Verification: full pytest 1455 passed; compileall and diff checks passed. Hosted checks are rerunning; no merge attempted without independent approval.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@opencode-agent
opencode-agent Bot disabled auto-merge August 23, 2026 08:57
@seonghobae
seonghobae enabled auto-merge (squash) August 23, 2026 10:01
devin-ai-integration[bot]

This comment was marked as resolved.

…ryption

# Conflicts:
#	contextual_orchestrator/orchestrator.py
#	contextual_orchestrator/server.py
#	fuzz/requirements-atheris.in
#	fuzz/requirements-atheris.txt
#	pyproject.toml
Rebase onto #769's runtime boundary: hypothesis moves out of runtime deps
(stays in the test extra), cryptography joins as the only production
dependency for PII field encryption. Regenerated requirements.lock via the
canonical pip-compile command (hypothesis-free, hash-locked) and both fuzz
lockfiles; hand-preserved the CPython 3.12 Atheris marker per its contract
test.
devin-ai-integration[bot]

This comment was marked as resolved.

Resolves fresh lockfile conflicts: uv.lock and requirements.lock
regenerated (uv lock / pip-compile), fuzz/requirements-property.txt
regenerated via uv pip compile -- none hand-merged.
@seonghobae
seonghobae enabled auto-merge (squash) August 25, 2026 00:58

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

Open in Devin Review

Comment on lines +1678 to +1712
def _drain_stream_queue(self) -> None:
while True:
with self._stream_condition:
while not self._stream_closing and not any(self._stream_events.values()):
self._stream_condition.wait()
event = self._next_stream_event()
if event is None:
return
kind, key, payload = event
self._stream_writing = True
try:
self._save_sync(kind, key, payload)
except Exception: # noqa: BLE001 - a best-effort stream write must not stop later persistence.
pass
finally:
with self._stream_condition:
self._stream_writing = False
self._stream_condition.notify_all()

def _next_stream_event(self) -> tuple[str, str | None, dict[str, Any]] | None:
"""Return one pending event fairly; caller holds ``_stream_condition``."""
kinds = tuple(self._STREAM_LIMITS)
for offset in range(len(kinds)):
index = (self._next_stream_index + offset) % len(kinds)
kind = kinds[index]
if self._stream_events[kind]:
self._next_stream_index = (index + 1) % len(kinds)
key, payload = self._stream_events[kind].popleft()
return kind, key, payload
return None

def _flush_streams(self) -> None:
with self._stream_condition:
while self._stream_writing or any(self._stream_events.values()):
self._stream_condition.wait()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: Async stream worker has no lost-wakeup/deadlock

The _StateStore background worker uses a single notify() in save(), but the worker is the sole waiter on an empty queue and re-checks the queue each loop; _flush_streams only waits while work is pending or in flight. _stream_condition is released before _save_sync takes _lock, so load()/close() cannot deadlock. Verified no hang or dropped events.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1657 to +1665
def save(self, kind: str, key: str | None, payload: dict[str, Any], *, durable: bool = False) -> None:
if kind in self._STREAM_LIMITS and not durable:
with self._stream_condition:
if self._stream_closing:
raise RuntimeError("state store is closed")
self._stream_events[kind].append((key, payload))
self._stream_condition.notify()
return
self._save_sync(kind, key, payload)

@devin-ai-integration devin-ai-integration Bot Aug 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: Analytics and denials become best-effort async

_append_audit_event defaults durable=True, so audit writes stay synchronous (now DB-capped at 256). Only record_analytics_event and authorization denials go through the async best-effort queue, which can silently drop on queue overflow or a failed worker write. Intended per the ADR, but analytics/denials are no longer guaranteed durable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread fuzz/requirements-atheris.txt Outdated
Comment on lines +3156 to 3171
def record_analytics_event(
self,
event_name: str,
detail: dict[str, Any],
*,
pii_fields: Iterable[str] = (),
) -> None:
"""Record a compact in-memory analytics event without prompt or output text."""
require_object_name(event_name, "analytics.event_name")
event = {
"event_time": int(time.time()),
"event_name": event_name,
"event_detail": redact_value(detail),
"event_detail": redact_value(self._protected_event_detail(detail, pii_fields)),
}
self._analytics_events.append(event)
if self._store is not None:

@devin-ai-integration devin-ai-integration Bot Aug 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: Analytics-encrypted PII is never decryptable

record_analytics_event encrypts declared pii_fields, but only list_recent_audit_events has a decrypt/replay path (admin + audit_replay). Analytics PII envelopes have no recovery surface, so PII placed there is effectively write-only. Appears deliberate; noted for awareness.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@opencode-agent
opencode-agent Bot disabled auto-merge August 25, 2026 01:09
@seonghobae
seonghobae enabled auto-merge August 25, 2026 01:51
@opencode-agent
opencode-agent Bot disabled auto-merge August 25, 2026 02:00
@seonghobae

Copy link
Copy Markdown
Contributor Author

Merge-gate evidence (2026-08-24): Deep diff review + fixes applied; all required checks green on current head except strix (org-wide NVIDIA NIM quota exhaustion — external provider-capacity blocker; serialization fix in ContextualWisdomLab/.github#1297). Full local suite green on this head.

# Conflicts:
#	.github/workflows/fuzz.yml
#	contextual_orchestrator/orchestrator.py
#	docs/fuzzing.md
#	fuzz/targets.py

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

Open in Devin Review

Comment on lines +1946 to +1947
if not isinstance(pii_key_name, str) or not pii_key_name:
raise ValueError("pii_key_name must be a non-empty string")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 State store thread leaks when PII key name is rejected

The durable state store starts its background thread and opens its sqlite connection (_StateStore(state_db)) before pii_key_name is validated. An invalid name then raises ValueError, orphaning that thread and connection for the life of the process because close() never runs.

Prompt for agents
In TaskOrchestrator.__init__ the pii_key_name validation (the isinstance/non-empty check that raises ValueError) is placed after self._store = _StateStore(state_db) is created. _StateStore.__init__ starts a daemon worker thread and opens a sqlite connection, and only close() tears those down. When pii_key_name is invalid the ValueError propagates out of __init__ before the caller ever gets a reference to close(), leaking the thread and connection. Move the pii_key_name validation (and assignment of self._pii_key_name / self._pii_encryptors) so it runs before the _StateStore is constructed, so an invalid argument fails before any resource is allocated.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread docs/fuzzing.md
Comment on lines +34 to +35
5. **PII key boundary** — unprefixed encryption-key text must be rejected;
accepted key material must declare `base64:`, `hex:`, or `passphrase:`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate list number in fuzzing doc

The new PII key-boundary target is numbered 5., the same as the reasoning-effort profile entry directly below it. The target list now has two item 5s and no item 6.

Prompt for agents
The numbered target list in docs/fuzzing.md now contains two entries labeled 5: the newly inserted PII key boundary item and the pre-existing reasoning-effort profile item that follows it. Renumber so the PII key boundary is 5 and the reasoning-effort profile is 6 (matching the 7/8 renumbering already applied in fuzz/targets.py).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +3254 to +3280
if role != "admin" or purpose != "audit_replay":
return selected
restored: list[dict[str, Any]] = []
encryptors: dict[str, Any] = {}
for event in selected:
detail = event.get("event_detail")
if not is_encrypted_detail(detail):
restored.append(event)
continue
restored_event = dict(event)
try:
metadata = detail.get(ENCRYPTED_FIELDS_KEY)
key_name = metadata.get("key_name") if isinstance(metadata, dict) else self._pii_key_name
if not isinstance(key_name, str) or not key_name:
raise PiiProtectionError("encrypted field metadata has no valid key name")
encryptor = encryptors.get(key_name)
if encryptor is None:
encryptor = load_pii_encryptor(key_name)
encryptors[key_name] = encryptor
restored_event["event_detail"] = encryptor.decrypt_fields(detail)
except PiiProtectionError:
restored_event["event_detail"] = {
**detail,
"__pii_protection_error__": "unavailable",
}
restored.append(restored_event)
return restored

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 Info: PII plaintext exposure limited to admin audit_replay

Decryption in list_recent_audit_events is gated on role admin and purpose audit_replay, and the purpose is chosen by the route via _admin_purpose(path) rather than request data, so callers cannot escalate. Only /admin/state maps to audit_replay; the other admin_state() caller passes no role/purpose and sees ciphertext. Consequence worth noting: every /admin/state GET returns plaintext PII to any admin-token holder and forces a synchronous durable authorization write that returns 503 if it fails.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

# Conflicts:
#	contextual_orchestrator/server.py
@seonghobae

Copy link
Copy Markdown
Contributor Author

Merge-gate evidence (2026-08-25): Integrated with the merged opaque-session auth design (#788) — purpose resolution now composes with session/bearer validation, denials and audit_replay access are audited, and state-changing admin routes keep the same-origin check. Full local suite green on this head. All required hosted checks green except strix (org-wide NVIDIA NIM quota exhaustion — external provider-capacity blocker; serialization fix in ContextualWisdomLab/.github#1297).

@seonghobae
seonghobae merged commit 40f4f6d into main Aug 25, 2026
32 of 33 checks passed
@seonghobae
seonghobae deleted the codex/pii-purpose-encryption branch August 25, 2026 06:36
seonghobae added a commit that referenced this pull request Aug 25, 2026
#803 enforcement landed; the admin console row now describes the active
control (purpose-authorized roles, field encryption, audited release)
instead of the pre-implementation Proposed label.
seonghobae added a commit to seonghobae/contextual-orchestrator that referenced this pull request Aug 25, 2026
…omLab#762)

* docs: design purpose-limited PII protection

* docs(security): close PII ADR review gaps

* fix: align admin PII policy with non-masking ADR

* fix: label proposed PII controls and canonicalize ADR

* test: assert the landed purpose-limited PII control row

ContextualWisdomLab#803 enforcement landed; the admin console row now describes the active
control (purpose-authorized roles, field encryption, audited release)
instead of the pre-implementation Proposed label.

---------

Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
seonghobae added a commit that referenced this pull request Aug 25, 2026
* docs: track embedding integration PR

* docs: record reasoning profile regression proof

* docs: refresh latest gateway and judge evidence

* docs: record review gateway full-suite proof

* docs: refresh exact product gap evidence

* docs: refresh release gap baseline

* docs: resolve baseline review findings

* docs: bind baseline to current PR head

* docs: refresh latest gap baseline heads

* docs: refresh current PR gap baseline

* docs: bind gap baseline to current head

* docs: avoid stale self head evidence

* docs: record central gateway dependency

* docs: reserve unique product baseline ADR number

* docs: refresh embedding ADR evidence

* docs: record merged trace authorization stack

* docs: record completed stacked merges

* docs: bind baseline to current provider stack

* docs: bind multimodal stack to current head

* docs: refresh product gap baseline evidence

* docs: record stacked multimodal merge

* docs: record Strix rerun evidence

* docs: add parent regression evidence

* docs: refresh central gateway evidence

* docs: refresh current PR and release evidence

* docs: record current pool-gate and release evidence

* docs: clarify central gateway migration boundary

* docs: bind baseline to latest release evidence

* docs: bind baseline to latest exact heads

* docs: record atheris PR promotion

* docs: refresh exact release evidence

* docs: bind baseline to current release heads

* docs: record independent current-head proof

* docs: keep observed PR evidence exact

* docs: record live protected approval requirements

* docs: refresh live product gap heads

* docs: restore PR inventory table rendering

* docs: record database naming gap

* docs: bind naming proof to current head

* docs: refresh product gap baseline evidence

* docs: add agent pool gap evidence

* docs: refresh stacked PR baseline heads

* docs: record reconciled passthrough stack

* docs: refresh central gateway prerequisite

* docs: record partial endpoint race predecessor

* docs: record ledger and hourly loop PRs

* docs: refresh hourly gateway prerequisite

* docs: refresh cost ledger evidence head

* docs: refresh central gateway prerequisite head

* docs: record cost ledger full verification

* docs: record exact cost ledger verification

* docs: track closed hourly caller and active central PR

* docs: refresh exact stacked PR evidence

* docs: refresh agent-pool stack evidence

* docs: refresh cost-ledger stack evidence

* docs: record naming and ledger repair heads

* docs: record stable current stacked heads

* docs: correct current cost-ledger stack evidence

* docs: record exact append rollback suite

* docs: refresh current PR heads and proof boundaries

* docs: refresh current PR evidence snapshot

* docs: record duplicate scheduler closure

* docs: record canonical central scheduler and stack proof

* docs: refresh model discovery coverage evidence

* docs: record chat capability security repair

* docs: refresh currency ranking evidence

* docs: record PR 765 security repair evidence

* docs: record PR 765 SSRF repair

* docs: record stacked CLI and lint PRs

* docs: track current CLI stack

* docs: record exact PR 765 suite evidence

* docs: record responses review disposition

* docs: pin external scheduler evidence

* docs: refresh provider discovery head

* docs: refresh gateway stack evidence

* docs: refresh gateway route evidence

* docs: refresh baseline for current PR queue

* docs: align baseline snapshot timestamp

* docs: record current CLI test repair

* docs: refresh baseline for current PR heads

* docs: refresh PR 805 exact-head baseline

* docs: refresh product technical gap snapshot

* docs: refresh PR 803 exact-head evidence

* docs: record stacked release authority verification

* docs: record current stacked PR 805 head

* docs: refresh PR 805 exact-head evidence

* docs: record PR 807 and 808 live gates

* docs: refresh PR 807 verification baseline

* docs: refresh PR 805 exact head

* docs: record PR 809 verification baseline

* docs: refresh PR 802 exact head

* docs: refresh PR 771 and 805 states

* docs: record current capability PR evidence

* docs: invalidate stale tool-fallback evidence

* docs: record latest live PR gate states

* docs: record full PR 810 local verification

* docs: record remote remediation PRs

* docs: record exact PR 803 audit remediation

* docs: record normal stack merges

* docs: distinguish baseline and live recheck times

* docs: record current PII retention verification

* docs: record stale no-op stack item

* docs: record exact current PR verification

* docs: refresh exact PR gate evidence

* docs: record queue dependency triage

* docs: record bounded PR verification

* docs: record protected auto merge state

* docs: record central CodeQL follow-up

* docs: refresh central exact-head control-plane evidence

* docs: record central OSV repair successor

* docs: classify superseded central hosted evidence

* docs: refresh contextual live PR heads

* docs: refresh scheduler current-head evidence

* docs: record OSV reporter contract repair

* docs: refresh contextual PR gate evidence

* docs: classify current central audit gates

* docs: refresh latest central check counts

* docs: refresh central PR evidence

* docs: record current OIDC caller evidence

* docs: refresh consolidated central stack evidence

* docs: refresh hosted gate counts

* docs: refresh contextual hosted gate evidence

* docs: refresh central live-head evidence

* docs: record current central docstring verification

* docs: record current OIDC caller verification

* docs: refresh OTEL and sampling PR evidence

* docs: refresh central stack and root evidence

* docs: record current OIDC stack verification

* docs: record restacked coverage PR

* docs: refresh live PR deadlock evidence

* docs: record cross-fork OSV repair

* docs: record live governance and queue evidence

* docs: record central queue refresh

* docs: refresh contextual exact-head evidence

* docs: refresh agent pool head evidence

* docs: record branch coverage evidence

* docs: refresh central restack evidence

* docs: refresh sampling PR evidence

* docs: refresh current OSV repair restack

* docs: consolidate backlog convergence and close the denial-recording DoS gap

All 29 open PRs are now independently verified clean (zero unresolved
threads, mergeable, green checks), blocked solely on the shared external
OpenCode App installation rate limit -- not a sampled subset as the prior
per-PR churn implied. Replace that granular, fast-staling bookkeeping with
one consolidated fact and strengthen the existing P0 delivery-gate row's
evidence accordingly.

Also record the authorization-denial persistence DoS found and fixed while
triaging #803 (unauthenticated denials were forcing synchronous,
lock-serialized disk commits shared with durable workflow_run/evaluation_run
state) and update the P1 PII gap row: #803 is now code-complete, not just
"open".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: record the admin-console UI-tooling deferral as ADR 0011

The standing org UI instructions require Figma/Storybook/ui-ux-pro-max/
Anti-Slop-UI for UI work, with the decision recorded in an ADR either way.
That decision existed only as one sentence inside the living gap-baseline
snapshot. Give it a standalone ADR: cites the existing Figma file
(vsZMd8WAv42HDRgcZuNcWk), states why a Node/Storybook toolchain isn't
warranted for one stdlib-only inline admin console today, and names three
concrete, checkable conditions that would make adoption correct rather than
optional.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: pin the canonical ADR directory in CLAUDE.md

PR #818's branch independently created docs/adr/0122-... while every other
ADR (0001-0011) lives in docs/planning/adrs/ -- no numeric collision, but no
documented convention either, so the drift will keep happening. Pin
docs/planning/adrs/ as canonical so future contributors converge without
needing to discover it by grepping prior PRs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: correct the Atheris fuzzing Python-version note

CLAUDE.md said "Python < 3.13"; pyproject.toml's actual fuzz extra marker
is atheris==3.1.0; python_version >= '3.12' (the opposite bound), matching
.github/workflows/fuzz.yml's comment that 3.1.0 covers both the 3.12 fuzz
runner and the central 3.14 coverage-evidence image. Found while checking
whether issue #95 (portable Atheris lock) is still open work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: record issue #95's closure in the gap-baseline queue

Follow-up to the CLAUDE.md Atheris-version fix: issue #95 is now closed
(resolved on main by a single version-gated pin, not the originally-scoped
two-way marker split), so drop its row from the open queue with a note on
why, matching this document's existing convention for stale/closed items.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: document the missing model_discovery.py module in CLAUDE.md

contextual_orchestrator/ has 13 real modules; CLAUDE.md's architecture
overview only documented 12 -- model_discovery.py (auto-discovery across
every KV-registered provider credential plus price-honest bootstrap
selection) was entirely absent, a real onboarding gap for a module this
central to the "auto model discovery" requirement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: link the two orphaned docs/*.md files from README's Design Artifacts

docs/fuzzing.md and docs/product-technical-gap-baseline.md both exist and
are referenced elsewhere (CLAUDE.md, this session's own gap-closing work)
but neither was linked from README's Design Artifacts index -- found by
diffing docs/*.md against README's linked set, same mechanical-check
approach that found the model_discovery.py doc gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fix stale class names in architecture.md's implementation mapping

The mapping named a class Agent and a class Orchestrator with route_once/
conduct methods; the actual code (verified directly) is ModelAgent and
TaskOrchestrator.route_once/.conduct. A reader tracing this doc into the
source would fail to find class Agent or class Orchestrator at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: document two undocumented API routes in README's Architecture section

Diffed server.py's real /api/v1/*/latest routes against README's documented
endpoint list: provider_readiness/latest and analytics_snapshots/latest
both exist and work but were never listed alongside the other 26 already
documented there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: correct stale admin-merge authorization claim, close 3-day plan gap

This track's "read this first" section claimed gh pr merge --admin is
"real, working, authorized... not a bypass to ask permission for each
time." Directly tested this session: it fails. Ruleset 18156473 has
bypass_actors: [] today -- the exit-condition procedure this same section
describes further down was apparently completed after iteration 76
(2026-08-20) without anyone coming back to correct the evergreen claim at
the top. A future agent trusting this file could waste real effort on a
bypass that no longer exists, or worse, believe it holds standing
authorization it doesn't.

Corrected the claim in place (kept for history, with the correction
directly above it), fixed the stale iteration-10 pointer, and added a full
dated Status entry closing the 3-day gap between this file and the
session's actual work (5 PRs converged clean, a real DoS fix, issue #95
closed on verified evidence, and 6 doc-accuracy fixes across the repo) --
this file's own convention is to log every iteration, and it hadn't been.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fix the same stale Agent/Orchestrator names in tech-stack.md

Same bug as f5f9c2a's architecture.md fix, found in a second file:
conductor/tech-stack.md's DDD layer mapping named Agent and Orchestrator,
which don't exist in the source. Real names are ModelAgent and
TaskOrchestrator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: address baseline review gaps

* docs(agents): remove stale KV known-deviation; align openai example with credential_key

AGENTS.md still claimed ModelClient reads os.environ.get(agent.api_key_env). The runtime already resolves provider keys and server tokens from the KV registry via get_credential(). Update the guidance and the example agent pool to use the modern credential_key field.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: restore document-mismatch as a blocked-status cause on 4 commercial reports

An 18-agent workflow audit (sweep + adversarial verify) found 9 real,
independently-confirmed documentation/code discrepancies.

Real code bug: commercial_procurement_readiness_report,
commercial_contract_readiness_report, commercial_onboarding_readiness_report,
and commercial_operations_readiness_report each omit "document mismatch"
from their blocked-status rule string, while 10+ sibling report methods in
the same file correctly include it. The blocking logic itself
(blocked_count = ... + len(concrete_blockers)) already treats document
mismatches as real blockers via commercial_release_candidate_report's
release_gates -- only the buyer-facing rule-string explanation silently
dropped the phrase on these four reports, misdescribing what actually
blocks the status. Fixed all four rule strings and added a regression test
per report (none existed before; this was unguarded).

Doc-only fixes: docs/commercial_launch_readiness.md and
docs/analytics_spec.md named a field, commercial_launch_external_input_count,
that has never existed in the API -- the real field is
launch_summary.external_input_group_count, already correctly locked by an
existing runtime test. Updated both docs and the two doc-text-presence
assertions in test_plugin_driven_artifacts.py that were locking the wrong
string. docs/fuzzing.md had three separate stale claims (wrong Python
version, a Targets list missing 2 of the actual 6 fuzzed surfaces, and a
"running locally" list missing the 5th command) -- fixed all three, and
fixed fuzz/targets.py's own docstring inconsistency (said "five surfaces"
above a list of six) found in the same pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: log iteration 78 in the track plan (ultracode doc-audit workflow)

Per iteration 77's own checklist: keep this file updated going forward,
not just session-local memory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: fix a third occurrence of the stale Agent/Orchestrator names

conductor/workflow.md's DDD glossary had the same bug already fixed in
architecture.md (f5f9c2a) and tech-stack.md (49fb1f8). Grepped the whole
repo's *.md files for the pattern after this fix -- confirmed no remaining
occurrences.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: close the remaining gap to 100% docstring coverage

interrogate reported 95.8% against the org's 100% target (well above the
enforced 80% CI gate) with exactly 11 missing docstrings -- small and
bounded, unlike a full re-audit. Added one-line docstrings to all 11:
5 in cost_ledger.py (NoopUsageTelemetrySink.emit_usage,
InMemoryUsageTelemetrySink.emit_usage/events, UsageTelemetryHealth.as_dict,
NonBlockingLedgerStore.telemetry_health) and 6 in server.py (the Handler
class itself, do_GET/do_PATCH/do_DELETE/do_POST/log_message).
interrogate now reports 100.0%.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: log iteration 79 (docstring coverage close, peer coordination round)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: keep protected-merge evidence exact

* docs: refresh exact protected-gate evidence

* docs: include current local-test PR evidence

* docs: refresh exact protected-gate evidence

* docs: record capability-safe discovery evidence

* docs: refresh model discovery evidence

* docs: track LineageWeave CLI gate

* docs: track pending shared Strix repair

* docs: refresh shared Strix repair head

* docs: track active Strix successor

* docs: link Strix stack parent

* docs: record shared Strix rollout block

* docs: refresh central Strix recovery evidence

* docs: clarify protected Strix bootstrap block

* docs: record shared Strix fallback failure

* docs: ground LineageWeave consumer gate

* docs: assign unique admin UI ADR number

* docs: record model-group feature gap evidence

* docs(launch): align renamed analytics field

* docs: log 2026-08-25 continuation (model groups, free discovery, hourly loop, queue recheck)

* docs(baseline): remove transient model coupling

* docs(baseline): refresh scheduler exact head

* docs: refresh capability-group exact-head baseline

* docs: record normalized group admin stack

* docs: refresh capability routing exact-head evidence

* docs: record modality discovery remediation evidence

* docs: trace model group product specification

* docs: record exact v0.2.0 candidate evidence

* docs: record adjacent PR remediation heads

* docs: record free orchestration and compose evidence

* docs: refresh free orchestration exact head

* docs: refresh reviewed orchestration evidence

* docs: record structured free judge evidence

* docs: refresh reasoning stream exact-head evidence

* docs: record responses ledger gap

* docs: refresh multimodal virtual model evidence

* docs: bind multimodal evidence to current stack

* docs: refresh free routing exact-head evidence

* docs: record free routing stack merge

* docs: refresh exact model routing delivery baseline

* docs: align fuzz target inventory

* docs: refresh exact-head protected queue evidence

* docs: refresh model discovery gap evidence

* docs: record group judge routing repair

* docs: correct exact routing head identity

* docs: refresh protected-main and free catalog evidence

* docs: include model judge fuzz command

* docs: refresh exact-head PR evidence

* docs: record telemetry repair and PR decomposition

* docs: refresh exact-head routing and CI gaps

* docs: record current routing and telemetry heads

* docs: record model-group API review repair

* docs: record effective catalog KV repair

* docs: record telemetry full-suite evidence

* docs: record catalog sync operator guidance

* docs: align gap baseline with current PRD

* docs: record Bytez chat discovery repair

* docs(adr): reserve admin console decision identifier

* docs: reserve cross-PR ADR identifiers

* docs: refresh exact-head product gap evidence

* docs: sync scheduled-loop exact head

* docs: separate contract and strategic value evidence

* docs: record secured k6 exact-head evidence

* docs: track orphaned performance recovery gap

* docs: refresh exact-head delivery evidence

* docs: record ledger review remediation

* docs: refresh routed and web exact heads

* docs: correct exact-head hashes

* docs: record structured free-cost contract

* docs: track constant-time budget recovery

* docs: record hosted cache test repair

* docs: refresh budget meter exact head

* docs: track recovered passthrough failover slice

* docs: record database PR cache-test repair

* docs: sync passthrough review repair

* docs: record async server hosted repair

* docs: record fail-closed passthrough review

* docs: record structured orchestration stack evidence

* docs: record exact coverage repair evidence

* docs: record structured control review repairs

* docs: refresh exact-head routing evidence

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: feature New or expanded product capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant