fleetwrit-server 0.0.1__py3-none-any.whl
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.
- fleetwrit_server/__init__.py +5 -0
- fleetwrit_server/app.py +169 -0
- fleetwrit_server/cli.py +141 -0
- fleetwrit_server/schema.sql +30 -0
- fleetwrit_server/seed.py +96 -0
- fleetwrit_server/static/assets/index-6Cu-XtEX.js +67 -0
- fleetwrit_server/static/assets/index-CjuqbQU3.css +1 -0
- fleetwrit_server/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- fleetwrit_server/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- fleetwrit_server/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- fleetwrit_server/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- fleetwrit_server/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- fleetwrit_server/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- fleetwrit_server/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- fleetwrit_server/static/assets/spline-sans-mono-latin-ext-wght-normal-Dh0aNLWd.woff2 +0 -0
- fleetwrit_server/static/assets/spline-sans-mono-latin-wght-normal-DlaB5ohX.woff2 +0 -0
- fleetwrit_server/static/index.html +14 -0
- fleetwrit_server/store.py +330 -0
- fleetwrit_server-0.0.1.dist-info/METADATA +33 -0
- fleetwrit_server-0.0.1.dist-info/RECORD +22 -0
- fleetwrit_server-0.0.1.dist-info/WHEEL +4 -0
- fleetwrit_server-0.0.1.dist-info/entry_points.txt +2 -0
fleetwrit_server/app.py
ADDED
|
@@ -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")
|
fleetwrit_server/cli.py
ADDED
|
@@ -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);
|
fleetwrit_server/seed.py
ADDED
|
@@ -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)
|