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.
@@ -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}}
@@ -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
+ [![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,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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fleetwrit-server = fleetwrit_server.cli:main