fleetwrit-server 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. fleetwrit_server-0.0.1/.gitignore +22 -0
  2. fleetwrit_server-0.0.1/Dockerfile +14 -0
  3. fleetwrit_server-0.0.1/PKG-INFO +33 -0
  4. fleetwrit_server-0.0.1/README.md +21 -0
  5. fleetwrit_server-0.0.1/fleetwrit_server/__init__.py +5 -0
  6. fleetwrit_server-0.0.1/fleetwrit_server/app.py +169 -0
  7. fleetwrit_server-0.0.1/fleetwrit_server/cli.py +141 -0
  8. fleetwrit_server-0.0.1/fleetwrit_server/schema.sql +30 -0
  9. fleetwrit_server-0.0.1/fleetwrit_server/seed.py +96 -0
  10. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/index-6Cu-XtEX.js +67 -0
  11. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/index-CjuqbQU3.css +1 -0
  12. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  13. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  14. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  15. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  16. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  17. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  18. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  19. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/spline-sans-mono-latin-ext-wght-normal-Dh0aNLWd.woff2 +0 -0
  20. fleetwrit_server-0.0.1/fleetwrit_server/static/assets/spline-sans-mono-latin-wght-normal-DlaB5ohX.woff2 +0 -0
  21. fleetwrit_server-0.0.1/fleetwrit_server/static/index.html +14 -0
  22. fleetwrit_server-0.0.1/fleetwrit_server/store.py +330 -0
  23. fleetwrit_server-0.0.1/pyproject.toml +26 -0
  24. fleetwrit_server-0.0.1/tests/test_app.py +52 -0
  25. fleetwrit_server-0.0.1/tests/test_cli.py +42 -0
  26. fleetwrit_server-0.0.1/tests/test_store.py +116 -0
@@ -0,0 +1,22 @@
1
+ # Dependencies / build
2
+ node_modules/
3
+ dist/
4
+ .venv/
5
+ *.egg-info/
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.pyc
10
+ .pytest_cache/
11
+
12
+ # Local dev state
13
+ *.db
14
+ *.db-wal
15
+ *.db-shm
16
+ .env.local
17
+ .gstack/
18
+
19
+ # Impeccable ephemera (keep DESIGN.md, design.json, surfaces, config)
20
+ .impeccable/review/
21
+ .impeccable/build/
22
+ .impeccable/mocks/
@@ -0,0 +1,14 @@
1
+ # Fleetwrit dev server (FastAPI + SQLite). Build context is the repo root so the
2
+ # sibling SDK (fleetwrit-python) can be installed for the shared fingerprint.
3
+ FROM python:3.12-slim
4
+
5
+ WORKDIR /app
6
+ COPY fleetwrit-python /app/fleetwrit-python
7
+ COPY server /app/server
8
+ RUN pip install --no-cache-dir ./fleetwrit-python ./server \
9
+ && mkdir -p /data
10
+
11
+ ENV FLEETWRIT_DB=/data/fleetwrit-dev.db
12
+ EXPOSE 4100
13
+
14
+ CMD ["uvicorn", "fleetwrit_server.app:app", "--host", "0.0.0.0", "--port", "4100"]
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.5
2
+ Name: fleetwrit-server
3
+ Version: 0.0.1
4
+ Summary: Fleetwrit v0 local server (fleetwrit dev): FastAPI + SQLite.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: cryptography>=42
8
+ Requires-Dist: fastapi>=0.110
9
+ Requires-Dist: fleetwrit>=0.0.1
10
+ Requires-Dist: uvicorn[standard]>=0.29
11
+ Description-Content-Type: text/markdown
12
+
13
+ # fleetwrit-server
14
+
15
+ [![test-server](https://github.com/teopopescu/fleetwrit/actions/workflows/test-server.yml/badge.svg)](https://github.com/teopopescu/fleetwrit/actions/workflows/test-server.yml)
16
+
17
+ The local Fleetwrit server: a FastAPI + SQLite service that receives agent
18
+ requests, records decisions and a hash-chained ledger, signs receipts, and
19
+ serves the ops dashboard (bundled in). It's the backend the
20
+ [`fleetwrit`](https://pypi.org/project/fleetwrit/) SDK talks to.
21
+
22
+ For local development, install the SDK with the `dev-server` extra and run both
23
+ with one command:
24
+
25
+ ```bash
26
+ pip install "fleetwrit[dev-server]"
27
+ fleetwrit dev # server API + dashboard on http://localhost:4100 (starts empty)
28
+ ```
29
+
30
+ `fleetwrit dev --demo` loads sample agents and requests. See the
31
+ [fleetwrit repo](https://github.com/teopopescu/fleetwrit) for Docker and more.
32
+
33
+ Apache-2.0.
@@ -0,0 +1,21 @@
1
+ # fleetwrit-server
2
+
3
+ [![test-server](https://github.com/teopopescu/fleetwrit/actions/workflows/test-server.yml/badge.svg)](https://github.com/teopopescu/fleetwrit/actions/workflows/test-server.yml)
4
+
5
+ The local Fleetwrit server: a FastAPI + SQLite service that receives agent
6
+ requests, records decisions and a hash-chained ledger, signs receipts, and
7
+ serves the ops dashboard (bundled in). It's the backend the
8
+ [`fleetwrit`](https://pypi.org/project/fleetwrit/) SDK talks to.
9
+
10
+ For local development, install the SDK with the `dev-server` extra and run both
11
+ with one command:
12
+
13
+ ```bash
14
+ pip install "fleetwrit[dev-server]"
15
+ fleetwrit dev # server API + dashboard on http://localhost:4100 (starts empty)
16
+ ```
17
+
18
+ `fleetwrit dev --demo` loads sample agents and requests. See the
19
+ [fleetwrit repo](https://github.com/teopopescu/fleetwrit) for Docker and more.
20
+
21
+ Apache-2.0.
@@ -0,0 +1,5 @@
1
+ """Fleetwrit v0 local server (fleetwrit-server)."""
2
+
3
+ from .app import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,169 @@
1
+ """Fleetwrit v0 local server (the `fleetwrit dev` path): FastAPI + SQLite.
2
+
3
+ Implements the agent-facing /v1 API the SDK's HttpTransport calls, plus the
4
+ dashboard-facing read/decide API. Auth is off (local/demo mode). Long-poll
5
+ returns {"outcome":"pending"} when undecided so the SDK re-polls.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from fastapi import FastAPI, Request
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from fastapi.responses import JSONResponse
18
+ from fastapi.staticfiles import StaticFiles
19
+ from starlette.exceptions import HTTPException as StarletteHTTPException
20
+
21
+ from .store import Store
22
+ from .seed import seed_demo
23
+
24
+ store = Store(os.getenv("FLEETWRIT_DB", "fleetwrit-dev.db"))
25
+ app = FastAPI(title="Fleetwrit dev server", version="0.1.0")
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # Start empty by default — a local dev server is for your own data. Set
34
+ # FLEETWRIT_SEED=1 (or `fleetwrit dev --demo`) to load the sample agents/requests.
35
+ if os.getenv("FLEETWRIT_SEED", "0") == "1":
36
+ seed_demo(store)
37
+
38
+
39
+ # --- agent-facing /v1 (key auth in prod; off in local mode) ----------------
40
+ @app.post("/v1/agents/register")
41
+ async def register(req: Request) -> dict[str, Any]:
42
+ body = await req.json()
43
+ store.register(body.get("agent", {}), body.get("action_types", []), body.get("runtime"))
44
+ return {"ok": True}
45
+
46
+
47
+ @app.post("/v1/requests")
48
+ async def create_request(req: Request) -> JSONResponse:
49
+ body = await req.json()
50
+ try:
51
+ return JSONResponse(store.create_request(body))
52
+ except ValueError as exc:
53
+ return JSONResponse({"error": str(exc)}, status_code=400)
54
+
55
+
56
+ @app.get("/v1/requests/{rid}/decision")
57
+ async def get_decision(rid: str, wait: int = 25) -> JSONResponse:
58
+ deadline = asyncio.get_event_loop().time() + min(max(wait, 0), 25)
59
+ while True:
60
+ d = store.decision(rid)
61
+ if d is None:
62
+ return JSONResponse({"error": "not found"}, status_code=404)
63
+ if d.get("outcome") != "pending":
64
+ return JSONResponse(d)
65
+ if asyncio.get_event_loop().time() >= deadline:
66
+ return JSONResponse({"outcome": "pending"})
67
+ await asyncio.sleep(0.4)
68
+
69
+
70
+ @app.post("/v1/requests/{rid}/ack")
71
+ async def ack(rid: str) -> JSONResponse:
72
+ try:
73
+ store.ack(rid)
74
+ except ValueError as exc:
75
+ return JSONResponse({"error": str(exc)}, status_code=409)
76
+ return JSONResponse({"ok": True})
77
+
78
+
79
+ @app.post("/v1/requests/{rid}/cancel")
80
+ async def cancel(rid: str) -> dict[str, Any]:
81
+ store.cancel(rid)
82
+ return {"ok": True}
83
+
84
+
85
+ # --- dashboard-facing ------------------------------------------------------
86
+ @app.get("/v1/overview")
87
+ async def overview() -> dict[str, Any]:
88
+ return store.overview()
89
+
90
+
91
+ @app.get("/v1/inbox")
92
+ async def inbox() -> dict[str, Any]:
93
+ return {"requests": store.inbox()}
94
+
95
+
96
+ @app.get("/v1/requests/{rid}")
97
+ async def request_detail(rid: str) -> JSONResponse:
98
+ r = store.get_request(rid)
99
+ if not r:
100
+ return JSONResponse({"error": "not found"}, status_code=404)
101
+ return JSONResponse(r)
102
+
103
+
104
+ @app.post("/v1/requests/{rid}/decide")
105
+ async def decide(rid: str, req: Request) -> JSONResponse:
106
+ body = await req.json()
107
+ reviewer = body.get("reviewer") or {
108
+ "subject": "you@acme.com", "email": "you@acme.com",
109
+ "name": "You", "issuer": "https://fleetwrit.local",
110
+ }
111
+ try:
112
+ result = store.decide(rid, body.get("outcome", "approved"), body.get("edits"), body.get("reason"), reviewer, body.get("value"), body.get("option"))
113
+ except KeyError:
114
+ return JSONResponse({"error": "not found"}, status_code=404)
115
+ except ValueError as exc:
116
+ return JSONResponse({"error": str(exc)}, status_code=409)
117
+ return JSONResponse(result)
118
+
119
+
120
+ @app.get("/v1/agents")
121
+ async def agents() -> dict[str, Any]:
122
+ return {"agents": store.agents()}
123
+
124
+
125
+ @app.get("/v1/action-types")
126
+ async def action_types() -> dict[str, Any]:
127
+ return {"action_types": store.action_types()}
128
+
129
+
130
+ @app.get("/v1/ledger")
131
+ async def ledger() -> dict[str, Any]:
132
+ return {"events": store.ledger()}
133
+
134
+
135
+ @app.get("/v1/ledger/verify")
136
+ async def ledger_verify() -> dict[str, Any]:
137
+ return store.verify_chain()
138
+
139
+
140
+ # --- operational -----------------------------------------------------------
141
+ @app.get("/healthz")
142
+ async def healthz() -> dict[str, Any]:
143
+ return {"ok": True}
144
+
145
+
146
+ @app.get("/.well-known/jwks.json")
147
+ async def jwks() -> dict[str, Any]:
148
+ return store.jwks()
149
+
150
+
151
+ # --- bundled dashboard -----------------------------------------------------
152
+ # When the built dashboard is packaged into ./static, serve it at / so the whole
153
+ # app is one process on one port. Mounted last, so the API routes above win; a
154
+ # 404 falls back to index.html for the SPA's client-side routes.
155
+ _STATIC = Path(__file__).with_name("static")
156
+
157
+
158
+ class _SPAStaticFiles(StaticFiles):
159
+ async def get_response(self, path: str, scope: Any) -> Any:
160
+ try:
161
+ return await super().get_response(path, scope)
162
+ except StarletteHTTPException as exc:
163
+ if exc.status_code == 404:
164
+ return await super().get_response("index.html", scope)
165
+ raise
166
+
167
+
168
+ if (_STATIC / "index.html").is_file():
169
+ app.mount("/", _SPAStaticFiles(directory=str(_STATIC), html=True), name="dashboard")
@@ -0,0 +1,141 @@
1
+ """`fleetwrit-server` console entrypoint.
2
+
3
+ Bare command runs the API with uvicorn (back-compat). `fleetwrit-server dev`
4
+ also launches the dashboard, so the whole local stack comes up in one command.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import os
11
+ import shutil
12
+ import socket
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+
19
+ def _free_port(preferred: int, host: str = "127.0.0.1", tries: int = 20) -> int:
20
+ """Return the first bindable port at or after ``preferred`` (up to ``tries``)."""
21
+ for port in range(preferred, preferred + tries):
22
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
23
+ try:
24
+ probe.bind((host, port))
25
+ return port
26
+ except OSError:
27
+ continue
28
+ raise RuntimeError(f"no free port in {preferred}..{preferred + tries - 1}")
29
+
30
+
31
+ def _run_server_only(port: int) -> None:
32
+ import uvicorn
33
+
34
+ uvicorn.run("fleetwrit_server.app:app", host="127.0.0.1", port=port, reload=False)
35
+
36
+
37
+ def _find_dashboard(start: str | None) -> Path | None:
38
+ """Look for a sibling ``dashboard/`` (with a package.json) from here upward."""
39
+ here = Path(start or os.getcwd()).resolve()
40
+ for base in (here, *here.parents):
41
+ candidate = base / "dashboard"
42
+ if (candidate / "package.json").is_file():
43
+ return candidate
44
+ return None
45
+
46
+
47
+ def run_dev(
48
+ port: int = 4100,
49
+ dashboard_port: int = 5174,
50
+ dashboard: bool = True,
51
+ seed: bool = False,
52
+ path: str | None = None,
53
+ ) -> int:
54
+ """Start the server, and (if found) the dashboard, then wait for Ctrl-C."""
55
+ env = {**os.environ, "FLEETWRIT_SEED": "1" if seed else "0"}
56
+ procs: list[subprocess.Popen] = []
57
+
58
+ resolved = _free_port(port)
59
+ if resolved != port:
60
+ print(f"· port {port} is busy — using {resolved} instead", flush=True)
61
+ port = resolved
62
+
63
+ server = subprocess.Popen(
64
+ [sys.executable, "-m", "uvicorn", "fleetwrit_server.app:app",
65
+ "--host", "127.0.0.1", "--port", str(port)],
66
+ env=env,
67
+ )
68
+ procs.append(server)
69
+ print(f"→ server http://localhost:{port} (API, seeded demo data)", flush=True)
70
+
71
+ # Prefer the dashboard bundled into the server: it is served by the API on
72
+ # the same port, so a pip-installed user needs no repo checkout and no Node.
73
+ bundled = (Path(__file__).with_name("static") / "index.html").is_file()
74
+ if dashboard and bundled:
75
+ print(f"→ dashboard http://localhost:{port} (bundled, served by the API)", flush=True)
76
+ elif dashboard:
77
+ dash_dir = _find_dashboard(path)
78
+ npm = shutil.which("npm")
79
+ if dash_dir is None:
80
+ print("! no bundled dashboard and no sibling dashboard/ — running the server only.", flush=True)
81
+ elif npm is None:
82
+ print("! npm not found on PATH — running the server without the dashboard.", flush=True)
83
+ else:
84
+ if not (dash_dir / "node_modules").is_dir():
85
+ print("· installing dashboard deps (first run)…", flush=True)
86
+ subprocess.run([npm, "install"], cwd=dash_dir, check=False)
87
+ dashboard_port = _free_port(dashboard_port)
88
+ dash_env = {**env, "VITE_FLEETWRIT_URL": f"http://localhost:{port}"}
89
+ procs.append(subprocess.Popen(
90
+ [npm, "run", "dev", "--", "--port", str(dashboard_port), "--strictPort"],
91
+ cwd=dash_dir, env=dash_env,
92
+ ))
93
+ print(f"→ dashboard http://localhost:{dashboard_port} (live against the server)", flush=True)
94
+
95
+ print("Press Ctrl-C to stop.", flush=True)
96
+ try:
97
+ while True:
98
+ for p in procs:
99
+ if p.poll() is not None:
100
+ raise KeyboardInterrupt
101
+ time.sleep(0.5)
102
+ except KeyboardInterrupt:
103
+ pass
104
+ finally:
105
+ for p in procs:
106
+ if p.poll() is None:
107
+ p.terminate()
108
+ for p in procs:
109
+ try:
110
+ p.wait(timeout=5)
111
+ except subprocess.TimeoutExpired:
112
+ p.kill()
113
+ return 0
114
+
115
+
116
+ def main(argv: list[str] | None = None) -> int:
117
+ parser = argparse.ArgumentParser(prog="fleetwrit-server", description="Fleetwrit dev server")
118
+ sub = parser.add_subparsers(dest="command")
119
+
120
+ dev = sub.add_parser("dev", help="Run the server and the dashboard together")
121
+ dev.add_argument("--port", type=int, default=int(os.getenv("PORT", "4100")))
122
+ dev.add_argument("--dashboard-port", type=int, default=5174)
123
+ dev.add_argument("--no-dashboard", action="store_true", help="Run the server only")
124
+ dev.add_argument("--demo", action="store_true", help="Load sample agents/requests (default: empty)")
125
+ dev.add_argument("--path", default=None, help="Repo root to find dashboard/ (default: cwd)")
126
+
127
+ args = parser.parse_args(argv)
128
+ if args.command == "dev":
129
+ return run_dev(
130
+ port=args.port,
131
+ dashboard_port=args.dashboard_port,
132
+ dashboard=not args.no_dashboard,
133
+ seed=args.demo,
134
+ path=args.path,
135
+ )
136
+ _run_server_only(int(os.getenv("PORT", "4100")))
137
+ return 0
138
+
139
+
140
+ if __name__ == "__main__":
141
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ CREATE TABLE IF NOT EXISTS agents(
2
+ agent_id TEXT, environment TEXT, version TEXT, runtime TEXT,
3
+ owner TEXT, last_seen TEXT, disabled INTEGER DEFAULT 0,
4
+ volume30d INTEGER DEFAULT 0,
5
+ PRIMARY KEY(agent_id, environment));
6
+
7
+ CREATE TABLE IF NOT EXISTS action_types(
8
+ type TEXT PRIMARY KEY, version TEXT, title TEXT, risk TEXT,
9
+ reversible INTEGER, editable TEXT, queue TEXT, owner TEXT,
10
+ summary TEXT, args_schema TEXT, display TEXT,
11
+ undeclared INTEGER DEFAULT 0, approval_rate REAL);
12
+
13
+ CREATE TABLE IF NOT EXISTS requests(
14
+ id TEXT PRIMARY KEY, idempotency_key TEXT UNIQUE, kind TEXT,
15
+ agent_id TEXT, environment TEXT, type TEXT, action_version TEXT,
16
+ tool TEXT, args TEXT, reversible INTEGER, fingerprint TEXT,
17
+ summary TEXT, context TEXT, queue TEXT, provenance TEXT,
18
+ risk TEXT, editable TEXT, title TEXT, display TEXT,
19
+ created_at TEXT, expires_at TEXT, on_expiry TEXT,
20
+ state TEXT, viewed_at TEXT, verdict TEXT);
21
+
22
+ CREATE TABLE IF NOT EXISTS decisions(
23
+ request_id TEXT PRIMARY KEY, outcome TEXT, final_args TEXT,
24
+ original_fingerprint TEXT, final_fingerprint TEXT, reason TEXT,
25
+ reviewer TEXT, receipt TEXT, value TEXT, option TEXT,
26
+ decided_at TEXT, consumed_at TEXT);
27
+
28
+ CREATE TABLE IF NOT EXISTS ledger(
29
+ seq INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, actor TEXT,
30
+ event TEXT, payload_hash TEXT, refs TEXT, prev_hash TEXT, hash TEXT);
@@ -0,0 +1,96 @@
1
+ """Seed the dev server with synthetic agents, action types and a little history.
2
+
3
+ Idempotent: does nothing if agents already exist. Not real data — @acme.com only.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import uuid
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any
11
+
12
+ from fleetwrit.fingerprint import fingerprint
13
+
14
+
15
+ def _iso(dt: datetime) -> str:
16
+ return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
17
+
18
+
19
+ AGENTS = [
20
+ {"agent": {"id": "sre-remediation", "environment": "prod", "version": "0.1.0a3"}, "runtime": "LangGraph"},
21
+ {"agent": {"id": "support-refunds", "environment": "prod", "version": "0.4.2"}, "runtime": "OpenAI Agents"},
22
+ {"agent": {"id": "ap-invoices", "environment": "staging", "version": "0.11.0"}, "runtime": "LlamaIndex"},
23
+ ]
24
+
25
+ ACTION_TYPES = [
26
+ {"type": "deploy.rollback", "version": "a1", "title": "Roll back deployment", "risk": "high", "reversible": False,
27
+ "editable": ["version"], "queue": "sre-oncall", "owner": "platform@acme.com",
28
+ "summary": "Roll back {service} to version {version}", "approval_rate": 0.98},
29
+ {"type": "service.restart", "version": "b2", "title": "Restart service", "risk": "medium", "reversible": True,
30
+ "editable": [], "queue": "sre-oncall", "owner": "platform@acme.com", "summary": "Restart {service}", "approval_rate": 0.99},
31
+ {"type": "refund.issue", "version": "c3", "title": "Issue refund", "risk": "high", "reversible": False,
32
+ "editable": ["amount"], "queue": "finance-ops", "owner": "payments-platform@acme.com",
33
+ "summary": "Refund {amount} on charge {charge}",
34
+ "display": {"amount": {"kind": "money", "currency_field": "currency"}}, "approval_rate": 0.96},
35
+ {"type": "payment.release", "version": "d4", "title": "Release vendor payment", "risk": "high", "reversible": False,
36
+ "editable": ["amount"], "queue": "finance-ops", "owner": "ap@acme.com",
37
+ "summary": "Release {amount} to {supplier}", "approval_rate": 0.9},
38
+ {"type": "data.delete", "version": "e5", "title": "Delete customer records", "risk": "critical", "reversible": False,
39
+ "editable": [], "queue": "privacy", "owner": "privacy@acme.com", "summary": "Delete {count} records", "approval_rate": 0.85},
40
+ ]
41
+
42
+ AGENT_ACTIONS = {
43
+ "sre-remediation": ["deploy.rollback", "service.restart", "data.delete"],
44
+ "support-refunds": ["refund.issue"],
45
+ "ap-invoices": ["payment.release"],
46
+ }
47
+
48
+
49
+ def _payload(agent: str, env: str, atype: str, aversion: str, tool: str | None, args: dict[str, Any],
50
+ summary: str, queue: str, context: dict[str, Any], reversible: bool, mins_ago: int, ttl_min: int) -> dict[str, Any]:
51
+ created = datetime.now(timezone.utc) - timedelta(minutes=mins_ago)
52
+ fp = fingerprint(type=atype, version=aversion, tool=tool, args=args, agent_id=agent, environment=env)
53
+ return {
54
+ "schema": "fleetwrit/v1", "kind": "approve",
55
+ "agent": {"id": agent, "environment": env, "version": None},
56
+ "action": {"type": atype, "version": aversion, "tool": tool, "args": args, "reversible": reversible},
57
+ "fingerprint": fp, "summary": summary, "context": context, "queue": queue,
58
+ "provenance": {"run_id": "run_" + uuid.uuid4().hex[:8], "parent_request_id": None, "trace_id": None},
59
+ "idempotency_key": "seed_" + uuid.uuid4().hex[:12],
60
+ "created_at": _iso(created), "expires_at": _iso(created + timedelta(minutes=ttl_min)), "on_expiry": "reject",
61
+ }
62
+
63
+
64
+ def seed_demo(store: Any) -> None:
65
+ if store.agents():
66
+ return
67
+ for a in AGENTS:
68
+ acts = [t for t in ACTION_TYPES if t["type"] in AGENT_ACTIONS[a["agent"]["id"]]]
69
+ store.register(a["agent"], acts, a["runtime"])
70
+
71
+ reviewer = {"subject": "marcus@acme.com", "email": "marcus@acme.com", "name": "Marcus Okonkwo", "issuer": "https://acme.okta.com"}
72
+
73
+ # pending requests (populate the inbox)
74
+ store.create_request(_payload("support-refunds", "prod", "refund.issue", "c3", "stripe.refunds.create",
75
+ {"charge": "ch_123", "amount": 400000, "currency": "gbp"}, "Refund £4,000 on charge ch_123",
76
+ "finance-ops", {"ticket": "ZD-99120", "prior_refunds": 0}, False, 6, 30))
77
+ store.create_request(_payload("sre-remediation", "prod", "deploy.rollback", "a1", "kubectl.rollout.undo",
78
+ {"service": "payments-api", "version": 42}, "Roll back payments-api to version 42",
79
+ "sre-oncall", {"alert": "PD-4471", "p95_ms": 920}, False, 3, 30))
80
+ store.create_request(_payload("sre-remediation", "prod", "data.delete", "e5", None,
81
+ {"count": 2410, "scope": "stale-sessions"}, "Delete 2,410 stale session records",
82
+ "privacy", {"policy": "gdpr-erasure"}, False, 1, 60))
83
+ # an undeclared action type observed on ap-invoices -> shows a coverage gap
84
+ store.create_request(_payload("ap-invoices", "staging", "vendor.payout", "z9", None,
85
+ {"supplier": "INV-7781", "amount": 1250000, "currency": "gbp"}, "Vendor payout INV-7781",
86
+ "finance-ops", {}, False, 8, 60))
87
+
88
+ # a little decided history (for overview counts + the ledger)
89
+ hist1 = store.create_request(_payload("support-refunds", "prod", "refund.issue", "c3", "stripe.refunds.create",
90
+ {"charge": "ch_884", "amount": 400000, "currency": "gbp"}, "Refund £4,000 on charge ch_884",
91
+ "finance-ops", {"ticket": "ZD-99044"}, False, 90, 240))
92
+ store.decide(hist1["id"], "approved", {"amount": 50000}, "Approved at £500 per policy cap", reviewer)
93
+ hist2 = store.create_request(_payload("sre-remediation", "prod", "deploy.rollback", "a1", "kubectl.rollout.undo",
94
+ {"service": "search-indexer", "version": 12}, "Roll back search-indexer to version 12",
95
+ "sre-oncall", {"alert": "PD-4460"}, False, 120, 240))
96
+ store.decide(hist2["id"], "rejected", None, "Prefer forward-fix; rollback drops the new index", reviewer)