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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2) format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-greek-wght-normal-CkhJZR-_.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-latin-wght-normal-Dx4kXJAl.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Spline Sans Mono Variable;font-style:normal;font-display:swap;font-weight:300 700;src:url(/assets/spline-sans-mono-latin-ext-wght-normal-Dh0aNLWd.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Spline Sans Mono Variable;font-style:normal;font-display:swap;font-weight:300 700;src:url(/assets/spline-sans-mono-latin-wght-normal-DlaB5ohX.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg: #f4f6f8;--bg-soft: #eaedf1;--rail: #f0f2f5;--card: #ffffff;--ink: #1a1d23;--text: #4a5058;--muted: #838a94;--line: #e7e9ee;--line-2: #d6dae1;--accent: #2b27a6;--accent-ink: #201d84;--accent-wash: #ecebfb;--stamp-red: #b3261e;--stamp-green: #1c6b49;--stamp-amber: #93611a;--r: 4px;--r-lg: 6px;--shadow-lg: 0 22px 55px -26px rgba(21, 23, 40, .42);--sans: "Inter Variable", system-ui, -apple-system, sans-serif;--display: "Inter Variable", system-ui, -apple-system, sans-serif;--mono: "Spline Sans Mono Variable", ui-monospace, "SF Mono", Menlo, monospace;--sidebar-w: 224px}*,*:before,*:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%}body{margin:0;font-family:var(--sans);font-size:.875rem;line-height:1.5;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-synthesis:none}h1,h2,h3,h4{font-family:var(--display);color:var(--ink);line-height:1.2;letter-spacing:-.01em;font-weight:600;margin:0}p{margin:0}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-ink)}img,svg{max-width:100%;display:block}::selection{background:var(--accent);color:#fff}:root{accent-color:var(--accent);caret-color:var(--accent)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}*{scrollbar-color:var(--line-2) transparent}::-webkit-scrollbar{width:11px;height:11px}::-webkit-scrollbar-thumb{background:var(--line-2);border:3px solid var(--bg);border-radius:6px}.mono{font-family:var(--mono);font-feature-settings:"tnum"}.tnum{font-variant-numeric:tabular-nums}.muted{color:var(--muted)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;font-family:var(--sans);font-weight:600;font-size:.9375rem;line-height:1;letter-spacing:.01em;padding:.72rem 1.2rem;border-radius:var(--r);border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer;transition:background .13s ease,border-color .13s ease,color .13s ease}.btn:hover{background:var(--accent-ink);border-color:var(--accent-ink)}.btn:not(.btn--ghost):after{content:"";position:absolute;top:3px;right:3px;bottom:3px;left:3px;border:1px solid rgba(255,255,255,.34);border-radius:2px;pointer-events:none}.btn--ghost{background:transparent;color:var(--ink);border-color:var(--line-2)}.btn--ghost:hover{background:var(--card);border-color:var(--ink);color:var(--ink)}.btn--reject{background:var(--stamp-red);border-color:var(--stamp-red)}.btn--reject:hover{background:#8f1e17;border-color:#8f1e17}.stamp{display:inline-flex;align-items:center;gap:.34rem;font-family:var(--mono);font-weight:500;font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;padding:.2rem .5rem;border-radius:var(--r);border:1px solid currentColor;line-height:1;white-space:nowrap}.stamp:before{content:"";width:5px;height:5px;border-radius:1px;background:currentColor;flex:none}.stamp--green{color:var(--stamp-green)}.stamp--accent{color:var(--accent)}.stamp--muted{color:var(--muted)}.stamp--red{color:var(--stamp-red)}.stamp--amber{color:var(--stamp-amber)}.shell{display:grid;grid-template-columns:var(--sidebar-w) 1fr;min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;background:var(--rail);border-right:1px solid var(--line-2)}.sidebar__brand{display:flex;align-items:center;gap:.6rem;padding:1.15rem 1.25rem;border-bottom:1px solid var(--line)}.sidebar__mark{color:var(--accent);display:flex}.sidebar__wordmark{font-family:var(--sans);font-weight:700;font-size:1.1rem;color:var(--ink);line-height:1;letter-spacing:-.02em}.nav{display:flex;flex-direction:column;padding:.85rem .7rem;gap:.1rem}.nav__link{display:flex;align-items:center;gap:.65rem;padding:.55rem .65rem;border-radius:var(--r);color:var(--text);font-weight:500;font-size:.9375rem;border:1px solid transparent}.nav__link:hover{background:var(--bg-soft);color:var(--ink)}.nav__link.is-active{background:var(--accent-wash);color:var(--accent-ink);border-color:color-mix(in srgb,var(--accent) 22%,transparent)}.nav__icon{color:currentColor;flex:none}.nav__count{margin-left:auto;font-size:.7rem;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);border-radius:var(--r);padding:.05rem .35rem;line-height:1.4}.sidebar__foot{margin-top:auto;padding:1rem 1.25rem;border-top:1px solid var(--line)}.sidebar__reviewer{display:flex;flex-direction:column;gap:.15rem}.sidebar__reviewer-label{font-size:.6rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}.sidebar__reviewer-name{font-size:.875rem;color:var(--ink)}.main{display:flex;flex-direction:column;min-width:0}.topbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;height:56px;padding:0 clamp(1.15rem,3vw,2.4rem);background:color-mix(in srgb,var(--bg) 82%,transparent);-webkit-backdrop-filter:saturate(130%) blur(10px);backdrop-filter:saturate(130%) blur(10px);border-bottom:1px solid var(--line)}.topbar__title{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}.topbar__demo{display:inline-flex;align-items:center;gap:.45rem;font-size:.68rem;letter-spacing:.06em;color:var(--stamp-amber);border:1px solid currentColor;border-radius:var(--r);padding:.25rem .55rem}.topbar__dot{width:6px;height:6px;border-radius:1px;background:currentColor}.content{padding:1.3rem clamp(1.1rem,2.5vw,1.8rem) 3rem}.page{max-width:1220px;margin-inline:auto;display:flex;flex-direction:column;gap:1.15rem}.pagehead{display:flex;flex-direction:column;gap:.85rem}.pagehead__rule{display:flex;align-items:baseline;gap:.9rem;font-size:.72rem;color:var(--accent);letter-spacing:.04em}.pagehead__rule:after{content:"";flex:1;height:1px;background:var(--line-2);align-self:center}.pagehead__no{color:var(--accent);font-weight:600}.pagehead__file{color:var(--muted)}.pagehead__row{display:flex;align-items:flex-end;justify-content:space-between;gap:1.5rem;flex-wrap:wrap}.pagehead__title{font-size:1.55rem;font-weight:700;letter-spacing:-.02em}.pagehead__lede{color:var(--muted);margin-top:.25rem;max-width:60ch;font-size:.85rem}.pagehead__actions{display:flex;align-items:center;gap:.8rem}.sectionhead{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1rem;padding-bottom:.65rem;border-bottom:1px solid var(--line)}.sectionhead__title{font-size:1rem;font-weight:600}.sectionhead__note{font-size:.7rem;color:var(--muted);letter-spacing:.04em}.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:.85rem}.tile{padding:1rem 1.1rem;border:1px solid var(--line-2);border-radius:var(--r-lg);background:var(--card);display:flex;flex-direction:column;gap:.45rem}.tile__label{font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}.tile__value{font-size:1.8rem;color:var(--ink);line-height:1;font-weight:600}.tile__foot{font-size:.8rem;color:var(--text)}.tile__delta{display:inline-flex;align-items:center;gap:.35rem}.tile__delta--good{color:var(--stamp-green)}.tile__delta--flag{color:var(--stamp-amber)}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem}.panel{background:var(--card);border:1px solid var(--line-2);border-radius:var(--r-lg);padding:1.35rem 1.4rem}.panel__h{font-size:.95rem;font-weight:600;margin-bottom:.85rem}.panel__body{color:var(--text)}.panel__more{display:inline-flex;align-items:center;gap:.4rem;margin-top:1rem;font-size:.875rem;font-weight:600}.reg{list-style:none;margin:0;padding:0}.reg__row{display:grid;grid-template-columns:minmax(9rem,auto) 1fr auto;align-items:center;gap:.9rem;padding:.6rem 0;border-top:1px solid var(--line)}.reg__row:first-child{border-top:none}.reg__row--3{grid-template-columns:minmax(8rem,auto) 1fr auto}.reg__key{font-size:.9rem;color:var(--ink)}.reg__sub{font-size:.8rem;color:var(--muted)}.reg__bar{height:6px;background:var(--bg-soft);border-radius:1px;overflow:hidden}.reg__bar-fill{display:block;height:100%;background:var(--accent);opacity:.75}.reg__val{font-size:.95rem;color:var(--ink);text-align:right}.gaps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.9rem}.gap{display:flex;gap:.7rem;align-items:flex-start}.gap__icon{color:var(--stamp-amber);flex:none;margin-top:.15rem}.gap__title{color:var(--ink);font-weight:600;font-size:.95rem}.gap__note{font-size:.78rem;color:var(--muted);margin-top:.15rem}.chain-strip{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;padding:.9rem 1.2rem;border:1px solid var(--line-2);border-radius:var(--r-lg);background:var(--card)}.chain-strip__stamp{display:inline-flex;align-items:center;gap:.45rem;color:var(--stamp-green);font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;border:1px solid currentColor;border-radius:var(--r);padding:.3rem .6rem}.chain-strip__note{font-size:.8rem;color:var(--muted);margin-left:auto}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}.filters{display:flex;flex-wrap:wrap;gap:.4rem}.filter{display:inline-flex;align-items:center;gap:.4rem;padding:.35rem .7rem;border:1px solid var(--line-2);border-radius:var(--r);background:var(--card);color:var(--text);cursor:pointer;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em}.filter:hover{border-color:var(--ink);color:var(--ink)}.filter.is-active{background:var(--accent-wash);color:var(--accent-ink);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}.filter__count{font-size:.7rem;opacity:.8}.toolbar__count{font-size:.75rem;color:var(--muted)}.table{border:1px solid var(--line-2);border-radius:var(--r-lg);overflow:hidden;background:var(--card)}.table__head,.table__row{display:grid;align-items:center;gap:.9rem;padding:.7rem 1.1rem}.table__head{grid-template-columns:8.5rem 1fr 5rem 7rem 6rem 2.2rem;background:var(--bg-soft);border-bottom:1px solid var(--line-2);font-size:.64rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.table__row{grid-template-columns:8.5rem 1fr 5rem 7rem 6rem 2.2rem;border-top:1px solid var(--line);font-size:.9rem;color:var(--text)}.table__row:first-of-type{border-top:none}.table__row--btn{width:100%;text-align:left;background:none;border-top:1px solid var(--line);cursor:pointer;font:inherit;color:inherit}.table__row--btn:hover{background:var(--bg-soft)}.table__title{color:var(--ink);font-weight:500}.table__go{color:var(--muted);justify-self:end}.table__row--btn:hover .table__go{color:var(--accent)}.col-right{text-align:right;justify-self:end}.cell-wrap{white-space:normal;word-break:break-word}.cell-flag{color:var(--stamp-amber)}.table__row--flag{background:color-mix(in srgb,var(--stamp-amber) 6%,var(--card))}.empty{padding:1.5rem 1.1rem;color:var(--muted);font-size:.9rem}.table--agents .table__head,.table--agents .table__row{grid-template-columns:9rem 4.5rem 8rem 6rem 5.5rem 1fr 3rem 6.5rem}.table--catalog .table__head,.table--catalog .table__row{grid-template-columns:9rem 1fr 5rem 6rem 12rem 3.2rem 4rem 5rem}.table--ledger .table__head,.table--ledger .table__row{grid-template-columns:3rem 11rem 9rem 8.5rem 6rem 1fr}.toggle{display:inline-flex;align-items:center;gap:.45rem;background:none;border:none;cursor:pointer;padding:0;font:inherit;color:var(--text)}.toggle__track{width:30px;height:17px;border-radius:2px;background:var(--accent);border:1px solid var(--accent-ink);position:relative;transition:background .13s ease}.toggle__thumb{position:absolute;top:1px;left:15px;width:12px;height:12px;border-radius:1px;background:#fff;transition:left .13s ease}.toggle.is-off .toggle__track{background:var(--line-2);border-color:var(--line-2)}.toggle.is-off .toggle__thumb{left:1px}.toggle__label{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}.ledger-actions{display:flex;align-items:center;gap:.8rem}.chain-badge{display:inline-flex;align-items:center;gap:.4rem;color:var(--stamp-green);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;border:1px solid currentColor;border-radius:var(--r);padding:.3rem .55rem}.chain-badge.is-broken{color:var(--stamp-red)}.verify-result{display:flex;align-items:center;gap:.5rem;padding:.7rem .95rem;border:1px solid var(--stamp-green);color:var(--stamp-green);border-radius:var(--r);font-size:.82rem}.verify-result.is-broken{border-color:var(--stamp-red);color:var(--stamp-red)}.search{min-width:min(28rem,60vw)}.backlink{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--text)}.backlink:hover{color:var(--accent)}.flip{transform:rotate(180deg)}.detailhead{display:flex;flex-direction:column;gap:.7rem}.detailhead__rule{display:flex;align-items:baseline;gap:.9rem;font-size:.72rem;color:var(--accent)}.detailhead__rule:after{content:"";flex:1;height:1px;background:var(--line-2);align-self:center}.detailhead__row{display:flex;align-items:flex-start;justify-content:space-between;gap:1.2rem;flex-wrap:wrap}.detailhead__title{font-size:clamp(1.7rem,3vw,2.3rem);max-width:30ch}.detailhead__stamps{display:flex;gap:.5rem;flex:none;padding-top:.4rem}.detailhead__meta{font-size:.8rem;color:var(--muted);display:flex;gap:.55rem;align-items:center;flex-wrap:wrap}.detailhead__meta .dot{color:var(--line-2)}.detail-grid{display:grid;grid-template-columns:1fr 340px;gap:1.1rem;align-items:start}.detail-main{display:flex;flex-direction:column;gap:1.1rem;min-width:0}.detail-side{position:sticky;top:76px}.args{margin:0;display:flex;flex-direction:column}.args__row{display:grid;grid-template-columns:8.5rem 1fr;gap:1rem;padding:.55rem 0;border-top:1px solid var(--line)}.args__row:first-child{border-top:none}.args__key{font-size:.7rem;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);align-self:center}.args__val{color:var(--ink);display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.args__editable{font-size:.6rem;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);border-radius:var(--r);padding:.05rem .3rem}.args__json{margin:0;white-space:pre-wrap;word-break:break-word;font-size:.78rem;line-height:1.45;max-width:100%}.inline-arrow{display:inline;vertical-align:middle;color:var(--muted);margin:0 .15rem}.strike{text-decoration:line-through;color:var(--muted)}.edited{color:var(--accent-ink);font-weight:600}.verdict{display:flex;gap:.8rem;align-items:flex-start;margin-bottom:1rem}.verdict__text{font-size:.9rem;color:var(--text)}.fingerprint{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;padding:.7rem .85rem;background:var(--bg-soft);border:1px solid var(--line);border-radius:var(--r)}.fingerprint__label{font-size:.62rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.fingerprint__value{font-size:.85rem;color:var(--ink);word-break:break-all}.fingerprint--new{background:var(--accent-wash);border-color:color-mix(in srgb,var(--accent) 25%,transparent);margin-top:.5rem}.prior{list-style:none;margin:0;padding:0}.prior__row{display:grid;grid-template-columns:auto 1fr auto;gap:.8rem;align-items:center;padding:.55rem 0;border-top:1px solid var(--line)}.prior__row:first-child{border-top:none}.prior__title{color:var(--ink);font-size:.9rem}.prior__by{font-size:.72rem;color:var(--muted);text-align:right}.decision{background:var(--card);border:1px solid var(--line-2);border-radius:var(--r-lg);padding:1.3rem 1.35rem;box-shadow:var(--shadow-lg);display:flex;flex-direction:column;gap:.9rem}.decision__modes{display:flex;flex-direction:column;gap:.4rem}.decision__mode{display:flex;align-items:center;gap:.55rem;padding:.6rem .75rem;border:1px solid var(--line-2);border-radius:var(--r);background:var(--bg);color:var(--ink);cursor:pointer;font:inherit;font-size:.9rem;font-weight:500}.decision__mode:hover{border-color:var(--ink)}.decision__mode.is-active{border-color:var(--accent);background:var(--accent-wash);color:var(--accent-ink)}.decision__mode--reject.is-active{border-color:var(--stamp-red);background:color-mix(in srgb,var(--stamp-red) 8%,var(--card));color:var(--stamp-red)}.decision__mode svg{flex:none}.decision__fields{display:flex;flex-direction:column;gap:.7rem}.decision__prompt,.decision__error{font-size:.78rem}.decision__prompt{color:var(--muted)}.decision__error{color:var(--stamp-red)}.decision__submit{width:100%;margin-top:.2rem}.field{display:flex;flex-direction:column;gap:.3rem}.field__label{font-size:.66rem;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}.field__input{font-family:var(--sans);font-size:.95rem;color:var(--ink);background:var(--bg);border:1px solid var(--line-2);border-radius:var(--r);padding:.55rem .65rem}.field__input.mono{font-family:var(--mono)}.field__input:focus{outline:none;border-color:var(--accent)}.field__input::placeholder{color:var(--muted)}.field__hint{font-size:.68rem;color:var(--muted)}.field--confirm .phrase{color:var(--stamp-red);font-weight:700;letter-spacing:.05em}textarea.field__input{resize:vertical;line-height:1.5}.receipt{background:var(--card);border:1px solid var(--line-2);border-radius:var(--r-lg);padding:1.4rem 1.35rem;box-shadow:var(--shadow-lg);display:flex;flex-direction:column;gap:.65rem;align-items:flex-start}.receipt--ok{border-color:color-mix(in srgb,var(--stamp-green) 45%,var(--line-2))}.receipt--halt{border-color:color-mix(in srgb,var(--stamp-red) 40%,var(--line-2))}.receipt__seal{color:var(--accent);margin-bottom:.2rem}.receipt--halt .receipt__seal{color:var(--stamp-red)}.receipt__title{font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:var(--ink)}.receipt__lines{margin:.4rem 0 0;width:100%;display:flex;flex-direction:column}.receipt__line{display:grid;grid-template-columns:5.5rem 1fr;gap:.7rem;padding:.4rem 0;border-top:1px dotted var(--line-2)}.receipt__line:first-child{border-top:none}.receipt__line dt{font-size:.62rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);align-self:center}.receipt__line dd{margin:0;font-size:.82rem;color:var(--ink);word-break:break-all}.receipt__reason{font-family:var(--sans);font-size:.85rem;color:var(--text);word-break:normal}.receipt__signed{font-size:.78rem;color:var(--stamp-green)}.receipt--halt .receipt__signed{color:var(--stamp-red)}.receipt__note{font-size:.72rem;color:var(--muted)}@media (max-width: 960px){.tiles{grid-template-columns:repeat(2,1fr)}.detail-grid{grid-template-columns:1fr}.detail-side{position:static}}@media (max-width: 860px){:root{--sidebar-w: 64px}.sidebar__wordmark,.nav__link span:not(.nav__count),.sidebar__foot{display:none}.sidebar__brand{justify-content:center;padding-inline:0}.nav__link{justify-content:center}.nav__count{position:absolute;margin:0;transform:translate(.9rem,-.9rem)}.nav__link{position:relative}.table{overflow-x:auto}.table__head,.table__row{min-width:720px}.grid-2{grid-template-columns:1fr}}@media (max-width: 620px){.tiles{grid-template-columns:1fr}.pagehead__row{flex-direction:column;align-items:flex-start}}.topbar__env{display:inline-flex;align-items:center;gap:.5rem;font-size:.85rem}.topbar__ws{font-weight:600;color:var(--ink)}.topbar__sep{color:var(--line-2)}.topbar__envchip{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--accent);background:var(--accent-wash);border-radius:var(--r);padding:.15rem .5rem}.range{display:inline-flex;gap:.15rem;padding:.2rem;background:var(--bg-soft);border:1px solid var(--line-2);border-radius:var(--r-lg);align-self:flex-start}.range__tab{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;background:none;cursor:pointer;font:inherit;font-size:.8rem;color:var(--muted);padding:.35rem .75rem;border-radius:var(--r);line-height:1}.range__tab:hover{color:var(--ink)}.range__tab.is-active{background:var(--card);color:var(--ink);box-shadow:0 1px 2px #14161c17}.chart{width:100%;height:150px;display:block}.chart__axis{stroke:var(--line-2);stroke-width:1}.chart__bar{fill:var(--accent);opacity:.82;transition:opacity .12s ease}.chart__bar:hover{opacity:1}.table__head .mono,.table__head span{font-family:var(--sans);letter-spacing:.06em}@media (prefers-reduced-motion: reduce){*{transition:none!important}}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Fleetwrit — Office of Record</title>
|
|
7
|
+
<meta name="description" content="The independent authorisation record for consequential AI-agent actions." />
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-6Cu-XtEX.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CjuqbQU3.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""SQLite store for the Fleetwrit v0 local server.
|
|
2
|
+
|
|
3
|
+
Agents, action types, requests, decisions and a hash-chained ledger; signs
|
|
4
|
+
receipts with Ed25519. Reuses the SDK fingerprint so edits recompute identically.
|
|
5
|
+
Schema lives in schema.sql.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Iterator
|
|
21
|
+
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
23
|
+
|
|
24
|
+
from fleetwrit.fingerprint import fingerprint as fw_fingerprint
|
|
25
|
+
|
|
26
|
+
_LOCK = threading.RLock()
|
|
27
|
+
KID = "fleetwrit-dev-1"
|
|
28
|
+
_SCHEMA = Path(__file__).with_name("schema.sql")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _now() -> str:
|
|
32
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_ts(value: str) -> datetime | None:
|
|
36
|
+
"""Parse an RFC 3339 timestamp (``Z``, explicit offset, or fractional seconds).
|
|
37
|
+
|
|
38
|
+
Returns None if unparseable so callers can decide how to fail, rather than a
|
|
39
|
+
narrow format silently disabling expiry on otherwise-valid deadlines.
|
|
40
|
+
"""
|
|
41
|
+
v = value.strip()
|
|
42
|
+
if v.endswith(("Z", "z")):
|
|
43
|
+
v = v[:-1] + "+00:00"
|
|
44
|
+
try:
|
|
45
|
+
dt = datetime.fromisoformat(v)
|
|
46
|
+
except ValueError:
|
|
47
|
+
return None
|
|
48
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _canon(obj: Any) -> str:
|
|
52
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sha(text: str) -> str:
|
|
56
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _b64(obj: dict[str, Any]) -> str:
|
|
60
|
+
return _b64b(json.dumps(obj, separators=(",", ":")).encode("utf-8"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _b64b(data: bytes) -> str:
|
|
64
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Store:
|
|
68
|
+
def __init__(self, path: str = "fleetwrit-dev.db") -> None:
|
|
69
|
+
self.path = path
|
|
70
|
+
self._key = Ed25519PrivateKey.generate()
|
|
71
|
+
# One persistent autocommit connection, serialized by _LOCK: a single-
|
|
72
|
+
# process dev server, so nested writes (e.g. decide -> ledger) can't
|
|
73
|
+
# deadlock two WAL writers.
|
|
74
|
+
self._db = sqlite3.connect(path, timeout=10, check_same_thread=False, isolation_level=None)
|
|
75
|
+
self._db.row_factory = sqlite3.Row
|
|
76
|
+
self._db.execute("PRAGMA journal_mode=WAL")
|
|
77
|
+
self._db.execute("PRAGMA busy_timeout=5000")
|
|
78
|
+
with _LOCK:
|
|
79
|
+
self._db.executescript(_SCHEMA.read_text())
|
|
80
|
+
|
|
81
|
+
@contextmanager
|
|
82
|
+
def _cx(self) -> Iterator[sqlite3.Connection]:
|
|
83
|
+
yield self._db
|
|
84
|
+
|
|
85
|
+
# --- ledger ---
|
|
86
|
+
def append_ledger(self, actor: str, event: str, payload: dict[str, Any], refs: dict[str, Any]) -> None:
|
|
87
|
+
with _LOCK, self._cx() as c:
|
|
88
|
+
row = c.execute("SELECT hash FROM ledger ORDER BY seq DESC LIMIT 1").fetchone()
|
|
89
|
+
prev = row["hash"] if row else "genesis"
|
|
90
|
+
ts = _now()
|
|
91
|
+
ph = _sha(_canon(payload))
|
|
92
|
+
digest = _sha("|".join([prev, ts, actor, event, ph, _canon(refs)]))
|
|
93
|
+
c.execute("INSERT INTO ledger(ts,actor,event,payload_hash,refs,prev_hash,hash) VALUES(?,?,?,?,?,?,?)", (ts, actor, event, ph, _canon(refs), prev, digest))
|
|
94
|
+
|
|
95
|
+
def ledger(self, limit: int = 200) -> list[dict[str, Any]]:
|
|
96
|
+
with _LOCK, self._cx() as c:
|
|
97
|
+
rows = c.execute("SELECT * FROM ledger ORDER BY seq DESC LIMIT ?", (limit,)).fetchall()
|
|
98
|
+
return [dict(r) for r in rows]
|
|
99
|
+
|
|
100
|
+
def verify_chain(self) -> dict[str, Any]:
|
|
101
|
+
with _LOCK, self._cx() as c:
|
|
102
|
+
rows = c.execute("SELECT * FROM ledger ORDER BY seq ASC").fetchall()
|
|
103
|
+
prev = "genesis"
|
|
104
|
+
for r in rows:
|
|
105
|
+
if r["prev_hash"] != prev:
|
|
106
|
+
return {"ok": False, "broken_seq": r["seq"]}
|
|
107
|
+
digest = _sha("|".join([prev, r["ts"], r["actor"], r["event"], r["payload_hash"], r["refs"]]))
|
|
108
|
+
if digest != r["hash"]:
|
|
109
|
+
return {"ok": False, "broken_seq": r["seq"]}
|
|
110
|
+
prev = r["hash"]
|
|
111
|
+
return {"ok": True, "events": len(rows)}
|
|
112
|
+
|
|
113
|
+
# --- registry ---
|
|
114
|
+
def register(self, agent: dict[str, Any], action_types: list[dict[str, Any]], runtime: str | None = None) -> None:
|
|
115
|
+
with _LOCK, self._cx() as c:
|
|
116
|
+
c.execute("INSERT INTO agents(agent_id,environment,version,runtime,owner,last_seen) VALUES(?,?,?,?,?,?) ON CONFLICT(agent_id,environment) DO UPDATE SET version=excluded.version, last_seen=excluded.last_seen, runtime=COALESCE(excluded.runtime, agents.runtime)", (agent["id"], agent.get("environment", "prod"), agent.get("version"), runtime, None, _now()))
|
|
117
|
+
for d in action_types:
|
|
118
|
+
c.execute("INSERT INTO action_types(type,version,title,risk,reversible,editable,queue,owner,summary,args_schema,display,undeclared,approval_rate) VALUES(?,?,?,?,?,?,?,?,?,?,?,0,?) ON CONFLICT(type) DO UPDATE SET version=excluded.version, title=excluded.title, risk=excluded.risk, reversible=excluded.reversible, editable=excluded.editable, queue=excluded.queue, owner=excluded.owner, summary=excluded.summary, display=excluded.display, undeclared=0", (d.get("type"), d.get("version"), d.get("title"), d.get("risk", "medium"), 1 if d.get("reversible", True) else 0, _canon(d.get("editable", [])), d.get("queue"), d.get("owner"), d.get("summary"), _canon(d.get("args_schema", {})), _canon(d.get("display", {})), d.get("approval_rate")))
|
|
119
|
+
self.append_ledger(agent["id"], "agent.registered", agent, {"agent": agent["id"]})
|
|
120
|
+
|
|
121
|
+
def agents(self) -> list[dict[str, Any]]:
|
|
122
|
+
with _LOCK, self._cx() as c:
|
|
123
|
+
rows = c.execute("SELECT * FROM agents ORDER BY last_seen DESC").fetchall()
|
|
124
|
+
out = []
|
|
125
|
+
for r in rows:
|
|
126
|
+
d = dict(r)
|
|
127
|
+
d["disabled"] = bool(d["disabled"])
|
|
128
|
+
out.append(d)
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
def action_types(self) -> list[dict[str, Any]]:
|
|
132
|
+
with _LOCK, self._cx() as c:
|
|
133
|
+
rows = c.execute("SELECT * FROM action_types ORDER BY type").fetchall()
|
|
134
|
+
out = []
|
|
135
|
+
for r in rows:
|
|
136
|
+
d = dict(r)
|
|
137
|
+
d["editable"] = json.loads(d.get("editable") or "[]")
|
|
138
|
+
d["reversible"] = bool(d["reversible"])
|
|
139
|
+
d["undeclared"] = bool(d["undeclared"])
|
|
140
|
+
out.append(d)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
def _action_type(self, type_: str) -> dict[str, Any] | None:
|
|
144
|
+
with _LOCK, self._cx() as c:
|
|
145
|
+
r = c.execute("SELECT * FROM action_types WHERE type=?", (type_,)).fetchone()
|
|
146
|
+
return dict(r) if r else None
|
|
147
|
+
|
|
148
|
+
# --- requests ---
|
|
149
|
+
def create_request(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
idem = payload["idempotency_key"]
|
|
151
|
+
expires_at = payload.get("expires_at")
|
|
152
|
+
if expires_at and _parse_ts(expires_at) is None:
|
|
153
|
+
raise ValueError(f"invalid expires_at timestamp: {expires_at!r}")
|
|
154
|
+
with _LOCK, self._cx() as c:
|
|
155
|
+
existing = c.execute("SELECT id FROM requests WHERE idempotency_key=?", (idem,)).fetchone()
|
|
156
|
+
if existing:
|
|
157
|
+
return {"id": existing["id"], "state": "reattached"}
|
|
158
|
+
action = payload["action"]
|
|
159
|
+
at = self._action_type(action["type"])
|
|
160
|
+
risk = (at or {}).get("risk", "medium")
|
|
161
|
+
editable = json.loads((at or {}).get("editable") or "[]")
|
|
162
|
+
title = (at or {}).get("title") or action["type"]
|
|
163
|
+
display = (at or {}).get("display") or "{}"
|
|
164
|
+
rid = "req_" + uuid.uuid4().hex[:12]
|
|
165
|
+
ag = payload["agent"]
|
|
166
|
+
with _LOCK, self._cx() as c:
|
|
167
|
+
c.execute("INSERT INTO requests(id,idempotency_key,kind,agent_id,environment,type,action_version,tool,args,reversible,fingerprint,summary,context,queue,provenance,risk,editable,title,display,created_at,expires_at,on_expiry,state,verdict) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (rid, idem, payload["kind"], ag["id"], ag.get("environment", "prod"), action["type"], action.get("version"), action.get("tool"), _canon(action["args"]), 1 if action.get("reversible", True) else 0, payload["fingerprint"], payload.get("summary"), _canon(payload.get("context", {})), payload.get("queue"), _canon(payload.get("provenance", {})), risk, _canon(editable), title, display, payload.get("created_at"), payload.get("expires_at"), payload.get("on_expiry", "reject"), "pending", "ask"))
|
|
168
|
+
if at is None:
|
|
169
|
+
c.execute("INSERT OR IGNORE INTO action_types(type,version,title,risk,reversible,editable,queue,owner,summary,args_schema,display,undeclared,approval_rate) VALUES(?,?,?,?,?,?,?,?,?,?,?,1,NULL)", (action["type"], action.get("version"), action["type"], risk, 1 if action.get("reversible", True) else 0, "[]", payload.get("queue"), None, payload.get("summary"), "{}", "{}"))
|
|
170
|
+
c.execute("UPDATE agents SET last_seen=? WHERE agent_id=?", (_now(), ag["id"]))
|
|
171
|
+
self.append_ledger(ag["id"], "request.created", {"fingerprint": payload["fingerprint"], "type": action["type"]}, {"request": rid})
|
|
172
|
+
self.append_ledger("opa", "policy.verdict", {"engine": "opa", "verdict": "ask", "type": action["type"]}, {"request": rid})
|
|
173
|
+
return {"id": rid, "state": "pending"}
|
|
174
|
+
|
|
175
|
+
def get_request(self, rid: str) -> dict[str, Any] | None:
|
|
176
|
+
with _LOCK, self._cx() as c:
|
|
177
|
+
r = c.execute("SELECT * FROM requests WHERE id=?", (rid,)).fetchone()
|
|
178
|
+
d = c.execute("SELECT * FROM decisions WHERE request_id=?", (rid,)).fetchone()
|
|
179
|
+
if not r:
|
|
180
|
+
return None
|
|
181
|
+
req = dict(r)
|
|
182
|
+
for k in ("args", "context", "provenance", "editable", "display"):
|
|
183
|
+
req[k] = json.loads(req[k] or ("[]" if k == "editable" else "{}"))
|
|
184
|
+
req["reversible"] = bool(req["reversible"])
|
|
185
|
+
if d:
|
|
186
|
+
dd = dict(d)
|
|
187
|
+
dd["final_args"] = json.loads(dd["final_args"] or "{}")
|
|
188
|
+
dd["reviewer"] = json.loads(dd["reviewer"]) if dd["reviewer"] else None
|
|
189
|
+
req["decision"] = dd
|
|
190
|
+
return req
|
|
191
|
+
|
|
192
|
+
def inbox(self) -> list[dict[str, Any]]:
|
|
193
|
+
with _LOCK, self._cx() as c:
|
|
194
|
+
rows = c.execute("SELECT id FROM requests WHERE state='pending' ORDER BY created_at ASC").fetchall()
|
|
195
|
+
order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
196
|
+
reqs = []
|
|
197
|
+
for row in rows:
|
|
198
|
+
self._maybe_expire(row["id"])
|
|
199
|
+
req = self.get_request(row["id"])
|
|
200
|
+
if req and req["state"] == "pending":
|
|
201
|
+
reqs.append(req)
|
|
202
|
+
reqs.sort(key=lambda r: (order.get(r["risk"], 2), r["expires_at"] or ""))
|
|
203
|
+
return reqs
|
|
204
|
+
|
|
205
|
+
def mark_viewed(self, rid: str, who: str) -> None:
|
|
206
|
+
with _LOCK, self._cx() as c:
|
|
207
|
+
r = c.execute("SELECT viewed_at FROM requests WHERE id=?", (rid,)).fetchone()
|
|
208
|
+
if r and not r["viewed_at"]:
|
|
209
|
+
c.execute("UPDATE requests SET viewed_at=? WHERE id=?", (_now(), rid))
|
|
210
|
+
self.append_ledger(who, "request.viewed", {}, {"request": rid})
|
|
211
|
+
|
|
212
|
+
def _maybe_expire(self, rid: str) -> None:
|
|
213
|
+
with _LOCK, self._cx() as c:
|
|
214
|
+
r = c.execute("SELECT state, expires_at FROM requests WHERE id=?", (rid,)).fetchone()
|
|
215
|
+
if not r or r["state"] != "pending" or not r["expires_at"]:
|
|
216
|
+
return
|
|
217
|
+
exp = _parse_ts(r["expires_at"])
|
|
218
|
+
if exp is None or datetime.now(timezone.utc) <= exp:
|
|
219
|
+
return
|
|
220
|
+
c.execute("UPDATE requests SET state='expired' WHERE id=?", (rid,))
|
|
221
|
+
self.append_ledger("system", "request.expired", {}, {"request": rid})
|
|
222
|
+
|
|
223
|
+
def decision(self, rid: str) -> dict[str, Any] | None:
|
|
224
|
+
self._maybe_expire(rid)
|
|
225
|
+
with _LOCK, self._cx() as c:
|
|
226
|
+
r = c.execute("SELECT * FROM requests WHERE id=?", (rid,)).fetchone()
|
|
227
|
+
d = c.execute("SELECT * FROM decisions WHERE request_id=?", (rid,)).fetchone()
|
|
228
|
+
if not r:
|
|
229
|
+
return None
|
|
230
|
+
if r["state"] == "pending":
|
|
231
|
+
return {"outcome": "pending"}
|
|
232
|
+
if not d:
|
|
233
|
+
return {"outcome": r["state"]}
|
|
234
|
+
return {
|
|
235
|
+
"outcome": d["outcome"],
|
|
236
|
+
"action": {"type": r["type"], "version": r["action_version"], "tool": r["tool"], "args": json.loads(d["final_args"] or r["args"]), "reversible": bool(r["reversible"])},
|
|
237
|
+
"original_fingerprint": d["original_fingerprint"], "final_fingerprint": d["final_fingerprint"],
|
|
238
|
+
"reason": d["reason"], "reviewer": json.loads(d["reviewer"]) if d["reviewer"] else None,
|
|
239
|
+
"receipt": d["receipt"],
|
|
240
|
+
"value": json.loads(d["value"]) if d["value"] is not None else None,
|
|
241
|
+
"option": json.loads(d["option"]) if d["option"] is not None else None,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def decide(self, rid: str, outcome: str, edits: dict[str, Any] | None, reason: str | None, reviewer: dict[str, Any], value: Any = None, option: Any = None) -> dict[str, Any]:
|
|
245
|
+
self._maybe_expire(rid)
|
|
246
|
+
req = self.get_request(rid)
|
|
247
|
+
if not req:
|
|
248
|
+
raise KeyError(rid)
|
|
249
|
+
if req["state"] != "pending":
|
|
250
|
+
raise ValueError(f"request is {req['state']}, not pending")
|
|
251
|
+
self.mark_viewed(rid, reviewer.get("email", "reviewer"))
|
|
252
|
+
args = dict(req["args"])
|
|
253
|
+
editable = set(req["editable"])
|
|
254
|
+
applied = {k: v for k, v in (edits or {}).items() if k in editable}
|
|
255
|
+
args.update(applied)
|
|
256
|
+
kind = req.get("kind", "approve")
|
|
257
|
+
if outcome == "rejected":
|
|
258
|
+
final_outcome = "rejected"
|
|
259
|
+
elif kind == "input":
|
|
260
|
+
final_outcome = "answered"
|
|
261
|
+
elif kind == "choose":
|
|
262
|
+
final_outcome = "chosen"
|
|
263
|
+
else:
|
|
264
|
+
final_outcome = "approved_with_edits" if applied else "approved"
|
|
265
|
+
final_fp = fw_fingerprint(type=req["type"], version=req["action_version"], tool=req["tool"], args=args, agent_id=req["agent_id"], environment=req["environment"])
|
|
266
|
+
receipt = self._sign(rid, final_fp, final_outcome, reviewer) if final_outcome != "rejected" else None
|
|
267
|
+
val = _canon(value) if value is not None else None
|
|
268
|
+
opt = _canon(option) if option is not None else None
|
|
269
|
+
with _LOCK, self._cx() as c:
|
|
270
|
+
c.execute("INSERT OR REPLACE INTO decisions(request_id,outcome,final_args,original_fingerprint,final_fingerprint,reason,reviewer,receipt,value,option,decided_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", (rid, final_outcome, _canon(args), req["fingerprint"], final_fp, reason, _canon(reviewer), receipt, val, opt, _now()))
|
|
271
|
+
c.execute("UPDATE requests SET state=? WHERE id=?", (final_outcome, rid))
|
|
272
|
+
self.append_ledger(reviewer.get("email", "reviewer"), "decision.made", {"outcome": final_outcome, "final_fingerprint": final_fp, "edits": applied, "reason": reason}, {"request": rid})
|
|
273
|
+
return self.decision(rid) or {}
|
|
274
|
+
|
|
275
|
+
def ack(self, rid: str) -> None:
|
|
276
|
+
with _LOCK, self._cx() as c:
|
|
277
|
+
d = c.execute("SELECT consumed_at FROM decisions WHERE request_id=?", (rid,)).fetchone()
|
|
278
|
+
if not d:
|
|
279
|
+
return # nothing decided to consume (e.g. expired/cancelled) — no-op
|
|
280
|
+
if d["consumed_at"]:
|
|
281
|
+
# exactly-once: a retry that reaches an already-consumed decision
|
|
282
|
+
# must fail, never deliver the same approval for a second run.
|
|
283
|
+
raise ValueError(f"decision for {rid} already consumed")
|
|
284
|
+
c.execute("UPDATE decisions SET consumed_at=? WHERE request_id=?", (_now(), rid))
|
|
285
|
+
self.append_ledger("agent", "decision.consumed", {}, {"request": rid})
|
|
286
|
+
|
|
287
|
+
def cancel(self, rid: str) -> None:
|
|
288
|
+
with _LOCK, self._cx() as c:
|
|
289
|
+
c.execute("UPDATE requests SET state='cancelled' WHERE id=? AND state='pending'", (rid,))
|
|
290
|
+
self.append_ledger("agent", "request.cancelled", {}, {"request": rid})
|
|
291
|
+
|
|
292
|
+
def toggle_agent(self, agent_id: str) -> None:
|
|
293
|
+
with _LOCK, self._cx() as c:
|
|
294
|
+
c.execute("UPDATE agents SET disabled=1-disabled WHERE agent_id=?", (agent_id,))
|
|
295
|
+
|
|
296
|
+
# --- overview ---
|
|
297
|
+
def overview(self) -> dict[str, Any]:
|
|
298
|
+
with _LOCK, self._cx() as c:
|
|
299
|
+
pending = [dict(r) for r in c.execute("SELECT risk,queue,created_at FROM requests WHERE state='pending'").fetchall()]
|
|
300
|
+
decided = c.execute("SELECT COUNT(*) n FROM decisions").fetchone()["n"]
|
|
301
|
+
approved = c.execute("SELECT COUNT(*) n FROM requests WHERE state IN ('approved','approved_with_edits')").fetchone()["n"]
|
|
302
|
+
expired = c.execute("SELECT COUNT(*) n FROM requests WHERE state='expired'").fetchone()["n"]
|
|
303
|
+
undeclared = c.execute("SELECT COUNT(*) n FROM action_types WHERE undeclared=1").fetchone()["n"]
|
|
304
|
+
runtimes = [dict(r) for r in c.execute("SELECT runtime, COUNT(*) n FROM agents GROUP BY runtime").fetchall()]
|
|
305
|
+
by_queue: dict[str, int] = {}
|
|
306
|
+
high = 0
|
|
307
|
+
for p in pending:
|
|
308
|
+
by_queue[p["queue"] or "unrouted"] = by_queue.get(p["queue"] or "unrouted", 0) + 1
|
|
309
|
+
if p["risk"] in ("high", "critical"):
|
|
310
|
+
high += 1
|
|
311
|
+
return {
|
|
312
|
+
"pending": len(pending), "pending_high": high, "pending_by_queue": by_queue,
|
|
313
|
+
"oldest": min((p["created_at"] for p in pending), default=None),
|
|
314
|
+
"decisions_total": decided, "approved": approved, "expired": expired, "undeclared": undeclared,
|
|
315
|
+
"agents_by_runtime": {(r["runtime"] or "unknown"): r["n"] for r in runtimes},
|
|
316
|
+
"chain": self.verify_chain(),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# --- receipts ---
|
|
320
|
+
def _sign(self, rid: str, fp: str, outcome: str, reviewer: dict[str, Any]) -> str:
|
|
321
|
+
header = {"alg": "EdDSA", "typ": "JWT", "kid": KID}
|
|
322
|
+
payload = {"request_id": rid, "fingerprint": fp, "outcome": outcome, "sub": reviewer.get("subject", reviewer.get("email", "reviewer")), "iss": reviewer.get("issuer", "https://fleetwrit.local"), "iat": int(time.time())}
|
|
323
|
+
signing_input = f"{_b64(header)}.{_b64(payload)}"
|
|
324
|
+
return f"{signing_input}.{_b64b(self._key.sign(signing_input.encode('ascii')))}"
|
|
325
|
+
|
|
326
|
+
def jwks(self) -> dict[str, Any]:
|
|
327
|
+
from cryptography.hazmat.primitives import serialization
|
|
328
|
+
|
|
329
|
+
raw = self._key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
330
|
+
return {"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": KID, "x": _b64b(raw)}]}
|
|
@@ -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
|
+
[](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,22 @@
|
|
|
1
|
+
fleetwrit_server/__init__.py,sha256=-eFLRborxC7iFbAR28bVsuz-IVviOByKNB0IRB5pixE,93
|
|
2
|
+
fleetwrit_server/app.py,sha256=BgFFGRncQcsVU3QZbZOu2PnoTrv2pZaWdNZyI5qfht8,5379
|
|
3
|
+
fleetwrit_server/cli.py,sha256=tj1SBEm-0xwFc5JMhC_RoaR-TDI8FYJVpSQYehZVmQM,5210
|
|
4
|
+
fleetwrit_server/seed.py,sha256=Xo-bfoBH__Vf9AA3uGc-yKeL8JuZkOSi2YjEaG-tgL4,5815
|
|
5
|
+
fleetwrit_server/store.py,sha256=lL95YKCPBX_pcyOSeA_oUc6n5ukD5Yh3nrWPT1kJzOk,18092
|
|
6
|
+
fleetwrit_server/static/index.html,sha256=LGcgwpvcvb7BhOZ_t5VLHhRKwvYjUdJI54eqrlN2ilI,530
|
|
7
|
+
fleetwrit_server/static/assets/index-6Cu-XtEX.js,sha256=XXyF7bQIzthKp9NuC3uQsaIjxC3FDRdwwL-I8DXbBjU,209686
|
|
8
|
+
fleetwrit_server/static/assets/index-CjuqbQU3.css,sha256=qgvUcivjWI5CVOg2xJZjKOE1ymTDEsW39zIcTdBqWpM,22965
|
|
9
|
+
fleetwrit_server/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2,sha256=yhVwYzOaxK1BjyFPOr_tEZsHmKtNN3OGzlyeWnpDXr0,25960
|
|
10
|
+
fleetwrit_server/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2,sha256=cdXuk8wenx1SCjqLZkVt4Yx4edjfCdV_zS6v91_vAHU,18748
|
|
11
|
+
fleetwrit_server/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2,sha256=bp4CCiX5tW1BjywIWx08CXJaTaI_5pOltGMGRgZzIZA,11232
|
|
12
|
+
fleetwrit_server/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2,sha256=G-NEjikvvwX_4Xb-HkPxNQE9ULHn0yStGlWPYj07tvY,18996
|
|
13
|
+
fleetwrit_server/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2,sha256=NLnFBMq3pz43t0Y0OkSRMuVs97VIGvLLgdx03P8lyVY,85068
|
|
14
|
+
fleetwrit_server/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2,sha256=MQDndehhbNJhG-7PojpCY9cDdYZ4m0PwNSNqLm-9TGI,48256
|
|
15
|
+
fleetwrit_server/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2,sha256=XGb54H6QxtSsSSLMaNYN4mwXsYWOZ3-15gP845UrP_I,10252
|
|
16
|
+
fleetwrit_server/static/assets/spline-sans-mono-latin-ext-wght-normal-Dh0aNLWd.woff2,sha256=DKmjmJBE0fm02h8wPOo8MJH1ywL-VNoE_VH86vZ27Pk,20816
|
|
17
|
+
fleetwrit_server/static/assets/spline-sans-mono-latin-wght-normal-DlaB5ohX.woff2,sha256=Rrfcr-PlHb6HvhuvqsP-12Rtsf4KFGZHpv_ONpnydS0,36476
|
|
18
|
+
fleetwrit_server/schema.sql,sha256=5JiiSncrVJnAKJtsr9TjYnArjMkogFC5or0DcgNeRLM,1363
|
|
19
|
+
fleetwrit_server-0.0.1.dist-info/METADATA,sha256=Ym4yryEB0qNS7ANWnB3-81yBehqRwpmYRhjQOrPiL1Q,1224
|
|
20
|
+
fleetwrit_server-0.0.1.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
|
|
21
|
+
fleetwrit_server-0.0.1.dist-info/entry_points.txt,sha256=lZ5Y8Dhq51pXoOrPssSLG7vGCAxexvHh3ANhRkDA1-Y,63
|
|
22
|
+
fleetwrit_server-0.0.1.dist-info/RECORD,,
|