Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,47 +565,52 @@ KYROZEN_SERVER_TOKEN=change-me python server.py --host 0.0.0.0 --port 8000
| `GET` | `/` | Dark-themed chat web UI |
| `POST` | `/api/chat` | Send a message with optional `profile`, `speaker`, `audience`, and `channel`; returns a memory receipt |
| `POST` | `/api/chat/stream` | SSE streaming chat with the same optional profile and memory context |
| `GET` | `/api/cost` | Token usage and cost summary |
| `GET` | `/api/health` | Provider status + memory count |
| `GET` | `/api/memory?q=keyword` | Search stored memories |
| `GET` | `/api/v2/memory?q=keyword&speaker=...&audience=...&channel=...` | Structured memory search with provenance and party scope |
| `GET/POST` | `/api/v2/tasks` | Durable task listing and creation |
| `POST` | `/api/v2/tasks/{task_id}/resume` | Explicitly resume a failed or blocked durable task |
| `GET` | `/api/v2/agents` | List specialised sub-agent profiles |
| `POST` | `/api/v2/agents/run` | Run a sub-agent with isolated memory and capabilities |
| `GET` | `/api/v2/events` | Auditable runtime, task, session, and learning events |
| `GET` | `/api/v2/learning` | Learning proposal status |
| `POST` | `/api/v2/learning/capsules` | Import a capsule as an inactive candidate |
| `GET` | `/api/v2/learning/constitution` | Inspect the immutable user-owned learning policy |
| `GET` | `/api/v2/learning/features` | Authoritative 20-feature registry and latest run status |
| `GET` | `/api/v2/learning/metrics?profile=...` | Profile completion, correction, error, tool, token, and latency metrics |
| `GET` | `/api/v2/learning/{proposal_id}/capsule` | Export a redacted, harness-neutral experience capsule |
| `GET` | `/api/v2/learning/{proposal_id}/evidence` | Proof card, applicability, replay, and outcome receipts |
| `POST` | `/api/v2/learning/{proposal_id}/replay` | Record paired sandboxed candidate/predecessor replay results |
| `POST` | `/api/v2/learning/{proposal_id}/omission` | Record paired with/without-artifact results |
| `POST` | `/api/v2/learning/{proposal_id}/retire` | Retire an artifact with non-regressing omission evidence |
| `POST` | `/api/v2/learning/{proposal_id}/replay` | Record paired sandboxed candidate/predecessor replay results |
| `POST` | `/api/v2/learning/{proposal_id}/restore` | Restore a retired artifact as a canary |
| `GET` | `/api/v2/learning/{proposal_id}/capsule` | Export a redacted, harness-neutral experience capsule |
| `POST` | `/api/v2/learning/capsules` | Import a capsule as an inactive candidate |
| `GET` | `/api/v2/learning/constitution` | Inspect the immutable user-owned learning policy |
| `POST` | `/api/v2/learning/{proposal_id}/retire` | Retire an artifact with non-regressing omission evidence |
| `POST` | `/api/v2/learning/{proposal_id}/rollback` | Roll back an activated proposal |
| `GET/POST` | `/api/v2/memory/claims` | List or create typed, attributed memory claims; filter with `speaker`, `audience`, and `channel` |
| `GET/DELETE` | `/api/v2/memory/claims/{claim_id}` | Explain or dependency-completely forget a claim with party filters |
| `GET` | `/api/v2/events` | Auditable runtime, task, session, and learning events |
| `GET/POST` | `/api/v2/schedules` | Durable interval and one-shot Gateway jobs |
| `GET` | `/api/v2/memory?q=keyword&speaker=...&audience=...&channel=...` | Structured memory search with provenance and party scope |
| `GET` | `/api/v2/memory/claims` | List typed, attributed memory claims; filter with `speaker`, `audience`, and `channel` |
| `POST` | `/api/v2/memory/claims` | Create a typed, attributed memory claim |
| `DELETE` | `/api/v2/memory/claims/{claim_id}` | Dependency-completely forget a claim with party filters |
| `GET` | `/api/v2/memory/claims/{claim_id}` | Explain a claim with party filters |
| `GET` | `/api/v2/schedules` | List durable interval and one-shot Gateway jobs |
| `POST` | `/api/v2/schedules` | Create a durable interval or one-shot Gateway job |
| `POST` | `/api/v2/schedules/{job_id}/disable` | Disable a scheduled job |
| `GET` | `/api/v2/sessions` | List durable sessions |
| `GET` | `/api/v2/sessions/{session_id}` | Resume/read a session context |
| `GET` | `/api/v2/skills` | List installed candidate/active skills |
| `POST` | `/api/v2/skills/install` | Install and validate a local `SKILL.md` package |
| `POST` | `/api/v2/skills/{skill_id}/activate` | Activate a validated skill |
| `POST` | `/api/v2/skills/{skill_id}/rollback` | Roll back a skill |
| `GET` | `/api/v2/sessions` | List durable sessions |
| `GET` | `/api/v2/sessions/{session_id}` | Resume/read a session context |
| `GET` | `/api/v2/agents` | List specialised sub-agent profiles |
| `POST` | `/api/v2/agents/run` | Run a sub-agent with isolated memory and capabilities |
| `GET` | `/api/v2/tasks` | List durable tasks |
| `POST` | `/api/v2/tasks` | Create a durable task |
| `POST` | `/api/v2/tasks/{task_id}/resume` | Explicitly resume a failed or blocked durable task |
| `GET` | `/api/voice/speak?text=...` | Text-to-speech via system TTS |
| `POST` | `/api/voice/transcribe` | Speech-to-text (passthrough) |
| `GET` | `/api/webhooks` | List registered webhooks |
| `POST` | `/api/webhooks/register` | Register a webhook URL |
| `POST` | `/api/webhooks/test` | Fire a test webhook |
| `POST` | `/mcp` | Model Context Protocol (JSON-RPC 2.0) |

Browser tools (`browser_open`, `browser_snapshot`, `browser_click`, `browser_type`, and
`browser_close`) use an isolated profile and are available after
`pip install 'openkyrozen[browser]' && playwright install chromium`. Private and
loopback destinations are blocked unless `KYROZEN_BROWSER_ALLOW_PRIVATE=1` is set.
| `GET` | `/api/cost` | Token usage and cost summary |
| `GET` | `/api/health` | Provider status + memory count |
| `GET` | `/api/voice/speak?text=...` | Text-to-speech via system TTS |
| `POST` | `/api/voice/transcribe` | Speech-to-text (passthrough) |
| `POST` | `/api/webhooks/register` | Register a webhook URL |
| `GET` | `/api/webhooks` | List registered webhooks |
| `POST` | `/api/webhooks/test` | Fire a test webhook |

Successful `POST /api/chat` requests emit one `chat.completed` webhook after
the reply is produced. `POST /api/chat/stream` emits the same event only after
Expand All @@ -616,7 +621,6 @@ it. The POST body is `{"event":"chat.completed","data":{...}}`, where
limited to 500 characters and redact common API-key/token patterns. Webhook
delivery errors are audited as `WEBHOOK_FAILURE` and never change the chat
response.
| `POST` | `/mcp` | Model Context Protocol (JSON-RPC 2.0) |

API and MCP routes allow direct loopback access without a token. Any
non-loopback deployment must set `KYROZEN_SERVER_TOKEN` and send it as
Expand Down
111 changes: 111 additions & 0 deletions scripts/check_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,115 @@ def _discovered_test_count() -> int:
return suite.countTestCases()


def _pipe_table_cells(line: str) -> list[str] | None:
"""Parse one simple Markdown pipe row without adding a dependency."""
stripped = line.strip()
if not stripped.startswith("|") or not stripped.endswith("|"):
return None
return [cell.strip() for cell in stripped[1:-1].split("|")]


def _endpoint_path(value: str) -> str:
"""Remove Markdown code formatting and query details from an endpoint."""
value = value.strip().strip("`")
return value.split("?", 1)[0].split("#", 1)[0]


def _endpoint_methods(value: str) -> list[str]:
"""Expand a documented method cell such as ``GET/POST``."""
value = value.replace("`", "").strip()
return [method.strip().upper() for method in value.split("/") if method.strip()]


def _check_readme_endpoint_table(path: Path, text: str, routes: set[tuple[str, str]]) -> list[str]:
"""Validate README's endpoint table against the live FastAPI route set."""
lines = text.splitlines()
heading_index = next(
(index for index, line in enumerate(lines) if line.strip() == "### REST API endpoints"),
None,
)
if heading_index is None:
return [f"{path.name}: missing '### REST API endpoints' heading"]

section_end = next(
(
index
for index in range(heading_index + 1, len(lines))
if re.match(r"^###\s+", lines[index])
),
len(lines),
)
errors: list[str] = []
header_index = next(
(
index
for index in range(heading_index + 1, section_end)
if (
(cells := _pipe_table_cells(lines[index]))
and [cell.lower() for cell in cells]
== ["method", "endpoint", "description"]
)
),
None,
)
if header_index is None:
return [f"{path.name}: endpoint table header is missing or malformed"]

separator_index = header_index + 1
separator = _pipe_table_cells(lines[separator_index]) if separator_index < section_end else None
if not separator or len(separator) != 3 or not all(re.fullmatch(r":?-{3,}:?", cell) for cell in separator):
return [
f"{path.name}:{separator_index + 1}: endpoint table separator is missing or malformed"
]

documented: set[tuple[str, str]] = set()
table_end = separator_index + 1
while table_end < section_end:
cells = _pipe_table_cells(lines[table_end])
if cells is None:
break
if len(cells) != 3:
errors.append(
f"{path.name}:{table_end + 1}: endpoint table row must have 3 columns"
)
table_end += 1
continue
methods = _endpoint_methods(cells[0])
endpoint = _endpoint_path(cells[1])
if not methods or not endpoint.startswith("/"):
errors.append(f"{path.name}:{table_end + 1}: malformed endpoint row")
for method in methods:
if not re.fullmatch(r"[A-Z]+", method):
errors.append(f"{path.name}:{table_end + 1}: invalid HTTP method '{method}'")
continue
pair = (method, endpoint)
if pair in documented:
errors.append(
f"{path.name}:{table_end + 1}: duplicate endpoint {method} {endpoint}"
)
documented.add(pair)
table_end += 1

# A table row after prose is a broken table, even if Markdown renderers
# happen to display it as a second table.
for index in range(table_end, section_end):
cells = _pipe_table_cells(lines[index])
if cells and len(cells) >= 2 and any(
method in _endpoint_methods(cells[0]) for method, _ in routes
):
errors.append(
f"{path.name}:{index + 1}: endpoint row is outside the contiguous REST table"
)

missing = sorted(routes - documented)
extra = sorted(documented - routes)
for method, endpoint in missing:
errors.append(f"{path.name}: endpoint table is missing live route {method} {endpoint}")
for method, endpoint in extra:
errors.append(f"{path.name}: endpoint table documents unknown route {method} {endpoint}")
return errors


def _check_verification_record() -> list[str]:
if not VERIFICATION_DOC.exists():
return ["docs/self-evolution.md is missing"]
Expand Down Expand Up @@ -159,6 +268,8 @@ def main() -> int:
errors.extend(_check_verification_record())
for readme in README_FILES:
text = readme.read_text(encoding="utf-8")
if readme.name == "README.md":
errors.extend(_check_readme_endpoint_table(readme, text, set(routes)))
for pattern in STALE_TOOL_PATTERNS:
match = pattern.search(text)
if match:
Expand Down
Loading