feat: enhance provenance with infrastructure, actor ID, and commit tracking
- Add infrastructure field to RunStatus model - Store infrastructure (software/hardware) from task result - Format username as ActivityPub actor ID (@user@domain) - Display owner, effects commit, and infrastructure in UI provenance section - Add artdag commit tracking for identity effect - Include infrastructure in raw JSON provenance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
39
server.py
39
server.py
@@ -30,6 +30,7 @@ from tasks import render_effect
|
||||
|
||||
# L2 server for auth verification
|
||||
L2_SERVER = os.environ.get("L2_SERVER", "http://localhost:8200")
|
||||
L2_DOMAIN = os.environ.get("L2_DOMAIN", "artdag.rose-ash.com")
|
||||
|
||||
# Cache directory (use /data/cache in Docker, ~/.artdag/cache locally)
|
||||
CACHE_DIR = Path(os.environ.get("CACHE_DIR", str(Path.home() / ".artdag" / "cache")))
|
||||
@@ -96,7 +97,8 @@ class RunStatus(BaseModel):
|
||||
celery_task_id: Optional[str] = None
|
||||
effects_commit: Optional[str] = None
|
||||
effect_url: Optional[str] = None # URL to effect source code
|
||||
username: Optional[str] = None # Owner of the run
|
||||
username: Optional[str] = None # Owner of the run (ActivityPub actor ID)
|
||||
infrastructure: Optional[dict] = None # Hardware/software used for rendering
|
||||
|
||||
|
||||
# ============ Auth ============
|
||||
@@ -274,6 +276,9 @@ async def create_run(request: RunRequest, username: str = Depends(get_required_u
|
||||
# Generate output name if not provided
|
||||
output_name = request.output_name or f"{request.recipe}-{run_id[:8]}"
|
||||
|
||||
# Format username as ActivityPub actor ID
|
||||
actor_id = f"@{username}@{L2_DOMAIN}"
|
||||
|
||||
# Create run record
|
||||
run = RunStatus(
|
||||
run_id=run_id,
|
||||
@@ -282,7 +287,7 @@ async def create_run(request: RunRequest, username: str = Depends(get_required_u
|
||||
inputs=request.inputs,
|
||||
output_name=output_name,
|
||||
created_at=datetime.now(timezone.utc).isoformat(),
|
||||
username=username
|
||||
username=actor_id
|
||||
)
|
||||
|
||||
# Submit to Celery
|
||||
@@ -324,6 +329,9 @@ async def get_run(run_id: str):
|
||||
run.effects_commit = effects[0].get("repo_commit")
|
||||
run.effect_url = effects[0].get("repo_url")
|
||||
|
||||
# Extract infrastructure info
|
||||
run.infrastructure = result.get("infrastructure")
|
||||
|
||||
# Cache the output
|
||||
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||
if output_path.exists():
|
||||
@@ -900,6 +908,8 @@ async def ui_detail_page(run_id: str):
|
||||
if effects:
|
||||
run.effects_commit = effects[0].get("repo_commit")
|
||||
run.effect_url = effects[0].get("repo_url")
|
||||
# Extract infrastructure info
|
||||
run.infrastructure = result.get("infrastructure")
|
||||
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||
if output_path.exists():
|
||||
cache_file(output_path)
|
||||
@@ -979,10 +989,18 @@ async def ui_detail_page(run_id: str):
|
||||
html += f'''
|
||||
<div class="provenance">
|
||||
<h2>Provenance</h2>
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Owner</div>
|
||||
<div class="prov-value">{run.username or "anonymous"}</div>
|
||||
</div>
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Effect</div>
|
||||
<div class="prov-value"><a href="{effect_url}" target="_blank">{run.recipe}</a></div>
|
||||
</div>
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Effects Commit</div>
|
||||
<div class="prov-value">{run.effects_commit or "N/A"}</div>
|
||||
</div>
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Input(s)</div>
|
||||
<div class="prov-value">
|
||||
@@ -1002,6 +1020,20 @@ async def ui_detail_page(run_id: str):
|
||||
</div>
|
||||
'''
|
||||
|
||||
# Infrastructure section
|
||||
if run.infrastructure:
|
||||
software = run.infrastructure.get("software", {})
|
||||
hardware = run.infrastructure.get("hardware", {})
|
||||
html += f'''
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Infrastructure</div>
|
||||
<div class="prov-value">
|
||||
Software: {software.get("name", "unknown")} ({software.get("content_hash", "unknown")[:16]}...)<br>
|
||||
Hardware: {hardware.get("name", "unknown")} ({hardware.get("content_hash", "unknown")[:16]}...)
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
html += f'''
|
||||
<div class="prov-item">
|
||||
<div class="prov-label">Run ID</div>
|
||||
@@ -1042,6 +1074,7 @@ async def ui_detail_page(run_id: str):
|
||||
"created_at": run.created_at,
|
||||
"completed_at": run.completed_at,
|
||||
"username": run.username,
|
||||
"infrastructure": run.infrastructure,
|
||||
"error": run.error
|
||||
}, indent=2)
|
||||
|
||||
@@ -1077,6 +1110,8 @@ async def ui_run_partial(run_id: str):
|
||||
if effects:
|
||||
run.effects_commit = effects[0].get("repo_commit")
|
||||
run.effect_url = effects[0].get("repo_url")
|
||||
# Extract infrastructure info
|
||||
run.infrastructure = result.get("infrastructure")
|
||||
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||
if output_path.exists():
|
||||
cache_file(output_path)
|
||||
|
||||
21
tasks.py
21
tasks.py
@@ -17,6 +17,7 @@ from celery_app import app
|
||||
|
||||
# Add effects to path (use env var in Docker, fallback to home dir locally)
|
||||
EFFECTS_PATH = Path(os.environ.get("EFFECTS_PATH", str(Path.home() / "artdag-effects")))
|
||||
ARTDAG_PATH = Path(os.environ.get("ARTDAG_PATH", str(Path.home() / "art" / "artdag")))
|
||||
|
||||
|
||||
def get_effects_commit() -> str:
|
||||
@@ -35,6 +36,22 @@ def get_effects_commit() -> str:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_artdag_commit() -> str:
|
||||
"""Get current git commit hash of artdag repo."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=ARTDAG_PATH,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
|
||||
sys.path.insert(0, str(EFFECTS_PATH / "dog"))
|
||||
|
||||
|
||||
@@ -128,11 +145,13 @@ def render_effect(self, input_hash: str, effect_name: str, output_name: str) ->
|
||||
# Build effect info based on source
|
||||
if effect_name == "identity":
|
||||
# Identity is from artdag package on GitHub
|
||||
artdag_commit = get_artdag_commit()
|
||||
effect_info = {
|
||||
"name": f"effect:{effect_name}",
|
||||
"content_hash": REGISTRY[f"effect:{effect_name}"]["hash"],
|
||||
"repo": "github",
|
||||
"repo_url": "https://github.com/gilesbradshaw/art-dag/blob/main/artdag/nodes/effect.py"
|
||||
"repo_commit": artdag_commit,
|
||||
"repo_url": f"https://github.com/gilesbradshaw/art-dag/blob/{artdag_commit}/artdag/nodes/effect.py"
|
||||
}
|
||||
else:
|
||||
# Other effects from rose-ash effects repo
|
||||
|
||||
Reference in New Issue
Block a user