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:
gilesb
2026-01-07 17:24:46 +00:00
parent c7074a9261
commit 618e3b1e04
2 changed files with 57 additions and 3 deletions

View File

@@ -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)

View File

@@ -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