feat: implement daemon model with registry and single-asset assignment (BAT-010, REQ-023/028)
Core Implementation (BAT-010):
- Add daemon registry schema and CRUD operations (req-023)
- Implement single-daemon asset assignment constraint (one asset per daemon)
- Add daemon_id field to service_token for daemon identification
- Add daemon.py CLI module for daemon management commands
- Add 20260513_0016_daemon_model.py migration for daemon tables
API Changes:
- GET /v1/assets: include daemon_id in AssetResponse
- POST /v1/assets/{asset_id}/ledger/trades: require daemon token binding
- GET /v1/user_info: include daemon metadata
- Enhanced service-token rotate to preserve daemon bindings
CLI Commands:
- pfhub daemon register --name <name> --runtime-id <runtime-id>
- pfhub daemon list
- pfhub daemon get <daemon-id>
- pfhub daemon revoke <daemon-id>
Requirements (REQ-023, REQ-028):
- REQ-023: Daemon Registry and Service Token Daemon Binding (Interface, Approved)
* AC-1: daemon CRUD operations
* AC-2: service token daemon binding for POST /v1/assets/{asset_id}/ledger/trades
* AC-3: single daemon per asset (exact-one assignment model)
- REQ-028: README.md Daemon Provisioning Runbook update (Documentation, Approved)
Documentation:
- Add dcy-006 Discovery for daemon-id and single-daemon asset assignment
- Update README.md with Daemon Provisioning Runbook section
- Add BAT-010 batch documentation with planning, implementation, verification
- Update rel-006 release document (daemon model implementation)
- Update data-model.md with daemon and daemon_asset_assignment tables
- Update interface-contract.md with daemon registry APIs
- Update runtime-flows.md with daemon lifecycle flows
Breaking Changes:
- REQ-005~009, REQ-011~012 status reverted to Approved due to revised contract
* daemon_id field addition and exact-one asset assignment requirement
* Existing Implemented evidence invalidated by contract revision
* BAT-001/002/008 re-verification required
Testing:
- Add test_daemon_registry.py for daemon CRUD operations
- Add test_daemon_assignment.py for single-asset assignment constraints
- Update test_auth.py, test_cli.py, test_v1.py for daemon support
- Verify daemon token binding in trade endpoint authorization
Impact:
- Database: 2 new tables (daemons, daemon_asset_assignments)
- Migration: 20260513_0016 creates daemon model schema
- CLI: 4 new daemon subcommands
- API: daemon_id propagation through v1 endpoints
- Verification: BAT-001/002/008 re-verification gates next release
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Daemon registry CLI commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from app.cli.core.utils import render_output
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
daemon_app = typer.Typer(help="Daemon registry management commands.")
|
||||
|
||||
|
||||
def _with_service(ctx: typer.Context, callback):
|
||||
app_context = ctx.obj
|
||||
repository = app_context.repository_factory(app_context.settings)
|
||||
service = AuthService(repository, app_context.settings)
|
||||
try:
|
||||
from app.cli.core.utils import execute_command
|
||||
return execute_command(app_context.console, callback=lambda: callback(repository, app_context))
|
||||
finally:
|
||||
if callable(getattr(type(repository), "close", None)):
|
||||
repository.close()
|
||||
|
||||
|
||||
@daemon_app.command("create")
|
||||
def create_daemon(
|
||||
ctx: typer.Context,
|
||||
daemon_slug: str = typer.Argument(..., help="Unique daemon slug (scoped to the owner user)."),
|
||||
user: str = typer.Option(..., "--user", help="Username of the daemon owner."),
|
||||
display_name: Optional[str] = typer.Option(None, "--display-name", help="Human-readable daemon name."),
|
||||
) -> None:
|
||||
"""Create a new daemon registry entry."""
|
||||
def _create(repo, app_context):
|
||||
owner = repo.get_user_by_username(user)
|
||||
if owner is None:
|
||||
raise ValueError(f"사용자를 찾을 수 없습니다: {user}")
|
||||
return repo.create_daemon(owner.id, daemon_slug=daemon_slug, display_name=display_name)
|
||||
|
||||
result = _with_service(ctx, _create)
|
||||
from app.domain.models import Daemon
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, result)
|
||||
|
||||
|
||||
@daemon_app.command("list")
|
||||
def list_daemons(
|
||||
ctx: typer.Context,
|
||||
user: str = typer.Option(..., "--user", help="Username to list daemons for."),
|
||||
) -> None:
|
||||
"""List all daemon registry entries for a user."""
|
||||
def _list(repo, app_context):
|
||||
owner = repo.get_user_by_username(user)
|
||||
if owner is None:
|
||||
raise ValueError(f"사용자를 찾을 수 없습니다: {user}")
|
||||
return repo.list_daemons_for_user(owner.id)
|
||||
|
||||
result = _with_service(ctx, _list)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, result, empty_message="No daemons found.")
|
||||
|
||||
|
||||
@daemon_app.command("update")
|
||||
def update_daemon(
|
||||
ctx: typer.Context,
|
||||
daemon_id: str = typer.Argument(..., help="Daemon ID to update."),
|
||||
display_name: Optional[str] = typer.Option(None, "--display-name", help="New display name."),
|
||||
deactivate: bool = typer.Option(False, "--deactivate", help="Mark daemon as inactive."),
|
||||
activate: bool = typer.Option(False, "--activate", help="Mark daemon as active."),
|
||||
) -> None:
|
||||
"""Update a daemon registry entry."""
|
||||
def _update(repo, app_context):
|
||||
is_active: bool | None = None
|
||||
if deactivate and activate:
|
||||
raise ValueError("--activate와 --deactivate를 동시에 사용할 수 없습니다")
|
||||
if deactivate:
|
||||
is_active = False
|
||||
elif activate:
|
||||
is_active = True
|
||||
return repo.update_daemon(daemon_id, display_name=display_name, is_active=is_active)
|
||||
|
||||
result = _with_service(ctx, _update)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, result)
|
||||
Reference in New Issue
Block a user