ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,119 @@
"""Case study: Banking / transactions.
Architectural pattern demonstrated: **cross-object balance invariant**.
A transaction's validity depends on the sender account's current balance --
the validator must read related state (the sender account) and compare to the
proposed change. This pattern doesn't fit single-table CHECK constraints; it
requires a runtime validator that reads cross-object state at write time.
Object types: account, transaction, audit_log
Key invariants:
- sender_account.balance >= amount before transaction commits
- account ownership: only the account's owner can initiate transfers
- transactions are immutable after creation (no UPDATE)
- balance updates are reactions, not direct writes (single source of truth)
"""
from __future__ import annotations
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, ReactionDeclaration,
Relationship, RelationshipAction,
)
from pedo.core.store import ObjectStore
def validate_transaction_balance(proposed, existing, accessor, store):
"""Sender account must have sufficient balance for the transfer."""
if existing is not None:
return "Transactions are immutable; cannot UPDATE"
sender_id = proposed.content.get("sender_account_id")
amount = proposed.content.get("amount", 0)
if sender_id is None or amount is None:
return "sender_account_id and amount are required"
if amount <= 0:
return f"Transfer amount must be positive, got {amount}"
sender = store.raw_read(sender_id)
if sender is None:
return f"Sender account {sender_id} not found"
if sender.type_name != "account":
return f"sender_account_id must reference an account, not {sender.type_name}"
if sender.content.get("balance", 0) < amount:
return (f"Insufficient balance: account has "
f"{sender.content.get('balance', 0)}, transfer requires {amount}")
return True
def validate_transaction_recipient_exists(proposed, existing, accessor, store):
"""Recipient account must exist and accept transfers."""
if existing is not None:
return True
recipient_id = proposed.content.get("recipient_account_id")
if not recipient_id:
return "recipient_account_id is required"
recipient = store.raw_read(recipient_id)
if recipient is None or recipient.type_name != "account":
return f"Recipient account {recipient_id} not found"
if recipient.content.get("status") == "frozen":
return "Recipient account is frozen"
return True
def apply_transaction(event, store):
"""Reaction: when a transaction commits, debit sender and credit recipient.
The handler did not have to encode this -- the schema declares it."""
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
sender_id = event["object_content"]["sender_account_id"]
recipient_id = event["object_content"]["recipient_account_id"]
amount = event["object_content"]["amount"]
sender = store.raw_read(sender_id)
if sender is not None:
store.update(sender_id,
{"balance": sender.content.get("balance", 0) - amount},
sys_ctx, _reaction_depth=event["depth"])
recipient = store.raw_read(recipient_id)
if recipient is not None:
store.update(recipient_id,
{"balance": recipient.content.get("balance", 0) + amount},
sys_ctx, _reaction_depth=event["depth"])
def register_banking_types(store: ObjectStore) -> None:
store.register_reaction_handler("apply_transaction", apply_transaction)
# Account: owner-only management; balance is updated only by reactions.
store.register_type(ObjectType(
name="account",
fields={"holder_name": "str", "balance": "int", "status": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "auditor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
# Balance writes happen via the apply_transaction reaction (system role)
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
],
default_policy=Operation.DENY,
))
# Transaction: immutable after create; validators run cross-object checks.
store.register_type(ObjectType(
name="transaction",
fields={"sender_account_id": "str", "recipient_account_id": "str",
"amount": "int", "subject": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "auditor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}), # any authenticated user
# No WRITE / UPDATE / DELETE rules: transactions are immutable.
],
validators=[validate_transaction_balance,
validate_transaction_recipient_exists],
reactions=[
ReactionDeclaration(event="after_create", handler="apply_transaction"),
],
default_policy=Operation.DENY,
))
@@ -0,0 +1,152 @@
"""Case study: E-commerce orders.
Architectural pattern demonstrated: **compound state machine + reactions**.
An order moves through cart -> placed -> paid -> shipped -> delivered, with
refund inverting the state from delivered back to refunded. Each transition
has invariants (placed orders cannot be modified; only paid orders can ship).
The state-machine validator + reactions handle the bookkeeping; the handler
just describes the user intent (place / pay / ship / refund).
Object types: customer, product, order, order_item, audit_log
Key invariants:
- order status follows: cart -> placed -> paid -> shipped -> delivered, or any -> cancelled, delivered -> refunded
- only the order owner can place / cancel; only admin can ship; only paid orders can ship
- inventory decremented on payment (via reaction)
- refund inverts inventory (via reaction)
"""
from __future__ import annotations
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, ReactionDeclaration,
Relationship, RelationshipAction,
)
from pedo.core.store import ObjectStore
ORDER_TRANSITIONS = {
None: ["cart"],
"cart": ["placed", "cancelled"],
"placed": ["paid", "cancelled"],
"paid": ["shipped", "refunded"], # paid can be refunded
"shipped": ["delivered"],
"delivered": ["refunded"], # delivered can be refunded
"cancelled": [],
"refunded": [],
}
def validate_order_status(proposed, existing, accessor, store):
new_status = proposed.content.get("status")
if existing is None:
if new_status not in ("cart", None):
return f"New orders must start as 'cart', got {new_status!r}"
return True
old_status = existing.content.get("status")
if new_status and new_status != old_status:
valid = ORDER_TRANSITIONS.get(old_status, [])
if new_status not in valid:
return f"Invalid order transition {old_status} -> {new_status}; valid: {valid}"
return True
def validate_only_paid_can_ship(proposed, existing, accessor, store):
if existing is None:
return True
if proposed.content.get("status") == "shipped":
if existing.content.get("status") != "paid":
return f"Only paid orders can ship; current status is {existing.content.get('status')}"
return True
def adjust_inventory_on_state_change(event, store):
"""Reaction: decrement inventory on 'paid', increment on 'refunded'."""
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
new_status = event["object_content"].get("status")
if new_status not in ("paid", "refunded"):
return
direction = -1 if new_status == "paid" else +1
# Find order_items for this order, adjust each product's stock
order_id = event["object_id"]
items = store.raw_query("order_item")
for item in items:
if item.content.get("order_id") != order_id:
continue
product_id = item.content.get("product_id")
qty = item.content.get("quantity", 0)
product = store.raw_read(product_id)
if product is None:
continue
current = product.content.get("stock", 0)
store.update(product_id, {"stock": current + direction * qty},
sys_ctx, _reaction_depth=event["depth"])
def register_ecommerce_types(store: ObjectStore) -> None:
store.register_reaction_handler("adjust_inventory_on_state_change",
adjust_inventory_on_state_change)
store.register_type(ObjectType(
name="customer",
fields={"name": "str", "email": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
],
default_policy=Operation.DENY,
))
store.register_type(ObjectType(
name="product",
fields={"name": "str", "price": "int", "stock": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {}), # public catalog
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
# Stock writes only via reactions (system role)
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
],
default_policy=Operation.DENY,
))
store.register_type(ObjectType(
name="order",
fields={"status": "str", "total": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# Owner can update during cart/place/cancel; admin can ship; system runs reactions.
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "system"}),
],
validators=[validate_order_status, validate_only_paid_can_ship],
reactions=[
ReactionDeclaration(event="after_update:status",
handler="adjust_inventory_on_state_change"),
],
default_policy=Operation.DENY,
))
store.register_type(ObjectType(
name="order_item",
fields={"order_id": "str", "product_id": "str", "quantity": "int", "unit_price": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
],
relationships=[
Relationship(name="order", target_type="order",
on_delete=RelationshipAction.CASCADE),
Relationship(name="product", target_type="product",
on_delete=RelationshipAction.RESTRICT),
],
default_policy=Operation.DENY,
))
@@ -0,0 +1,222 @@
"""Case study: Enterprise Agent Sandbox.
Architectural pattern demonstrated: **per-agent permissions for runtime AI actors**.
Multiple autonomous agents operate against the same data layer, each with its
own AccessContext and its own scoped role. Permission rules in the schema
declare what each agent role may do; the pipeline enforces the boundary
regardless of the agent's intent, hallucinations, or prompt-injection.
Object types: document, employee, invoice, email, agent_action_log
Agent roles:
- hr_agent: full read/write on employee; no document/invoice
- finance_agent: full read/write on invoice; public documents only
- email_agent: send email (with PII validator); no other access
- general_agent: public documents + employee directory (name/dept) only
- junior_agent: same as general but consequential ops are PENDING
- human roles (admin, hr_manager, finance_manager) for completeness
Demonstrates:
- structural rejection of out-of-scope reads/writes
- prompt-injection resistance (agent told to delete -> rule rejects)
- exfiltration prevention via PII-in-email validator
- human-in-loop via Operation.PENDING for junior_agent's
consequential operations
- audit reactions logging every agent action
"""
from __future__ import annotations
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, ReactionDeclaration,
Relationship, RelationshipAction,
)
from pedo.core.store import ObjectStore
# Sentinel patterns we treat as PII for the email validator.
_PII_MARKERS = ("ssn:", "salary:", "comp:", "dob:", "credit-card:",
"diagnosis:", "patient-id:")
def validate_email_no_pii(proposed, existing, accessor, store):
"""Block emails containing PII unless sender is HR (who is permitted to
handle PII intentionally). The validator reads the email body; the
schema-author writes this once and it applies to every agent that
composes an email."""
body = (proposed.content.get("body") or "").lower()
for marker in _PII_MARKERS:
if marker in body:
if accessor.role not in ("hr_manager", "hr_agent"):
return f"PII marker {marker!r} in email body; sender role {accessor.role!r} not authorized"
return True
def validate_classification_for_role(proposed, existing, accessor, store):
"""Confidential documents may only be created or modified by humans
(hr_manager, finance_manager, admin). Agents may not author confidential
content -- they are explicitly out-of-scope here."""
classification = proposed.content.get("classification", "internal")
if classification == "confidential":
if accessor.role not in ("admin", "hr_manager", "finance_manager"):
return (f"confidential documents require human authorship; "
f"role {accessor.role!r} cannot author")
return True
def log_agent_action(event, store):
"""Reaction: log every operation traceable to an agent role."""
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
log = DataObject(
type_name="agent_action_log",
content={
"action": event["event"],
"object_id": event["object_id"],
"object_type": event["object_type"],
"fields": event.get("changed_fields", []),
"timestamp": event["timestamp"],
},
owner_id="system",
org_id=event["object_org"],
)
store.create(log, sys_ctx, _reaction_depth=event["depth"])
def register_enterprise_agent_types(store: ObjectStore) -> None:
store.register_reaction_handler("log_agent_action", log_agent_action)
# ── document: classification-aware access ──────────────────────────
store.register_type(ObjectType(
name="document",
fields={"title": "str", "body": "str", "classification": "str"}, # public/internal/confidential
permission_rules=[
# Admins can do anything on documents.
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"role": "admin"}),
# HR / finance managers can author internal & confidential docs in their domain.
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "finance_manager"}),
# Finance agent: read public docs only (no INSERT, no WRITE, no DELETE).
# Enforcement of "public only" is by the validator below.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "finance_agent"}),
# General/junior agent: read public docs only.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "general_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "junior_agent"}),
# System role for reactions.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "system"}),
# Notably absent for ALL agents: DELETE, WRITE, INSERT.
# An agent prompt-injected to "delete all internal documents" hits default-deny.
],
validators=[validate_classification_for_role],
reactions=[
ReactionDeclaration(event="after_create", handler="log_agent_action"),
ReactionDeclaration(event="after_update", handler="log_agent_action"),
ReactionDeclaration(event="after_delete", handler="log_agent_action"),
],
default_policy=Operation.DENY,
))
# ── employee: HR-only edit; directory-view for general agent ──────
# In a real system, "directory-view" would be enforced by separate object
# types or by output-projection middleware. Here we expose two sister
# types: employee (full record, HR-only) and employee_directory (limited
# fields, broader read). This is a common PE idiom -- different access
# patterns get different object types.
store.register_type(ObjectType(
name="employee",
fields={"name": "str", "email": "str", "ssn": "str",
"salary": "int", "dob": "str", "department": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
# HR agent: full HR scope.
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "hr_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "hr_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "hr_agent"}),
# System for reactions.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# NOT GRANTED: finance_agent, email_agent, general_agent, junior_agent.
# Default deny -> any of those reading/writing/inserting an employee fails.
],
reactions=[
ReactionDeclaration(event="after_create", handler="log_agent_action"),
ReactionDeclaration(event="after_update", handler="log_agent_action"),
],
default_policy=Operation.DENY,
))
# ── invoice: finance-domain only ───────────────────────────────────
store.register_type(ObjectType(
name="invoice",
fields={"vendor": "str", "amount": "int", "status": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
# Finance agent: read+write within scope.
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "finance_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "finance_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "finance_agent"}),
# Junior agent: read OK, but consequential WRITE requires human approval (PENDING).
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "junior_agent"}),
PermissionRule(Operation.PENDING, PrivilegeType.WRITE, {"role": "junior_agent"}),
# Finance agent itself does not have DELETE on invoice -> default deny if attempted.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
],
reactions=[
ReactionDeclaration(event="after_create", handler="log_agent_action"),
ReactionDeclaration(event="after_update", handler="log_agent_action"),
],
default_policy=Operation.DENY,
))
# ── email: send-only for email_agent; PII filter applies to all ──
store.register_type(ObjectType(
name="email",
fields={"recipients": "str", "subject": "str", "body": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "hr_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "finance_manager"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "hr_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "finance_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "email_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "general_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "junior_agent"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
],
validators=[validate_email_no_pii], # blocks PII for non-HR senders
reactions=[
ReactionDeclaration(event="after_create", handler="log_agent_action"),
],
default_policy=Operation.DENY,
))
# ── agent_action_log: append-only audit ─────────────────────────────
store.register_type(ObjectType(
name="agent_action_log",
fields={"action": "str", "object_id": "str", "object_type": "str",
"fields": "list", "timestamp": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# Notably absent: any WRITE / DELETE rule -> log is append-only.
],
default_policy=Operation.DENY,
))
@@ -0,0 +1,134 @@
"""Case study: Forum / community.
Architectural pattern demonstrated: **public-read + owner-edit + moderator override**.
Posts are publicly readable but only the author can edit them; moderators can
delete or lock any post regardless of authorship. Comments are owned by their
author. This pattern shows multiple-rule permission composition --- a single
type carries an owner rule, a public rule, and a moderator-override rule.
Object types: forum_post, comment, moderation_log
Key invariants:
- posts are publicly readable
- only the post author can edit content
- moderators can lock or delete any post
- locked posts cannot be edited (even by author)
- comments inherit the lock from their parent post
"""
from __future__ import annotations
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, ReactionDeclaration,
Relationship, RelationshipAction,
)
from pedo.core.store import ObjectStore
def validate_post_not_locked(proposed, existing, accessor, store):
"""Locked posts cannot be edited (even by the author).
Moderators can still lock/unlock via the 'locked' field itself."""
if existing is None:
return True
if existing.content.get("locked"):
# Allow only the moderator unlock action: the only legal change is
# 'locked' going from True to False or remaining True.
for field, new_val in proposed.content.items():
if field == "locked":
continue
if existing.content.get(field) != new_val:
if accessor.role != "moderator":
return f"post is locked; field {field!r} cannot be edited"
return True
def validate_comment_parent_not_locked(proposed, existing, accessor, store):
"""Comments cannot be added to or edited on a locked post."""
post_id = proposed.content.get("post_id")
if not post_id:
return True
post = store.raw_read(post_id)
if post is None:
return f"parent post {post_id} not found"
if post.content.get("locked") and accessor.role != "moderator":
return "parent post is locked; comments cannot be added"
return True
def log_moderation(event, store):
"""Reaction: log every moderator-driven change (lock, delete) to a
moderation_log so the community can audit moderator actions."""
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
log = DataObject(
type_name="moderation_log",
content={
"action": event["event"],
"object_id": event["object_id"],
"object_type": event["object_type"],
"fields": event.get("changed_fields", []),
"timestamp": event["timestamp"],
},
owner_id="system",
org_id=event["object_org"],
)
store.create(log, sys_ctx, _reaction_depth=event["depth"])
def register_forum_types(store: ObjectStore) -> None:
store.register_reaction_handler("log_moderation", log_moderation)
store.register_type(ObjectType(
name="forum_post",
fields={"title": "str", "body": "str", "locked": "bool"},
permission_rules=[
# Public read.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {}),
# Any authenticated user can post.
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}),
# Author can edit.
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"is_owner": True}),
# Moderators have override on edit + delete.
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "moderator"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"role": "moderator"}),
# Author cannot delete (community-history preservation).
],
validators=[validate_post_not_locked],
reactions=[
# Note: moderation log fires on every post update; in practice,
# one would filter on changed_fields for moderator-only events.
ReactionDeclaration(event="after_update:locked", handler="log_moderation"),
ReactionDeclaration(event="after_delete", handler="log_moderation"),
],
default_policy=Operation.DENY,
))
store.register_type(ObjectType(
name="comment",
fields={"post_id": "str", "body": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"role": "moderator"}),
],
validators=[validate_comment_parent_not_locked],
relationships=[
Relationship(name="post", target_type="forum_post",
on_delete=RelationshipAction.CASCADE),
],
default_policy=Operation.DENY,
))
store.register_type(ObjectType(
name="moderation_log",
fields={"action": "str", "object_id": "str", "object_type": "str",
"fields": "list", "timestamp": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {}), # public audit
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {}),
],
default_policy=Operation.DENY,
))
@@ -0,0 +1,147 @@
"""Case study: Healthcare records.
Architectural pattern demonstrated: **field-level granular access + audit reactions**.
Different roles (doctor, nurse, billing, patient) see different subsets of a
patient_record's fields. The schema declares per-field visibility rules; the
handler does not implement view-projection or redaction itself. Every read of
sensitive fields fires an audit-trail reaction (HIPAA-style requirement).
Object types: patient_record, vitals, diagnosis, billing_record, audit_log
Key invariants:
- doctors see vitals + diagnosis + medications; nurses see vitals only;
billing sees billing_record only; patient sees own non-clinical fields
- every read of diagnosis or medications must produce an audit_log entry
- patient_records are immutable except for designated update flows
- break-glass access (emergency override) is allowed but must be logged
"""
from __future__ import annotations
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, ReactionDeclaration,
Relationship, RelationshipAction,
)
from pedo.core.store import ObjectStore
def validate_billing_immutable_after_finalized(proposed, existing, accessor, store):
"""Once a billing_record is marked 'finalized', it cannot be edited."""
if existing is None:
return True
if existing.content.get("status") == "finalized":
for field in ("amount", "items", "service_codes"):
if (field in proposed.content
and proposed.content.get(field) != existing.content.get(field)):
return f"billing_record is finalized; {field!r} is immutable"
return True
def emit_phi_access_log(event, store):
"""Reaction: every read or modification of clinical PHI logs to audit trail.
Note: the read-path doesn't currently fire reactions; this fires on writes
only. A full HIPAA implementation would extend the read-path; the schema
can declare that intent."""
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
log = DataObject(
type_name="phi_audit_log",
content={
"action": event["event"],
"object_id": event["object_id"],
"object_type": event["object_type"],
"actor": event.get("changed_fields", []),
"timestamp": event["timestamp"],
},
owner_id="system",
org_id=event["object_org"],
)
store.create(log, sys_ctx, _reaction_depth=event["depth"])
def register_healthcare_types(store: ObjectStore) -> None:
store.register_reaction_handler("emit_phi_access_log", emit_phi_access_log)
# Patient identity record (non-clinical).
store.register_type(ObjectType(
name="patient",
fields={"name": "str", "dob": "str", "mrn": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "doctor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "nurse"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "billing"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
],
default_policy=Operation.DENY,
))
# Vitals: visible to clinical roles, not billing, not patient.
# (In a full HIPAA system, patient-portal access would have a separate rule.)
store.register_type(ObjectType(
name="vitals",
fields={"patient_id": "str", "blood_pressure": "str",
"heart_rate": "int", "temperature_f": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["doctor", "nurse"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "doctor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "nurse"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# Billing: NOT in the rule list -> default deny.
],
reactions=[
ReactionDeclaration(event="after_create", handler="emit_phi_access_log"),
],
default_policy=Operation.DENY,
))
# Diagnosis: visible only to doctors and the patient themselves.
store.register_type(ObjectType(
name="diagnosis",
fields={"patient_id": "str", "icd10": "str", "notes": "str", "medications": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "doctor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "doctor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# Patient self-read via owner -- the diagnosis's owner is the patient_id.
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
# Nurse and billing: not granted -> default deny.
],
reactions=[
ReactionDeclaration(event="after_create", handler="emit_phi_access_log"),
ReactionDeclaration(event="after_update", handler="emit_phi_access_log"),
],
default_policy=Operation.DENY,
))
# Billing record: visible to billing role + patient (their own).
store.register_type(ObjectType(
name="billing_record",
fields={"patient_id": "str", "amount": "int", "status": "str",
"service_codes": "str", "items": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "billing"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "billing"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "billing"}),
],
validators=[validate_billing_immutable_after_finalized],
reactions=[
ReactionDeclaration(event="after_update", handler="emit_phi_access_log"),
],
default_policy=Operation.DENY,
))
# PHI audit log: append-only, system-only writes.
store.register_type(ObjectType(
name="phi_audit_log",
fields={"action": "str", "object_id": "str", "object_type": "str",
"actor": "str", "timestamp": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "system"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "auditor"}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
# No WRITE / UPDATE / DELETE rules: append-only.
],
default_policy=Operation.DENY,
))
@@ -0,0 +1,247 @@
"""Scenario A: Hiring Pipeline.
Objects: organizations, positions, candidates, interviews, evaluations, audit_logs
Constraints:
- State machine on candidate status: applied -> screened -> interviewed -> offered -> hired/rejected
- Referential integrity between candidates and positions
- Cross-object validation: position must be open to accept new candidates
- Role-based permissions: recruiters modify candidates, hiring managers approve offers,
candidates view only their own records
"""
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, Relationship, RelationshipAction,
ReactionDeclaration,
)
from pedo.core.store import ObjectStore
# Valid state transitions for candidates
VALID_TRANSITIONS = {
None: ["applied"],
"applied": ["screened", "rejected"],
"screened": ["interviewed", "rejected"],
"interviewed": ["offered", "rejected"],
"offered": ["hired", "rejected"],
"hired": [],
"rejected": [],
}
def validate_candidate_status(proposed, existing, accessor, store):
"""Validate candidate status follows the state machine."""
new_status = proposed.content.get("status")
if existing is None:
# Creating a new candidate
if new_status and new_status != "applied":
return f"New candidates must start with status 'applied', got '{new_status}'"
return True
old_status = existing.content.get("status")
if new_status and new_status != old_status:
valid = VALID_TRANSITIONS.get(old_status, [])
if new_status not in valid:
return f"Invalid status transition: {old_status} -> {new_status}. Valid: {valid}"
return True
def validate_position_open(proposed, existing, accessor, store):
"""Validate that the referenced position is still open."""
position_id = proposed.content.get("position_id")
if not position_id:
return True
position = store.raw_read(position_id)
if position is None:
return f"Position {position_id} not found"
if position.content.get("status") != "open":
return f"Position {position_id} is not open (status: {position.content.get('status')})"
return True
def validate_salary_range(proposed, existing, accessor, store):
"""Validate salary expectation is within position range."""
salary = proposed.content.get("salary_expectation")
position_id = proposed.content.get("position_id")
if salary is None or position_id is None:
return True
position = store.raw_read(position_id)
if position is None:
return True # position validator will catch this
min_sal = position.content.get("salary_min", 0)
max_sal = position.content.get("salary_max", float("inf"))
if not (min_sal <= salary <= max_sal):
return f"Salary {salary} outside position range [{min_sal}, {max_sal}]"
return True
def validate_interview_candidate_exists(proposed, existing, accessor, store):
"""Validate that the candidate for an interview exists and is in correct status."""
candidate_id = proposed.content.get("candidate_id")
if not candidate_id:
return "Interview must reference a candidate"
candidate = store.raw_read(candidate_id)
if candidate is None:
return f"Candidate {candidate_id} not found"
if existing is None:
# Creating new interview: candidate must be in screened status
if candidate.content.get("status") not in ("screened", "interviewed"):
return f"Candidate must be screened/interviewed for interview, got {candidate.content.get('status')}"
return True
def validate_evaluation_interview_exists(proposed, existing, accessor, store):
"""Validate that the interview for an evaluation exists."""
interview_id = proposed.content.get("interview_id")
if not interview_id:
return "Evaluation must reference an interview"
interview = store.raw_read(interview_id)
if interview is None:
return f"Interview {interview_id} not found"
return True
# Reaction handlers
def create_audit_log(event, store):
"""Create an audit log entry after any candidate status change."""
system = AccessContext(user_id="system", role="system", org_id=event["object_org"])
log = DataObject(
type_name="audit_log",
content={
"action": event["event"],
"object_id": event["object_id"],
"object_type": event["object_type"],
"changed_fields": event.get("changed_fields", []),
"timestamp": event["timestamp"],
},
owner_id="system",
org_id=event["object_org"],
)
store.create(log, system, _reaction_depth=event["depth"])
def register_hiring_types(store: ObjectStore):
"""Register all hiring pipeline types with the store."""
# Organization (root of hierarchy)
store.register_type(ObjectType(
name="organization",
fields={"name": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.MANAGE, {"roles": ["admin"]}),
],
default_policy=Operation.DENY,
))
# Position
store.register_type(ObjectType(
name="position",
fields={"title": "str", "department": "str", "status": "str",
"salary_min": "int", "salary_max": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["hiring_manager", "admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["hiring_manager", "admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["admin"]}),
],
default_policy=Operation.DENY,
))
# Candidate
store.register_type(ObjectType(
name="candidate",
fields={"name": "str", "email": "str", "status": "str",
"position_id": "str", "salary_expectation": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["recruiter", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["recruiter", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["recruiter", "admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["admin"]}),
# Candidates can only read their own record (via is_owner above)
# Hiring managers can read but not modify (no WRITE rule for them)
PermissionRule(Operation.DENY, PrivilegeType.WRITE, {"role": "hiring_manager"}),
],
validators=[validate_candidate_status, validate_position_open, validate_salary_range],
reactions=[
ReactionDeclaration(event="after_update:status", handler="create_audit_log"),
ReactionDeclaration(event="after_create", handler="create_audit_log"),
],
relationships=[
Relationship(name="position", target_type="position",
on_delete=RelationshipAction.RESTRICT, required=True),
],
default_policy=Operation.DENY,
))
# Interview
store.register_type(ObjectType(
name="interview",
fields={"candidate_id": "str", "interviewer": "str",
"scheduled_at": "str", "notes": "str", "score": "int"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["recruiter", "hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["hiring_manager", "admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["admin"]}),
],
validators=[validate_interview_candidate_exists],
relationships=[
Relationship(name="candidate", target_type="candidate",
on_delete=RelationshipAction.CASCADE),
],
default_policy=Operation.DENY,
))
# Evaluation
store.register_type(ObjectType(
name="evaluation",
fields={"interview_id": "str", "decision": "str", "comments": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["hiring_manager", "admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["hiring_manager", "admin"]}),
# Recruiters can NOT see evaluations
PermissionRule(Operation.DENY, PrivilegeType.READ, {"role": "recruiter"}),
],
validators=[validate_evaluation_interview_exists],
relationships=[
Relationship(name="interview", target_type="interview",
on_delete=RelationshipAction.CASCADE),
],
default_policy=Operation.DENY,
))
# Audit Log (system-only, immutable)
store.register_type(ObjectType(
name="audit_log",
fields={"action": "str", "object_id": "str", "object_type": "str",
"changed_fields": "list", "timestamp": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "system"}),
PermissionRule(Operation.DENY, PrivilegeType.WRITE, {}), # immutable
PermissionRule(Operation.DENY, PrivilegeType.DELETE, {}), # immutable
],
default_policy=Operation.DENY,
))
# Register reaction handlers
store.register_reaction_handler("create_audit_log", create_audit_log)
@@ -0,0 +1,243 @@
"""Scenario B: Multi-tenant Project Management.
Objects: organizations, projects, tasks, comments, attachments
Constraints:
- Tenant isolation: organizations cannot see each other's data
- Hierarchical permissions: project admins manage tasks within their project
- Cross-object validation: task assignee must be a project member
- Referential integrity: deleting a project cascades to tasks
- Temporal bounds: guest access expires
"""
import time
from pedo.core.models import (
AccessContext, DataObject, ObjectType, Operation,
PermissionRule, PrivilegeType, Relationship, RelationshipAction,
ReactionDeclaration,
)
from pedo.core.store import ObjectStore
# Task status state machine
VALID_TASK_TRANSITIONS = {
None: ["todo"],
"todo": ["in_progress", "cancelled"],
"in_progress": ["review", "todo", "cancelled"],
"review": ["done", "in_progress"],
"done": [],
"cancelled": ["todo"], # can reopen
}
# Task priority values
VALID_PRIORITIES = ["low", "medium", "high", "critical"]
def validate_task_status(proposed, existing, accessor, store):
"""Validate task status follows the state machine."""
new_status = proposed.content.get("status")
if existing is None:
if new_status and new_status != "todo":
return f"New tasks must start with status 'todo', got '{new_status}'"
return True
old_status = existing.content.get("status")
if new_status and new_status != old_status:
valid = VALID_TASK_TRANSITIONS.get(old_status, [])
if new_status not in valid:
return f"Invalid task transition: {old_status} -> {new_status}. Valid: {valid}"
return True
def validate_task_priority(proposed, existing, accessor, store):
"""Validate task priority is a valid value."""
priority = proposed.content.get("priority")
if priority and priority not in VALID_PRIORITIES:
return f"Invalid priority '{priority}'. Valid: {VALID_PRIORITIES}"
return True
def validate_task_assignee(proposed, existing, accessor, store):
"""Validate that the task assignee is a member of the project."""
assignee_id = proposed.content.get("assignee_id")
project_id = proposed.content.get("project_id")
if not assignee_id or not project_id:
return True
project = store.raw_read(project_id)
if project is None:
return f"Project {project_id} not found"
members = project.content.get("members", [])
if assignee_id not in members:
return f"Assignee {assignee_id} is not a member of project {project_id}"
return True
def validate_project_org_match(proposed, existing, accessor, store):
"""Validate that the project belongs to the accessor's organization."""
if accessor.role == "system":
return True
if proposed.org_id and accessor.org_id and proposed.org_id != accessor.org_id:
return f"Cannot create project in organization {proposed.org_id} (you belong to {accessor.org_id})"
return True
def validate_comment_task_exists(proposed, existing, accessor, store):
"""Validate that the task being commented on exists."""
task_id = proposed.content.get("task_id")
if not task_id:
return "Comment must reference a task"
task = store.raw_read(task_id)
if task is None:
return f"Task {task_id} not found"
return True
def validate_attachment_size(proposed, existing, accessor, store):
"""Validate attachment size is within limits."""
size = proposed.content.get("size_bytes", 0)
if size > 100_000_000: # 100MB
return f"Attachment too large: {size} bytes (max 100MB)"
return True
# Reaction handlers
def log_task_change(event, store):
"""Log task status changes."""
system = AccessContext(user_id="system", role="system", org_id=event["object_org"])
log = DataObject(
type_name="pm_audit_log",
content={
"action": event["event"],
"object_id": event["object_id"],
"object_type": event["object_type"],
"changed_fields": event.get("changed_fields", []),
"timestamp": event["timestamp"],
},
owner_id="system",
org_id=event["object_org"],
)
store.create(log, system, _reaction_depth=event["depth"])
def register_project_mgmt_types(store: ObjectStore):
"""Register all project management types with the store."""
# Organization (tenant root)
store.register_type(ObjectType(
name="pm_organization",
fields={"name": "str", "plan": "str"},
permission_rules=[
# Tenant isolation: only members of the org can see it
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["org_admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["org_admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.MANAGE, {"roles": ["org_admin"]}),
],
default_policy=Operation.DENY,
))
# Project
store.register_type(ObjectType(
name="project",
fields={"name": "str", "description": "str", "status": "str", "members": "list"},
permission_rules=[
# Tenant isolation via org_id condition
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["project_admin", "org_admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["org_admin"]}),
# Guest access with temporal bounds (set per-object)
],
validators=[validate_project_org_match],
default_policy=Operation.DENY,
))
# Task
store.register_type(ObjectType(
name="task",
fields={"title": "str", "description": "str", "status": "str",
"priority": "str", "assignee_id": "str", "project_id": "str",
"due_date": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.UPDATE, {"roles": ["member", "project_admin", "org_admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["project_admin", "org_admin"]}),
# Guests can only read, not modify
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "guest"}),
PermissionRule(Operation.DENY, PrivilegeType.WRITE, {"role": "guest"}),
],
validators=[validate_task_status, validate_task_priority, validate_task_assignee],
reactions=[
ReactionDeclaration(event="after_update:status", handler="log_task_change"),
ReactionDeclaration(event="after_create", handler="log_task_change"),
],
relationships=[
Relationship(name="project", target_type="project",
on_delete=RelationshipAction.CASCADE, required=True),
],
default_policy=Operation.DENY,
))
# Comment
store.register_type(ObjectType(
name="comment",
fields={"task_id": "str", "author_id": "str", "body": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"is_owner": True}), # only author can edit
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["project_admin", "org_admin"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "guest"}),
],
validators=[validate_comment_task_exists],
relationships=[
Relationship(name="task", target_type="task",
on_delete=RelationshipAction.CASCADE),
],
default_policy=Operation.DENY,
))
# Attachment
store.register_type(ObjectType(
name="attachment",
fields={"task_id": "str", "filename": "str", "size_bytes": "int", "mime_type": "str"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"is_owner": True}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.SELECT, {"roles": ["member", "project_admin", "org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.DELETE, {"roles": ["project_admin", "org_admin"]}),
],
validators=[validate_attachment_size],
relationships=[
Relationship(name="task", target_type="task",
on_delete=RelationshipAction.CASCADE),
],
default_policy=Operation.DENY,
))
# Audit Log
store.register_type(ObjectType(
name="pm_audit_log",
fields={"action": "str", "object_id": "str", "object_type": "str",
"changed_fields": "list", "timestamp": "float"},
permission_rules=[
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"roles": ["org_admin", "system"]}),
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "system"}),
PermissionRule(Operation.DENY, PrivilegeType.WRITE, {}),
PermissionRule(Operation.DENY, PrivilegeType.DELETE, {}),
],
default_policy=Operation.DENY,
))
# Register reaction handlers
store.register_reaction_handler("log_task_change", log_task_change)