Skip to content

Implement the audit event log and payload conformance - #15

Merged
monthop-gmail merged 1 commit into
mainfrom
feat/audit-event-log
Aug 18, 2026
Merged

Implement the audit event log and payload conformance#15
monthop-gmail merged 1 commit into
mainfrom
feat/audit-event-log

Conversation

@monthop-gmail

Copy link
Copy Markdown
Owner

Closes #6 — และปิด ADR-0006 ข้อ 2 ด้วย · conformance.status ขยับออกจาก unknown เป็นครั้งแรก

core           243 passed · coverage 100%
observability   59 passed · coverage 100%
conformance     37 payload validate ผ่าน · guarantee 8 ข้อผ่าน · FAIL=0

🔍 รอบแรกที่รัน จับ contract defect ได้ — ไม่ใช่ของเรา

FAIL  SIGHTING_RECORDED: 'SIGHTING_RECORDED' is not one of
      ['JOB_CREATED', 'STATE_TRANSITION', ...] at event_type

event/v1 $defs.EventType ยังเป็น enum ปิด 7 ค่า ซึ่งขัดกับ 3 อย่างพร้อมกัน:

  1. platform_rules ในไฟล์เดียวกัน"consumer ที่เจอ event_type ที่ไม่รู้จักต้องเก็บ event ไว้ ห้าม drop ห้าม fail" · กฎที่อธิบายวิธีรับมือค่าที่ schema ไม่ยอมให้มีอยู่
  2. ADR-0006 Rule 2 ฉบับแก้ + RFC-0009 ที่ merge ไปเมื่อเช้านี้ — ประกาศว่า vocabulary เป็นชุดเปิด · ค่าที่ platform เพิ่มเองก็จะ validate ไม่ผ่านเหมือนกัน
  3. description ของ field เอง ที่เขียนว่า 🔓 ชุดเปิด

แจ้งเป็น agent-platform#17 พร้อมเสนอรูปแบบแก้ (EventTypeName ที่บังคับ pattern แต่ไม่ปิดชุดค่า)

นี่คือเหตุผลทั้งหมดที่ ADR-0006 ยืนกรานว่า payload จริง ไม่ใช่ $ref check$ref เราถูกทุกจุด pin ถูกทุกตัว แต่ payload ที่ contract บอกเองว่าต้องรับได้ ยัง validate ไม่ผ่าน

Scope ของ #6

  • core event types ครบ 7 — พร้อมกำกับว่า plane ไหนส่งตัวไหน · state machine ยังส่งแค่ 3 ตัวของมัน
  • required fields — ตาม RFC-0008 ที่ amend Phase 1: Implement Audit & Event Log #6 ไว้: job_id optional · subject required · tenant_id required
  • append-only (no mutation, no deletion)
  • no silent state change
  • in-memory event store
  • unit tests สำหรับการสร้าง event และ immutability

Out of scope ตามที่ issue ระบุ: metrics backend · dashboard · correlation IDs ← RFC-0008 เพิ่มเข้ามาแล้ว จึงทำด้วย

packages/observability — 2 จุดที่เป็นสาระ

tenant isolation เป็น partition ไม่ใช่ filter

RFC-0006 ระบุว่า isolation ต้องอยู่ที่ ชั้นเก็บ และบอกตรง ๆ ว่า WHERE tenant_id = ? ไม่นับ

self._partitions: dict[str, list[Event]] = {}   # ไม่ใช่ list เดียวแล้ว filter

list เดียวที่กันด้วย filter คือการเอา predicate หนึ่งตัวไปวางระหว่าง tenant สองราย · ที่นี่ไม่มี predicate ให้ลืม เพราะไม่มี list ที่แชร์กัน

ทุกการอ่านต้องระบุ tenant_id · ไม่มี method ไหนคืน event ข้าม tenanttenants() และการ iterate คืนแค่ id · len() เป็นตัวเลขรวมที่ไม่พา content ของ tenant ไหนออกมา · tenant ที่ไม่รู้จักอ่านได้เป็นว่าง ไม่ raise เพื่อไม่ให้การลองยิงบอกอะไรกับคนถาม

append-only คือการไม่มี method นั้น

ไม่มี update delete clear truncateAPI ที่ไม่มีทางแก้ประวัติ ถูกเกลี้ยกล่อมให้แก้ประวัติไม่ได้ ซึ่งแข็งกว่า flag ที่ใครก็ส่งได้

digest(tenant_id) เป็น hash chain บน event id ตามลำดับที่เขียน — ทำให้คำว่า append-only ตรวจได้ ไม่ใช่แค่อ้าง · เอา digest ไว้ ทำงานต่อ แล้วเอาอีกครั้ง: การโตทำให้เปลี่ยน และการแก้/ลบ record เก่าก็ทำให้เปลี่ยนเหมือนกัน

extend() ไม่ atomic โดยเจตนา — append บางส่วนเก็บตัวที่ถูกต้องไว้แล้วรายงานตัวที่ไม่ถูก · การทิ้ง record ที่รับไปแล้วเพื่อลงโทษตัวที่มาทีหลังคือการทิ้งประวัติที่เกิดขึ้นจริง

Intake — ปฏิเสธ ไม่ซ่อมให้

สถานการณ์ ผล
ไม่มี job_id รับ — sighting เป็น event จริงที่ไม่มี job เป็นต้นเหตุ
job_id = "none" "-" "0" FabricatedIdentifier
resolve tenant ไม่ได้ MissingTenant — reject ที่ intake ไม่ default ให้
resolver ตอบ None MissingTenant — resolver ที่ตอบไม่ได้ ไม่ถูกเดาแทน
ไม่มี subject MissingSubject
ไม่มี source.system ExternalSourceRequired
occurred_at parse ไม่ได้ ValueError — ใส่เวลาของเราแทนคือการรายงานเวลาผิดใน record ที่แก้ไม่ได้
event_type ที่ไม่รู้จัก รับและเก็บไว้ · is_recognised เป็น False

source.kind ถูกบังคับเป็น external ไม่ใช่เชื่อค่าที่ส่งมา — event ที่เข้ามาทางนี้เป็น external ด้วยข้อเท็จจริงที่มันมาถึงที่นี่ ไม่ว่ามันจะอ้างตัวว่าอะไร

การเดา tenant ถือว่าแย่กว่าการเสีย event — event ที่หายเป็นช่องว่างที่มองเห็น ส่วนกิจกรรมของ tenant หนึ่งที่ไปอยู่ใน trail ที่แก้ไม่ได้ของอีก tenant ไม่มีใครเห็น

conformance/payload_check.py

รัน 6 job ข้าม 2 tenant ผ่าน engine จริง — ครบทุก terminal state · pause กลางคันแล้ว resume · reject แล้วยื่นใหม่ · recovery ด้วย supersede() — บวก event จากภายนอกที่ไม่มี job เป็นต้นเหตุ · แล้ว validate payload ทั้ง 37 ตัว ที่ระบบผลิตออกมา

ไม่มี fixture ที่เขียนขึ้นเพื่อให้ schema ผ่านแม้ตัวเดียว · ถ้า payload ไม่ conform ทางแก้คือ code หรือ issue ที่ upstream ไม่ใช่แก้ fixture

และตรวจ guarantee 8 ข้อที่ JSON Schema ตรวจไม่ได้:

ok  append-only — digest ผูกกับลำดับที่เขียนจริง
ok  no silent state change — ทุก transition มี event ตรงจำนวน
ok  ทุก event ตอบได้ว่าเกี่ยวกับอะไร
ok  job_id ครบทุก event ของเรา และไม่ถูกสร้างขึ้นให้ event ภายนอก
ok  event ที่ resolve tenant ไม่ได้ ถูก reject ที่ intake
ok  external event คง source ไว้ แยกออกจาก internal ได้
ok  ไม่มี private reasoning ใน audit record
ok  tenant isolation — 2 partition ไม่ปนกัน

โครงตาม payload_check.py ของ care-agent-platform ที่ทำก่อนและควรลอกมากกว่าคิดใหม่

known_gaps — แคบที่สุดเท่าที่จะแคบได้

การยกเว้นต้องเข้าเงื่อนไข 2 ข้อพร้อมกัน — error ต้องอยู่ที่ event_type และ event ต้องมี source.kind: external · failure อื่นทุกแบบทำให้ run แดง

มีวันหมดอายุ 2026-11-18 และ payload_check ตรวจอายุเองเป็น check ข้อ 3 — ADR-0006 ห้ามยกเว้นถาวร

manifest

conformance:
  status: passing
  last_verified: 2026-08-18
registration: conforming
gaps:
  - id: event-type-enum-closed
    issue: .../agent-platform/issues/17

ครบ 3 ข้อของ ADR-0006 — manifest · conformance test ที่ validate payload จริงใน CI · release gate (job core + conformance ทุก PR) · บันทึก gaps แบบเดียวกับที่ care-agent-platform ทำ

⚠️ ข้อแม้ที่ต้องบอกตรง ๆ

check พวกนี้จะบล็อกการ merge ได้จริงต่อเมื่อเปิด branch protection แล้วตั้งเป็น required — เป็น repo setting ไม่ใช่สิ่งที่ไฟล์ใน PR นี้ยืนยันได้ · เขียนกำกับไว้ทั้งใน manifest และหัว workflow แล้ว เหมือนที่ทำกับ CODEOWNERS (เรื่องเดียวกัน ยังไม่ได้เปิดทั้งคู่)

Impacted planes

Observability เป็นหลัก · Control แตะเฉพาะ EventType ครบ 7 และ Event ที่กว้างขึ้นสำหรับ event ภายนอก — job state machine ไม่เปลี่ยนพฤติกรรมสักจุด (243 tests เดิมผ่านหมด)

Risk analysis

  • known_gaps กลายเป็นข้ออ้างทั่วไป (high) → ต้องเข้า 2 เงื่อนไขพร้อมกัน · มีวันหมดอายุที่ตรวจอัตโนมัติ · การขยายรายการต้องมี issue ใหม่
  • passing ทั้งที่ยังมี gap (medium) → ตามแบบที่ care-agent-platform ทำและ platform รับแล้ว (gaps RFC needed: which states may reach FAILED #14 Implement the audit event log and payload conformance #15 กับ status passing) · gap อยู่ฝั่ง contract ไม่ใช่ implementation และ 35 จาก 37 payload ที่ เราผลิตเอง ผ่านหมด
  • release gate ยังไม่บล็อกจริง (medium) → ระบุไว้ 2 ที่ · เปิด branch protection แล้วจบ
  • extend() ไม่ atomic (low) → เจตนา พร้อมเทสต์ที่ยืนยันพฤติกรรม
  • scenario ไม่ครอบทุก event type (low) → TASK_ASSIGNED EXECUTION_STARTED EXECUTION_FAILED ยังไม่มีใครส่ง เพราะ orchestration/execution เป็น Phase 1: End-to-end flow simulation #7

Related

#6 · #2 (state machine ที่ผลิต payload) · #5 #7 ที่เหลือ · agent-platform#17

🤖 Generated with Claude Code

Closes issue #6 and, with it, ADR-0006 requirement 2 — a conformance test in CI
that validates real payloads. conformance.status moves off unknown for the first
time.

The event vocabulary is now all seven canonical types in one place, with a note on
which plane emits which; the state machine still emits only its own three. Event
grew the surface RFC-0008 needs for inbound events: job_id optional, an
event_type that can hold a string outside our vocabulary, correlation_id, and a
source that an external event keeps.

packages/observability holds storage and intake. Two design points are the
substance rather than detail. Tenant isolation is a partition per tenant, not one
list with a filter, because RFC-0006 states isolation as a storage-layer guarantee
and says a WHERE clause does not satisfy it - a filter puts one forgotten
predicate between two tenants, and there is no predicate to forget when there is
no shared list. Append-only is the absence of update, delete, and clear rather
than a flag, and digest() turns the claim into something checkable: a shortened or
altered history does not reproduce.

Intake refuses rather than repairs. A placeholder job_id is rejected instead of
being stored or quietly turned into absence, because the sender's intent is
ambiguous and an immutable log is the wrong place to resolve an ambiguity by
guessing. An unresolvable tenant is rejected outright - a lost event is a visible
gap, while one tenant's activity in another tenant's trail is not. An unparseable
timestamp is refused rather than replaced with ours, which would misreport when
the thing happened. An unrecognised event_type is none of those things: event/v1
says keep it and skip interpreting it, so it is kept.

conformance/payload_check.py runs six jobs across two tenants through the real
engine - every terminal state, the mid-run pause, rejection and resubmission,
recovery by supersession - plus inbound external events, then validates all 37
emitted payloads. Nothing is hand-written to please the schema. It also asserts
the eight event/v1 guarantees JSON Schema cannot express. The approach follows
care-agent-platform's payload_check.py, which got here first.

The first run earned its keep by finding a contract defect rather than one of
ours: event/v1 $defs.EventType is still a closed enum, which contradicts the same
file's platform_rules ("keep an unknown event_type, never drop or fail") and the
RFC-0009 amendment to ADR-0006 Rule 2 that made the vocabulary open this morning.
Filed as agent-platform#17. Recorded as a known gap scoped to two conditions at
once - the error must be at event_type and the event must be external - with an
expiry date, since ADR-0006 forbids a permanent exception. Any other failure turns
the run red.

The manifest now reads passing and conforming, with the gap listed the way
care-agent-platform lists its own. One honest caveat is recorded there and in the
workflow: these checks only block a merge once branch protection marks them
required, which is a repository setting and not something a file here can assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@monthop-gmail monthop-gmail added the enhancement New feature or request label Aug 18, 2026
@monthop-gmail
monthop-gmail merged commit 8d6a819 into main Aug 18, 2026
4 checks passed
@monthop-gmail
monthop-gmail deleted the feat/audit-event-log branch August 18, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 1: Implement Audit & Event Log

1 participant