Knowledge Base
ProtoPoke ships a project-scoped knowledge base for cross-session AI
memory. An AI client (over MCP) and a human operator (via the Notes
tab in the TUI) can read and write two kinds of entries that travel
inside the .pp project file:
- Findings — structured claims about the protocol under investigation: hypotheses, confirmed facts, ruled-out theories, with scope (protocol / message / field / byte range / forwarder), evidence frame IDs, status, and confidence.
- Notes — free-form markdown entries for context that does not fit the Finding shape (open questions, design hypotheses about the whole protocol, test-setup reminders).
The point is to stop the AI from re-deriving everything from scratch
on every session. On session start, call list_findings and
list_notes to recover what previous sessions established before
re-running the analysis tools.
list_findings and list_notes return compact rows to keep the
session-start recovery cheap: long descriptions / note bodies are
previewed (with a description_truncated / body_truncated flag),
evidence frame-ID lists are given as evidence_frame_count /
counter_evidence_frame_count, and null scope fields are omitted. Call
get_finding(id) / get_note(id) for the complete record — full text
and the full evidence frame-ID lists.
Why "AI as advisor"¶
The knowledge base is deliberately additive. The MCP layer does
not expose any tool to load, edit, or save the active
ProtocolDefinition — that authority stays with the operator. When
the AI has enough evidence to draft a definition it emits the YAML in
chat (see get_protocol_definition_schema for the spec), and the
operator reviews, saves, and loads it manually.
The same principle holds for findings and notes the user has reviewed:
mutations from the Notes tab set locked=True on the entry, and the
MCP layer refuses subsequent AI updates or deletes on locked entries
or on entries the AI did not author. The AI's only recourse in
that case is to add a counter-finding explaining the disagreement.
Finding schema¶
| Field | Type | Notes |
|---|---|---|
id |
string (UUID) | Stable identifier — never changes |
created_at / updated_at |
float (epoch seconds) | Set by the store |
author |
string | "ai" for MCP adds, "user" for TUI adds |
locked |
bool | True once the user has mutated the entry — MCP refuses further AI mutations |
protocol_name |
string | null | Optional scope |
message_name |
string | null | Optional scope (message type) |
field_name |
string | null | Optional scope (field within a message) |
byte_offset / byte_length |
int | null | Pin to a raw byte range when no field exists yet |
direction |
client_to_server | server_to_client | null |
Optional |
forwarder_id |
string | null | Stable forwarder UUID — survives renames; the current forwarder_name is resolved into the MCP response |
title |
string | One-line summary (required) |
description |
string | Markdown body |
status |
hypothesis | confirmed | ruled_out | needs_review |
Lifecycle state |
confidence |
low | medium | high |
|
evidence_frame_ids |
list[string] | Frame IDs that support the claim |
counter_evidence_frame_ids |
list[string] | Frames that would refute it |
tags |
list[string] | Free-form filtering |
Note schema¶
| Field | Type | Notes |
|---|---|---|
id, created_at, updated_at, author, locked |
— | Same semantics as Finding |
title |
string | One-line label |
body_md |
string | Markdown body |
tags |
list[string] | Free-form filtering |
MCP tools¶
# Findings
list_findings(query=None, status=None, author=None,
protocol_name=None, message_name=None, field_name=None,
forwarder_id=None, tags=None)
get_finding(finding_id)
add_finding(title, description="", status="hypothesis",
confidence="medium",
protocol_name=None, message_name=None, field_name=None,
byte_offset=None, byte_length=None,
direction=None, forwarder_id=None,
evidence_frame_ids=None, counter_evidence_frame_ids=None,
tags=None)
update_finding(finding_id, ...) # any subset of the add_finding kwargs
remove_finding(finding_id)
# Notes
list_notes(query=None, author=None, tags=None)
get_note(note_id)
add_note(title, body_md="", tags=None)
update_note(note_id, title=None, body_md=None, tags=None)
remove_note(note_id)
Worked example¶
The AI clusters frames and notices that bytes 4-5 of LoginRequest
are always different by exactly the bit-flip pattern characteristic of
a CRC. It records the hypothesis:
add_finding(
title="bytes 4-5 of LoginRequest look like a CRC16",
description=(
"Bytes vary across frames but the high bit of byte 4 never "
"matches the high bit of any byte 0-3 — consistent with a "
"checksum, inconsistent with a length."
),
status="hypothesis", confidence="medium",
message_name="LoginRequest",
byte_offset=4, byte_length=2,
evidence_frame_ids=["frame-id-1", "frame-id-2", "frame-id-3"],
tags=["crc", "checksum"],
)
Then it uses tamper to flip a single bit in byte 4 of one such frame and observes that the server rejects the request — confirming the checksum hypothesis. It promotes the finding:
update_finding(finding_id="<the id returned above>",
status="confirmed", confidence="high",
description=description + "\n\nConfirmed by flipping "
"one bit in frame-id-1: server rejected.")
The operator reviews the finding in the Notes tab, marks it as
correct, and saves the project. The Notes-tab save flips locked to
True. Next session, the AI calls list_findings(message_name="LoginRequest")
and immediately sees the confirmed CRC16 location plus the operator's
sign-off. It can build on top of the finding (e.g. propose the right
polynomial) but cannot silently rewrite it.
Persistence¶
Both lists are saved as plain JSON members in the .pp archive:
See the Project File reference for the full archive layout.