Implement lifecycle workspace publication and audit delivery

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 20:17:24 +02:00
parent ee472acc87
commit 5fec7de4cb
21 changed files with 1022 additions and 78 deletions

View file

@ -17,6 +17,7 @@ from .disposition import DispositionRefused, Verb
from .policy import PolicyError
from .provenance import HumanControlNotDischargeable
from .review import ReviewError
from .lifecycle_service import LifecycleError
from .store import Conflict, EvidenceUnavailable, StoreError
from .ui import STYLES, document, error_page, overview_section, review_page
@ -38,9 +39,10 @@ def _one(query: str, *, limit=8192, fields=16) -> dict[str, str]:
class App:
def __init__(self, login: KeyCapeLogin, review=None, *, readiness=lambda: False):
def __init__(self, login: KeyCapeLogin, review=None, *, readiness=lambda: False, lifecycle=None):
self.login = login
self.review, self.readiness = review, readiness
self.lifecycle = lifecycle
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "/")
@ -84,6 +86,11 @@ class App:
}
status, body, content_type = exc.status, error_page(messages.get(exc.code,
"The review could not complete its required checks. Reload after the service is available.")), "text/html"
except LifecycleError as exc:
status, body, content_type = exc.status, document("Decision unavailable",
'<h1>Decision unavailable</h1><p>The request could not complete its required checks. '
'Sign in again if your session expired, or reload the decision before retrying.</p>'
'<p><a href="/decisions">Return to the workspace</a></p>'), "text/html"
except DispositionRefused as exc:
status, body, content_type = 409, error_page({
"G_ACK": "Record all required highlight acknowledgments before accepting or declining.",
@ -125,6 +132,14 @@ class App:
return 303, "", "text/plain", [("Location", "/"), _cookie(FLOW_COOKIE, "", 0),
_cookie(SESSION_COOKIE, new_sid, 900)]
session = self.login.session(sid)
if self.lifecycle is not None and (path == "/decisions" or path.startswith("/decisions/")):
if session is None:
raise LifecycleError(401, "identity_expired")
from .lifecycle_ui import LifecycleWorkspace
params = _one(environ.get("QUERY_STRING", ""))
form = self._form(environ, session) if method == "POST" else None
status, body, extra = LifecycleWorkspace(self.lifecycle).dispatch(method, path, params, session, form)
return status, body, "application/octet-stream" if isinstance(body, bytes) else "text/html", extra
if self.review is not None and (path == "/review" or path.startswith("/presentations/")):
if session is None:
raise ReviewError(401, "session_expired")
@ -188,6 +203,7 @@ class App:
if method == "GET" and path == "/":
if session:
content = (f"<p>Signed in as {html.escape(session.subject)}.</p>"
+ ('<p><a href="/decisions">Open the decision workspace</a></p>' if self.lifecycle is not None else '')
+ (self._overview(session) if self.review is not None else '')
+ ('<h2>Open a decision review</h2><p>Enter the memo identifier supplied with your review request.</p>'
'<form method="get" action="/review"><label>Memo identifier <input name="memo_id" required maxlength="256"></label>'
@ -244,7 +260,8 @@ def main():
runtime = Runtime.from_file(os.environ["INFD_REVIEW_CONFIG"])
runtime.pump.start()
app = App(login, runtime.controller if runtime else None,
readiness=runtime.pump.ready if runtime else lambda: False)
readiness=runtime.pump.ready if runtime else lambda: False,
lifecycle=runtime.lifecycle if runtime else None)
try:
serve(app, host=host, port=8080, threads=4)
finally: