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
244 lines
11 KiB
Python
244 lines
11 KiB
Python
"""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)
|