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 for auth verification
|
||||||
L2_SERVER = os.environ.get("L2_SERVER", "http://localhost:8200")
|
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 directory (use /data/cache in Docker, ~/.artdag/cache locally)
|
||||||
CACHE_DIR = Path(os.environ.get("CACHE_DIR", str(Path.home() / ".artdag" / "cache")))
|
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
|
celery_task_id: Optional[str] = None
|
||||||
effects_commit: Optional[str] = None
|
effects_commit: Optional[str] = None
|
||||||
effect_url: Optional[str] = None # URL to effect source code
|
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 ============
|
# ============ Auth ============
|
||||||
@@ -274,6 +276,9 @@ async def create_run(request: RunRequest, username: str = Depends(get_required_u
|
|||||||
# Generate output name if not provided
|
# Generate output name if not provided
|
||||||
output_name = request.output_name or f"{request.recipe}-{run_id[:8]}"
|
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
|
# Create run record
|
||||||
run = RunStatus(
|
run = RunStatus(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
@@ -282,7 +287,7 @@ async def create_run(request: RunRequest, username: str = Depends(get_required_u
|
|||||||
inputs=request.inputs,
|
inputs=request.inputs,
|
||||||
output_name=output_name,
|
output_name=output_name,
|
||||||
created_at=datetime.now(timezone.utc).isoformat(),
|
created_at=datetime.now(timezone.utc).isoformat(),
|
||||||
username=username
|
username=actor_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Submit to Celery
|
# Submit to Celery
|
||||||
@@ -324,6 +329,9 @@ async def get_run(run_id: str):
|
|||||||
run.effects_commit = effects[0].get("repo_commit")
|
run.effects_commit = effects[0].get("repo_commit")
|
||||||
run.effect_url = effects[0].get("repo_url")
|
run.effect_url = effects[0].get("repo_url")
|
||||||
|
|
||||||
|
# Extract infrastructure info
|
||||||
|
run.infrastructure = result.get("infrastructure")
|
||||||
|
|
||||||
# Cache the output
|
# Cache the output
|
||||||
output_path = Path(result.get("output", {}).get("local_path", ""))
|
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||||
if output_path.exists():
|
if output_path.exists():
|
||||||
@@ -900,6 +908,8 @@ async def ui_detail_page(run_id: str):
|
|||||||
if effects:
|
if effects:
|
||||||
run.effects_commit = effects[0].get("repo_commit")
|
run.effects_commit = effects[0].get("repo_commit")
|
||||||
run.effect_url = effects[0].get("repo_url")
|
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", ""))
|
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||||
if output_path.exists():
|
if output_path.exists():
|
||||||
cache_file(output_path)
|
cache_file(output_path)
|
||||||
@@ -979,10 +989,18 @@ async def ui_detail_page(run_id: str):
|
|||||||
html += f'''
|
html += f'''
|
||||||
<div class="provenance">
|
<div class="provenance">
|
||||||
<h2>Provenance</h2>
|
<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-item">
|
||||||
<div class="prov-label">Effect</div>
|
<div class="prov-label">Effect</div>
|
||||||
<div class="prov-value"><a href="{effect_url}" target="_blank">{run.recipe}</a></div>
|
<div class="prov-value"><a href="{effect_url}" target="_blank">{run.recipe}</a></div>
|
||||||
</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-item">
|
||||||
<div class="prov-label">Input(s)</div>
|
<div class="prov-label">Input(s)</div>
|
||||||
<div class="prov-value">
|
<div class="prov-value">
|
||||||
@@ -1002,6 +1020,20 @@ async def ui_detail_page(run_id: str):
|
|||||||
</div>
|
</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'''
|
html += f'''
|
||||||
<div class="prov-item">
|
<div class="prov-item">
|
||||||
<div class="prov-label">Run ID</div>
|
<div class="prov-label">Run ID</div>
|
||||||
@@ -1042,6 +1074,7 @@ async def ui_detail_page(run_id: str):
|
|||||||
"created_at": run.created_at,
|
"created_at": run.created_at,
|
||||||
"completed_at": run.completed_at,
|
"completed_at": run.completed_at,
|
||||||
"username": run.username,
|
"username": run.username,
|
||||||
|
"infrastructure": run.infrastructure,
|
||||||
"error": run.error
|
"error": run.error
|
||||||
}, indent=2)
|
}, indent=2)
|
||||||
|
|
||||||
@@ -1077,6 +1110,8 @@ async def ui_run_partial(run_id: str):
|
|||||||
if effects:
|
if effects:
|
||||||
run.effects_commit = effects[0].get("repo_commit")
|
run.effects_commit = effects[0].get("repo_commit")
|
||||||
run.effect_url = effects[0].get("repo_url")
|
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", ""))
|
output_path = Path(result.get("output", {}).get("local_path", ""))
|
||||||
if output_path.exists():
|
if output_path.exists():
|
||||||
cache_file(output_path)
|
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)
|
# 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")))
|
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:
|
def get_effects_commit() -> str:
|
||||||
@@ -35,6 +36,22 @@ def get_effects_commit() -> str:
|
|||||||
return "unknown"
|
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"))
|
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
|
# Build effect info based on source
|
||||||
if effect_name == "identity":
|
if effect_name == "identity":
|
||||||
# Identity is from artdag package on GitHub
|
# Identity is from artdag package on GitHub
|
||||||
|
artdag_commit = get_artdag_commit()
|
||||||
effect_info = {
|
effect_info = {
|
||||||
"name": f"effect:{effect_name}",
|
"name": f"effect:{effect_name}",
|
||||||
"content_hash": REGISTRY[f"effect:{effect_name}"]["hash"],
|
"content_hash": REGISTRY[f"effect:{effect_name}"]["hash"],
|
||||||
"repo": "github",
|
"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:
|
else:
|
||||||
# Other effects from rose-ash effects repo
|
# Other effects from rose-ash effects repo
|
||||||
|
|||||||
Reference in New Issue
Block a user