primarypath-server 1.0.0__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.
- app/__init__.py +351 -0
- app/_version.py +8 -0
- app/agents.py +150 -0
- app/alembic.ini +37 -0
- app/auth.py +714 -0
- app/chat.py +122 -0
- app/config.py +258 -0
- app/costgov.py +345 -0
- app/db.py +56 -0
- app/faults.py +26 -0
- app/grants.py +122 -0
- app/idempotency.py +58 -0
- app/jcs.py +117 -0
- app/ledger/__init__.py +41 -0
- app/ledger/fold.py +74 -0
- app/ledger/hashing.py +19 -0
- app/ledger/projection.py +389 -0
- app/ledger/store.py +1042 -0
- app/ledger/verify.py +31 -0
- app/main.py +50 -0
- app/maintenance.py +56 -0
- app/metrics.py +89 -0
- app/migrations/env.py +20 -0
- app/migrations/script.py.mako +23 -0
- app/migrations/versions/0001_baseline.py +19 -0
- app/migrations/versions/0002_events_and_projection.py +78 -0
- app/migrations/versions/0003_slack_messages.py +27 -0
- app/migrations/versions/0004_sessions.py +31 -0
- app/migrations/versions/0005_siem_cursor.py +26 -0
- app/migrations/versions/0006_teams_messages.py +27 -0
- app/migrations/versions/0007_tenant_scoping.py +50 -0
- app/migrations/versions/0008_policy_view.py +39 -0
- app/migrations/versions/0009_api_tokens.py +37 -0
- app/migrations/versions/0010_session_groups.py +23 -0
- app/migrations/versions/0011_budgets_and_evals.py +75 -0
- app/migrations/versions/0012_run_event_ts_index.py +23 -0
- app/migrations/versions/0013_agent_chains.py +102 -0
- app/migrations/versions/0014_dual_control.py +33 -0
- app/migrations/versions/0015_sampled_mode.py +24 -0
- app/migrations/versions/0016_sampling_salt.py +25 -0
- app/migrations/versions/0017_live_tokens_index.py +25 -0
- app/migrations/versions/0018_webhook_outbox.py +47 -0
- app/migrations/versions/0019_usage_view_and_rollout_index.py +68 -0
- app/migrations/versions/0020_policy_metadata_and_lifecycle.py +67 -0
- app/migrations/versions/0021_execution_grants.py +59 -0
- app/migrations/versions/0022_digest_alg_and_executor_instance.py +45 -0
- app/migrations/versions/0023_idempotency_keys.py +38 -0
- app/migrations/versions/0024_approvals_view_keyset_index.py +26 -0
- app/netguard.py +73 -0
- app/obslog.py +105 -0
- app/policy.py +390 -0
- app/ratelimit.py +78 -0
- app/routes/__init__.py +0 -0
- app/routes/admin.py +312 -0
- app/routes/agents.py +111 -0
- app/routes/approvals.py +576 -0
- app/routes/audit.py +213 -0
- app/routes/costgov.py +150 -0
- app/routes/health.py +53 -0
- app/routes/policy.py +72 -0
- app/routes/runs.py +115 -0
- app/routes/slack.py +40 -0
- app/routes/teams.py +29 -0
- app/routes/traces.py +55 -0
- app/schemas.py +248 -0
- app/security.py +203 -0
- app/siem.py +248 -0
- app/signing.py +342 -0
- app/sla.py +234 -0
- app/slack.py +282 -0
- app/teams.py +276 -0
- app/tokens.py +117 -0
- app/traces.py +309 -0
- app/web-dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- app/web-dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- app/web-dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- app/web-dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- app/web-dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- app/web-dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- app/web-dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- app/web-dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- app/web-dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- app/web-dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- app/web-dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- app/web-dist/assets/index-B_JDe_G2.js +10 -0
- app/web-dist/assets/index-J7J-YtwM.css +1 -0
- app/web-dist/index.html +24 -0
- app/webhooks.py +405 -0
- primarypath_server-1.0.0.dist-info/METADATA +35 -0
- primarypath_server-1.0.0.dist-info/RECORD +94 -0
- primarypath_server-1.0.0.dist-info/WHEEL +5 -0
- primarypath_server-1.0.0.dist-info/entry_points.txt +3 -0
- primarypath_server-1.0.0.dist-info/licenses/LICENSE +202 -0
- primarypath_server-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,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:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2)format("woff2-variations");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,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:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--fs-2xs:.6875rem;--fs-xs:.75rem;--fs-sm:.78125rem;--fs-md:.8125rem;--fs-base:.875rem;--fs-lg:.9375rem;--fs-xl:1.125rem;--radius-sm:4px;--radius:8px;--radius-lg:12px;--surface-0:#f5f7fa;--surface-1:#fdfdfe;--surface-2:#fdfdfe;--surface-inset:#f0f3f8;--text-primary:#131824;--text-secondary:#545e70;--text-muted:#667085;--border:#e3e7ef;--border-strong:#ccd3de;--accent:#2150c8;--accent-fg:#fff;--accent-bg:#e8eefb;--ok:#067647;--ok-bg:#e6f5ec;--on-ok:#fff;--warn:#b25409;--warn-bg:#fbf1e4;--danger:#b42318;--danger-bg:#fceceb;--on-danger:#fff;--font-sans:"Geist Variable", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono:"Geist Mono Variable", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--nav-w:176px;--rail-w:200px;--case-w:clamp(380px, 30vw, 640px);--runs-detail-w:clamp(460px, 36vw, 760px);--row-h:46px}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--surface-0:#0b0e15;--surface-1:#131722;--surface-2:#191e2b;--surface-inset:#1d2331;--text-primary:#e9ebf2;--text-secondary:#9aa2b1;--text-muted:#7c8494;--border:#262c3b;--border-strong:#343c4f;--accent:#85a8fa;--accent-fg:#0c1120;--accent-bg:#172440;--ok:#34d399;--ok-bg:#12251d;--on-ok:#0c1f17;--warn:#eaa640;--warn-bg:#2a2011;--danger:#f28b82;--danger-bg:#2a1717;--on-danger:#2a0f0c}}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--surface-0:#0b0e15;--surface-1:#131722;--surface-2:#191e2b;--surface-inset:#1d2331;--text-primary:#e9ebf2;--text-secondary:#9aa2b1;--text-muted:#7c8494;--border:#262c3b;--border-strong:#343c4f;--accent:#85a8fa;--accent-fg:#0c1120;--accent-bg:#172440;--ok:#34d399;--ok-bg:#12251d;--on-ok:#0c1f17;--warn:#eaa640;--warn-bg:#2a2011;--danger:#f28b82;--danger-bg:#2a1717;--on-danger:#2a0f0c}*{box-sizing:border-box}html,body,#root{height:100%}:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.input:focus-visible,.rev-input:focus-visible,.topsearch input:focus-visible,.qscroll.has-cursor:focus-visible{outline:none}.qscroll:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}body{font-family:var(--font-sans);font-size:var(--fs-base);background:var(--surface-0);color:var(--text-primary);font-synthesis:none;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:0;line-height:1.5}.sr-only{clip:rect(0 0 0 0);width:1px;height:1px;position:absolute;overflow:hidden}.mono{font-family:var(--font-mono)}.muted{color:var(--text-muted)}.fs-xs{font-size:var(--fs-xs)}.fs-2xs{font-size:var(--fs-2xs)}.spacer{flex:1}button{font:inherit}.btn,.icon-btn,.nav-item,.view-item,.facet-opt,.seg button,.fchip,.vchip,.cbx,.evidence,.raw-toggle{transition:background-color .12s ease-out,border-color .12s ease-out,color .12s ease-out}svg.i{flex:none;width:17px;height:17px}.app{grid-template-rows:auto 1fr;height:100%;min-height:0;display:grid}.topbar{background:var(--surface-1);border-bottom:1px solid var(--border);align-items:center;gap:12px;height:52px;padding:0 14px;display:flex}.brand{align-items:baseline;gap:8px;display:flex}.brand b{font-family:var(--font-mono);font-size:var(--fs-lg);letter-spacing:-.02em;font-weight:600}.brand span{font-size:var(--fs-xs);color:var(--text-muted)}.topsearch{background:var(--surface-inset);border:1px solid var(--border);border-radius:var(--radius);max-width:460px;color:var(--text-muted);font-size:var(--fs-md);flex:1;align-items:center;gap:8px;padding:7px 11px;display:flex}.topsearch input{color:var(--text-primary);font:inherit;font-size:var(--fs-md);background:0 0;border:0;outline:none;flex:1}.top-right{align-items:center;gap:10px;margin-left:auto;display:flex}.tenant{font-size:var(--fs-md);color:var(--text-secondary);align-items:center;gap:5px;display:flex}.env{font-size:var(--fs-2xs);background:var(--ok-bg);color:var(--ok);border-radius:999px;padding:2px 8px;font-weight:500}.env.signin-off{background:var(--warn-bg);color:var(--warn)}.user{font-size:var(--fs-md);color:var(--text-secondary);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:6px;max-width:260px;display:flex;overflow:hidden}.rev-input{border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--surface-inset);color:var(--text-primary);font:inherit;font-size:var(--fs-md);outline:none;width:220px;padding:6px 10px}.rev-input:focus{border-color:var(--accent)}.icon-btn{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-1);color:var(--text-secondary);cursor:pointer;justify-content:center;align-items:center;gap:6px;padding:6px 8px;display:inline-flex}.icon-btn:hover{border-color:var(--border-strong);color:var(--text-primary)}.body{grid-template-columns:var(--nav-w) 1fr;min-height:0;display:grid}.nav{border-right:1px solid var(--border);background:var(--surface-1);padding:8px;overflow-y:auto}.nav-item{text-align:left;border-radius:var(--radius);width:100%;color:var(--text-secondary);font-size:var(--fs-md);cursor:pointer;background:0 0;border:0;align-items:center;gap:9px;margin-bottom:2px;padding:8px 10px;display:flex}.nav-item:hover{background:var(--surface-inset);color:var(--text-primary)}.nav-item.active{background:var(--accent-bg);color:var(--accent);font-weight:500}.nav-item .count{font-size:var(--fs-2xs);font-variant-numeric:tabular-nums;background:var(--surface-inset);color:var(--text-secondary);border-radius:999px;margin-left:auto;padding:0 7px}.nav-item.active .count{background:var(--accent);color:var(--accent-fg)}.nav-sep{background:var(--border);height:1px;margin:8px 4px}.outlet{min-width:0;min-height:0;display:flex;overflow:hidden}.inbox{grid-template-columns:var(--rail-w) minmax(0, 1fr) var(--case-w);width:100%;min-height:0;display:grid}.inbox.case-collapsed{grid-template-columns:var(--rail-w) minmax(0, 1fr) 0}.rail{border-right:1px solid var(--border);background:var(--surface-1);padding:12px 10px;overflow-y:auto}.rail-h{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin:0 4px 6px}.view-item{text-align:left;width:100%;color:var(--text-secondary);font-size:var(--fs-md);cursor:pointer;border-radius:var(--radius);background:0 0;border:0;align-items:center;gap:7px;margin-bottom:1px;padding:6px 8px;display:flex}.view-item:hover{background:var(--surface-inset);color:var(--text-primary)}.view-item.active{background:var(--accent-bg);color:var(--accent);font-weight:500}.view-item .vc{font-size:var(--fs-2xs);color:var(--text-muted);font-variant-numeric:tabular-nums;margin-left:auto}.facet{margin-top:14px}.facet-opt{text-align:left;width:100%;color:var(--text-secondary);font-size:var(--fs-sm);cursor:pointer;border-radius:var(--radius);background:0 0;border:0;align-items:center;gap:7px;padding:5px 8px;display:flex}.facet-opt:hover{background:var(--surface-inset)}.facet-opt.active{color:var(--accent);font-weight:500}.facet-opt .fc{font-size:var(--fs-2xs);color:var(--text-muted);font-variant-numeric:tabular-nums;margin-left:auto}.facet-opt .tick{border:1px solid var(--border-strong);border-radius:var(--radius-sm);flex:none;width:13px;height:13px}.facet-opt.active .tick{background:var(--accent);border-color:var(--accent)}.queue{flex-direction:column;min-width:0;min-height:0;display:flex}.queue-bar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:8px;padding:10px 12px;display:flex}.fchip{font-size:var(--fs-xs);background:var(--accent-bg);color:var(--accent);cursor:pointer;border:0;border-radius:999px;align-items:center;gap:5px;max-width:240px;padding:3px 9px;display:inline-flex}.fchip .fchip-t{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.fchip.status{background:var(--surface-inset);color:var(--text-secondary)}.queue-count{font-size:var(--fs-xs);color:var(--text-muted)}.seg{border:1px solid var(--border);border-radius:var(--radius);display:inline-flex;overflow:hidden}.seg button{background:var(--surface-1);color:var(--text-secondary);font-size:var(--fs-sm);cursor:pointer;border:0;border-right:1px solid var(--border);padding:5px 12px}.seg button:last-child{border-right:0}.seg button.active{background:var(--accent-bg);color:var(--accent);font-weight:500}.qwrap{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.qscroll{flex:1;min-height:0;position:relative;overflow:auto}.qhead,.qrow{align-items:center;gap:8px;display:grid}.qhead{z-index:2;background:var(--surface-0);border-bottom:1px solid var(--border);height:34px;padding:0 12px;position:sticky;top:0}.qhead .h{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:3px;display:flex}.qhead button.h{text-align:left;background:0 0;border:0;padding:0;font-family:inherit}.qhead button.h svg.i{width:13px;height:13px}.qrow{height:var(--row-h);border-bottom:1px solid var(--border);cursor:pointer;background:var(--surface-1);border-left:3px solid #0000;padding:0 12px}.qrow:hover{background:var(--surface-inset)}.qrow.open{background:var(--accent-bg)}.qrow.cursor{box-shadow:inset 0 0 0 2px var(--accent)}.qrow.lead-danger{border-left-color:var(--danger)}.qrow.lead-warn{border-left-color:var(--warn)}.qrow .action{font-family:var(--font-mono);font-size:var(--fs-sm);color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.qrow .sub{font-size:var(--fs-sm);color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.qrow .wait{font-size:var(--fs-sm);color:var(--text-secondary);font-variant-numeric:tabular-nums}.st{font-size:var(--fs-xs);color:var(--text-secondary);align-items:center;gap:6px;display:inline-flex}.st .dot{background:var(--text-muted);border-radius:50%;flex:none;width:7px;height:7px}.st-pending .dot{background:var(--warn)}.st-granted .dot{background:var(--ok)}.st-denied .dot{background:var(--danger)}.st-cancelled .dot,.st-expired .dot,.st-superseded .dot{background:var(--text-muted)}.st-changes_requested .dot{background:var(--accent)}.sla{font-size:var(--fs-sm);font-variant-numeric:tabular-nums;white-space:nowrap}.sla-overdue{color:var(--danger);font-weight:500}.sla-due{color:var(--warn);font-weight:500}.sla-ok{color:var(--text-muted)}.risk-ring{border:1.5px solid var(--danger);vertical-align:baseline;border-radius:50%;width:8px;height:8px;margin-right:7px;display:inline-block}.cbx{border:1px solid var(--border-strong);border-radius:var(--radius-sm);cursor:pointer;width:15px;height:15px;color:var(--accent-fg);background:var(--surface-1);flex:none;justify-content:center;align-items:center;display:inline-flex}.cbx.on{background:var(--accent);border-color:var(--accent)}.risk{color:var(--danger)}.badge{font-size:var(--fs-2xs);white-space:nowrap;border-radius:999px;padding:1px 8px;font-weight:500;display:inline-block}.badge.pending{background:var(--warn-bg);color:var(--warn)}.badge.granted{background:var(--ok-bg);color:var(--ok)}.badge.denied{background:var(--danger-bg);color:var(--danger)}.badge.cancelled,.badge.expired,.badge.superseded{background:var(--surface-inset);color:var(--text-secondary)}.badge.changes_requested{background:var(--accent-bg);color:var(--accent)}.risk-badge{font-size:var(--fs-2xs);border-radius:var(--radius-sm);vertical-align:1px;text-transform:none;margin-right:7px;padding:0 6px;font-weight:500;display:inline-block}.risk-badge.low{background:var(--surface-inset);color:var(--text-secondary)}.risk-badge.medium{background:var(--warn-bg);color:var(--warn)}.risk-badge.high{background:var(--danger-bg);color:var(--danger)}.risk-badge.critical{background:var(--danger);color:var(--on-danger)}.cbx.off{cursor:not-allowed;opacity:.6;border-style:dashed}.kv-line{font-size:var(--fs-sm);color:var(--text-primary);margin:2px 0 6px;line-height:1.5}.kv-line .k{color:var(--text-muted);margin-right:8px}.policy-block{padding:6px 0 2px}.digest{letter-spacing:.02em;margin-right:6px}.linkish{color:var(--accent);cursor:pointer;font:inherit;background:0 0;border:0;padding:0}.linkish:hover{text-decoration:underline}.diff{font-size:var(--fs-xs);margin:4px 0 0;padding-left:16px}.diff code{font-family:var(--font-mono);font-size:var(--fs-2xs)}.diff-added{color:var(--ok)}.diff-removed{color:var(--danger)}.diff-changed{color:var(--warn)}.badge.overdue{background:var(--danger);color:var(--on-danger);font-variant-numeric:tabular-nums}.badge.due{background:var(--warn-bg);color:var(--warn)}.badge.ok{background:var(--surface-inset);color:var(--text-secondary)}.badge.override{background:var(--accent-bg);color:var(--accent)}.bulkbar{border-top:1px solid var(--border);background:var(--surface-2);flex-wrap:wrap;align-items:center;gap:8px;padding:9px 12px;display:flex}.bulkbar .n{font-size:var(--fs-sm);font-weight:500}.conn-banner{font-size:var(--fs-sm);background:var(--warn-bg);color:var(--warn);border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:7px 12px;display:flex}kbd{font-family:var(--font-mono);font-size:var(--fs-2xs);border:1px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--surface-inset);color:var(--text-secondary);border-bottom-width:2px;padding:1px 5px}.kbd-hint{font-size:var(--fs-xs);color:var(--text-muted);margin-top:6px}.key-hints{border-top:1px solid var(--border);font-size:var(--fs-xs);color:var(--text-muted);background:var(--surface-1);white-space:nowrap;gap:14px;padding:6px 12px;display:flex;overflow:hidden}.key-hints span{align-items:center;gap:5px;display:inline-flex}.kbd-backdrop{z-index:60;background:color-mix(in srgb, var(--text-primary) 22%, transparent);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.kbd-sheet{background:var(--surface-1);border:1px solid var(--border-strong);border-radius:var(--radius);min-width:340px;max-width:560px;padding:16px 18px;box-shadow:0 12px 40px #00000040}.kbd-sheet-h{font-size:var(--fs-lg);justify-content:space-between;align-items:center;gap:16px;margin-bottom:10px;font-weight:600;display:flex}.kbd-groups{flex-wrap:wrap;gap:26px;display:flex}.kbd-group{min-width:150px}.kbd-row{font-size:var(--fs-sm);color:var(--text-secondary);align-items:baseline;gap:10px;padding:3px 0;display:flex}.kbd-row kbd{white-space:nowrap}.newpill{z-index:3;background:var(--accent);color:var(--accent-fg);font-size:var(--fs-sm);cursor:pointer;border:0;border-radius:999px;padding:5px 14px;position:absolute;top:42px;left:50%;transform:translate(-50%);box-shadow:0 1px 4px #0000002e}.btn{border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--surface-1);color:var(--text-primary);cursor:pointer;font-size:var(--fs-md);padding:7px 14px}.btn:hover{border-color:var(--text-muted)}.btn:disabled{opacity:.5;cursor:not-allowed}.btn.approve{background:var(--ok);border-color:var(--ok);color:var(--on-ok)}.btn.reject{background:var(--danger);border-color:var(--danger);color:var(--on-danger)}.btn.small{font-size:var(--fs-xs);padding:4px 10px}.input,textarea.input{border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--surface-1);width:100%;color:var(--text-primary);font:inherit;font-size:var(--fs-md);outline:none;padding:8px 10px}.input:focus{border-color:var(--accent)}.case{border-left:1px solid var(--border);background:var(--surface-1);min-width:0;padding:14px 16px;overflow-y:auto}.case-h{align-items:center;gap:8px;margin-bottom:10px;display:flex}.case-h .mono{font-size:var(--fs-xs);color:var(--text-secondary);overflow-wrap:anywhere}.ctx-hero{font-size:var(--fs-xl);letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere;margin:4px 0 8px;font-weight:600}.ctx-hero .ctx-to{color:var(--text-secondary);font-weight:500}.case-reopen{z-index:4;position:absolute;top:60px;right:8px}.sec-h{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin:14px 0 6px}.kv{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}.kv .tool{font-family:var(--font-mono);font-size:var(--fs-md);background:var(--surface-inset);border-bottom:1px solid var(--border);padding:8px 10px}.kv-row{font-size:var(--fs-sm);border-bottom:1px solid var(--border);justify-content:space-between;gap:12px;padding:6px 10px;display:flex}.kv-row:last-child{border-bottom:0}.kv-row .k{color:var(--text-secondary);overflow-wrap:anywhere;flex:none;max-width:40%}.kv-row .v{font-family:var(--font-mono);color:var(--text-primary);overflow-wrap:anywhere;text-align:right;flex:auto;min-width:0}.raw-toggle{font-size:var(--fs-xs);color:var(--accent);cursor:pointer;background:0;border:0;padding:6px 2px}pre.raw{font-family:var(--font-mono);font-size:var(--fs-xs);background:var(--surface-inset);border-radius:var(--radius);margin:4px 0 0;padding:10px;overflow-x:auto}.reasoning{white-space:pre-wrap;font-size:var(--fs-md);color:var(--text-primary);overflow-wrap:anywhere}.evidence{font-size:var(--fs-sm);color:var(--accent);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;background:var(--surface-1);align-items:center;gap:6px;width:100%;padding:8px 10px;display:flex}.evidence:hover{border-color:var(--border-strong)}.decide-row{flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px;display:flex}.decision-record{font-size:var(--fs-sm);color:var(--text-secondary)}.annotation{border-left:2px solid var(--border-strong);font-size:var(--fs-sm);overflow-wrap:anywhere;margin:6px 0;padding:4px 10px}.annotation b{color:var(--text-primary);font-weight:500}.error{color:var(--danger);font-size:var(--fs-sm);margin-top:8px}.empty{color:var(--text-muted);text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:6px;height:100%;padding:40px 20px;display:flex}.empty .big{color:var(--text-secondary);font-size:var(--fs-lg)}.skel{border-radius:var(--radius-sm);background:var(--surface-inset);height:10px}.skel-row{height:var(--row-h);border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:0 12px;display:grid}.page-h{font-size:var(--fs-xl);letter-spacing:-.015em;margin:0 0 4px;font-weight:600;line-height:1.25}.page-sub{font-size:var(--fs-md);color:var(--text-muted);margin:0 0 18px}#verify-result,#signature-result{font-family:var(--font-mono);font-size:var(--fs-sm);white-space:pre-wrap;margin-top:8px}.ok{color:var(--ok)}.bad{color:var(--danger)}@media (width<=1080px){.inbox{grid-template-columns:var(--rail-w) minmax(0, 1fr) 0}.inbox .case:not(.case-empty){z-index:40;border-left:0;position:fixed;inset:0}}@media (width<=780px){.inbox{grid-template-columns:minmax(0,1fr) 0 0}.inbox .rail{display:none}}.runs{grid-template-columns:minmax(0, 1fr) var(--runs-detail-w);width:100%;min-height:0;display:grid}.runlist{border-right:1px solid var(--border);flex-direction:column;min-width:0;min-height:0;display:flex}.rtl{border-left:1px solid var(--border);background:var(--surface-1);min-width:0;padding:14px 16px;overflow-y:auto}.tl-summary{font-size:var(--fs-sm);color:var(--text-secondary);margin-top:2px}.tl{border-left:2px solid var(--border);margin-top:14px;margin-left:11px;padding-left:18px}.tl-step{padding-bottom:14px;position:relative}.tl-dot{border:2px solid var(--surface-1);border-radius:50%;width:12px;height:12px;position:absolute;top:3px;left:-25px}.tl-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-1);padding:8px 10px}.tl-top{justify-content:space-between;align-items:center;gap:8px;display:flex}.tl-title{font-size:var(--fs-sm);color:var(--text-primary);align-items:center;gap:6px;font-weight:500;display:inline-flex}.tl-meta{font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap}.tl-sub{font-size:var(--fs-xs);color:var(--text-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin-top:4px}@media (width<=1080px){.runs{grid-template-columns:1fr}.rtl{border-left:0;border-top:1px solid var(--border)}}.work{flex-direction:column;width:100%;min-height:0;display:flex}.work-bar{border-bottom:1px solid var(--border);background:var(--surface-1);flex-wrap:wrap;align-items:center;gap:12px;padding:12px 16px;display:flex}.work-title{font-size:var(--fs-lg);font-weight:600}.work-sub{font-size:var(--fs-sm);color:var(--text-muted)}.work-create{border-bottom:1px solid var(--border);background:var(--surface-1);flex-wrap:wrap;align-items:center;gap:8px;padding:12px 16px;display:flex}.work-scroll{flex:1;min-height:0;overflow:auto}.work-foot{border-top:1px solid var(--border);background:var(--surface-1);font-size:var(--fs-xs);color:var(--text-muted);align-items:center;gap:7px;padding:9px 16px;display:flex}.work-foot svg.i{width:14px;height:14px;color:var(--accent)}.mode-sel{font-size:var(--fs-xs);border:1px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--surface-1);color:var(--text-primary);padding:3px 6px;font-family:inherit}.rec-chip{font-size:var(--fs-xs);white-space:nowrap;border-radius:999px;padding:1px 9px;font-weight:500}.rec-chip.promote{background:var(--ok-bg);color:var(--ok)}.rec-chip.demote{background:var(--danger-bg);color:var(--danger)}.rec-chip.hold{background:var(--surface-inset);color:var(--text-secondary)}.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-fg)}.sbar{background:var(--surface-inset);border-radius:999px;height:8px;overflow:hidden}.sbar-fill{background:var(--accent);border-radius:999px;height:100%}.sbar-fill.over{background:var(--danger)}.sbar-cap{font-size:var(--fs-xs);color:var(--text-secondary);margin-top:4px}.sbar-cap .over{color:var(--danger)}.mixcard{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-1);padding:8px 12px}.vault{background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:var(--radius-lg);max-width:900px;color:var(--text-primary);margin-bottom:16px;padding:20px 22px}.vault-top{flex-wrap:wrap;align-items:baseline;gap:12px;margin-bottom:14px;display:flex}.vault-h{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.05em;color:var(--accent);align-items:center;gap:7px;font-weight:500;display:inline-flex}.vault-h svg.i{width:14px;height:14px}.vault-meta{font-size:var(--fs-xs);color:var(--text-secondary)}.vault-err{font-size:var(--fs-sm);color:var(--danger);flex-wrap:wrap;align-items:center;gap:10px;display:flex}.head-hash{font-family:var(--font-mono);color:var(--accent);overflow-wrap:anywhere;flex-wrap:wrap;gap:2px 14px;margin-bottom:16px;font-size:1.35rem;line-height:1.55;display:flex}.chain-strip{font-family:var(--font-mono);font-size:var(--fs-2xs);white-space:nowrap;scrollbar-width:thin;scrollbar-color:var(--border-strong) transparent;align-items:center;padding-bottom:4px;display:flex;overflow-x:auto}.chain-wrap{align-items:center;display:inline-flex}.chain-node{border:1px solid var(--border-strong);color:var(--text-secondary);background:var(--surface-1);border-radius:6px;padding:3px 8px}.chain-node b{color:var(--text-primary);margin-right:5px;font-weight:600}.chain-node.genesis{color:var(--text-secondary);background:0 0;border-style:dashed}.chain-link{color:var(--text-secondary);padding:0 7px}.vault-actions{flex-wrap:wrap;gap:8px;margin-top:16px;display:flex}.btn.vbtn-primary{background:var(--accent);border-color:var(--accent);color:var(--accent-fg);font-weight:500}.prov-row{border-top:1px solid var(--border);margin-top:14px;padding-top:10px}.vchip{font-size:var(--fs-xs);color:var(--accent);background:var(--accent-bg);cursor:pointer;border:0;border-radius:999px;align-items:center;gap:6px;padding:4px 12px 4px 9px;font-family:inherit;font-weight:500;display:inline-flex}.vchip:hover{text-decoration:underline}.vchip svg.i{width:14px;height:14px}.settings{flex:1;min-width:0;padding:20px 24px;overflow-y:auto}.set-card{background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-lg);max-width:900px;margin-bottom:16px;padding:16px 18px}.set-h{font-size:var(--fs-base);margin:0 0 2px;font-weight:600}.set-sub{font-size:var(--fs-sm);color:var(--text-muted);margin:0 0 14px}.set-grid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px 16px;display:grid}.set-stat{font-size:var(--fs-sm);align-items:center;gap:8px;display:flex}.set-k{color:var(--text-muted)}.set-v{color:var(--text-primary);overflow-wrap:anywhere}.set-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.token-secret{font-family:var(--font-mono);font-size:var(--fs-sm);background:var(--warn-bg);color:var(--warn);border:1px solid var(--border);border-radius:var(--radius);word-break:break-all;margin-top:10px;padding:8px 10px}.set-card table{border-collapse:collapse;width:100%;font-size:var(--fs-sm);margin-top:14px}.set-card th{text-align:left;font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);border-bottom:1px solid var(--border);padding:6px 8px;font-weight:400}.set-card td{border-top:1px solid var(--border);vertical-align:middle;padding:8px}.verdict{border-radius:var(--radius);font-size:var(--fs-sm);border:1px solid var(--border);align-items:flex-start;gap:8px;max-width:900px;margin-top:12px;padding:10px 12px;line-height:1.5;display:flex}.verdict.good{background:var(--ok-bg);border-color:var(--ok)}.verdict.bad-v{background:var(--danger-bg);border-color:var(--danger)}.verdict .i{flex:none;margin-top:1px}.verdict .ok,.verdict .bad{overflow-wrap:anywhere}@media (width<=1180px){.inbox,.inbox.case-collapsed{grid-template-columns:minmax(0, 1fr) var(--case-w)}.inbox.case-collapsed{grid-template-columns:minmax(0,1fr) 0}.rail{display:none}.runs{grid-template-columns:minmax(0,1fr) minmax(280px,40%)}}@media (width<=860px){.body{grid-template-rows:auto 1fr;grid-template-columns:1fr}.nav{border-right:0;border-bottom:1px solid var(--border);flex-direction:row;gap:2px;padding:4px 6px;display:flex;overflow:auto hidden}.nav-item{white-space:nowrap;flex:none;width:auto;margin-bottom:0}.nav-sep{width:1px;height:auto;margin:4px 2px}.inbox,.inbox.case-collapsed{grid-template-columns:minmax(0,1fr);position:relative}.case{z-index:5;width:min(100%,440px);position:absolute;inset:0 0 0 auto;box-shadow:-8px 0 24px #0000001f}.inbox.case-collapsed .case{display:none}.runs{grid-template-columns:minmax(0,1fr);position:relative}.rev-input{width:140px}.user{max-width:140px}.topbar{flex-wrap:wrap;gap:8px;height:auto;min-height:52px;padding:6px 10px}.top-right{flex-wrap:wrap;min-width:0}.key-hints{display:none}}@media (width<=640px){.qhead,.qrow,.skel-row{gap:6px;padding-left:8px;padding-right:8px;grid-template-columns:20px 62px minmax(0,1fr) 70px!important}.qhead>:nth-child(4),.qrow>:nth-child(4),.qhead>:nth-child(5),.qrow>:nth-child(5),.skel-row>:nth-child(4),.skel-row>:nth-child(5){display:none}.case{width:100%;box-shadow:none}.queue-bar{gap:6px;padding:8px}.topbar .env,.topbar .tenant{display:none}.rev-input{width:110px}.bulkbar .input{max-width:100%!important}.work-bar,.set-card{padding-left:10px;padding-right:10px}.work-scroll .qhead,.work-scroll .qrow,.runlist .qhead,.runlist .qrow{min-width:640px}.work-scroll,.runlist{overflow-x:auto}}.sim-banner{z-index:1000;font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--warn);background:var(--warn-bg);border:1px solid var(--warn);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:6px 8px 6px 12px;display:flex;position:fixed;bottom:12px;left:50%;transform:translate(-50%)}.sim-banner button{font:inherit;color:inherit;cursor:pointer;border-radius:var(--radius-sm);background:0 0;border:1px solid;padding:1px 8px}
|
app/web-dist/index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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" />
|
|
6
|
+
<title>primarypath · approval console</title>
|
|
7
|
+
<!-- the verified seal in brand cobalt; inline so nothing is fetched -->
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232150c8' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM8.3 12l2.4 2.4 4.9-4.9'/%3E%3C/svg%3E" />
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-B_JDe_G2.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-J7J-YtwM.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!-- stamp the saved theme before first paint so a user whose choice
|
|
14
|
+
differs from the OS scheme never sees a wrong-theme flash -->
|
|
15
|
+
<script>
|
|
16
|
+
try {
|
|
17
|
+
var t = localStorage.getItem("ppTheme");
|
|
18
|
+
if (t === "light" || t === "dark")
|
|
19
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
20
|
+
} catch (e) {}
|
|
21
|
+
</script>
|
|
22
|
+
<div id="root"></div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
app/webhooks.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Webhook delivery (F02): a transactional outbox.
|
|
2
|
+
|
|
3
|
+
The callback row is written INSIDE the transaction that commits the
|
|
4
|
+
decision (a Ledger decision hook), so "decided" and "will be delivered"
|
|
5
|
+
are one atomic fact: a crash after commit, a restart, a replica dying
|
|
6
|
+
mid-POST — none of them lose the callback. A worker on every replica
|
|
7
|
+
leases due rows (`FOR UPDATE SKIP LOCKED`, so two replicas never deliver
|
|
8
|
+
the same row at once), POSTs, and records the outcome: delivered, or
|
|
9
|
+
retried with capped exponential backoff, or — after the configured
|
|
10
|
+
attempts — dead-lettered for an operator to replay from the admin plane.
|
|
11
|
+
|
|
12
|
+
At-least-once, therefore: a delivery whose response was lost is retried
|
|
13
|
+
after its lease expires. Receivers dedupe on `delivery_id` (unique per
|
|
14
|
+
row) or on `approval_id`.
|
|
15
|
+
|
|
16
|
+
Every delivery is signed over the exact bytes sent, so a receiver can
|
|
17
|
+
authenticate the decision independently of transport:
|
|
18
|
+
X-PrimaryPath-Signature t=<ts_ms>,v1=<hex HMAC-SHA256> (shared secret)
|
|
19
|
+
X-PrimaryPath-Signature-Ed25519 key_id=<id>,sig=<b64> (the audit key)
|
|
20
|
+
both over `f"{ts_ms}." + body`; `verify_hmac` / `verify_ed25519` below
|
|
21
|
+
are the reference checks. The Ed25519 key is the export-signing key, so
|
|
22
|
+
the receiver verifies against `GET /api/audit/keys` (or its pinned copy)
|
|
23
|
+
with no shared secret at all.
|
|
24
|
+
|
|
25
|
+
Chat notifications ride the same table (B10): a row whose url is
|
|
26
|
+
`chat:<surface>` is handed to the sender that surface registered instead
|
|
27
|
+
of being POSTed, and gets the same retries, dead-lettering and replay. The
|
|
28
|
+
sender must be registered on every replica (it is, at startup).
|
|
29
|
+
|
|
30
|
+
Transport, not truth: the decision event in the chain is the record;
|
|
31
|
+
this table is operational state and is never exported.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import base64
|
|
37
|
+
import hashlib
|
|
38
|
+
import hmac
|
|
39
|
+
import logging
|
|
40
|
+
import random
|
|
41
|
+
import time
|
|
42
|
+
from collections.abc import Awaitable, Callable
|
|
43
|
+
|
|
44
|
+
import httpx
|
|
45
|
+
from psycopg.types.json import Jsonb
|
|
46
|
+
|
|
47
|
+
from .ledger.hashing import canonical
|
|
48
|
+
|
|
49
|
+
log = logging.getLogger("primarypath.webhooks")
|
|
50
|
+
|
|
51
|
+
DEFAULT_TENANT = "default"
|
|
52
|
+
PENDING, DELIVERED, DEAD = "pending", "delivered", "dead"
|
|
53
|
+
CHAT_SCHEME = "chat:"
|
|
54
|
+
|
|
55
|
+
_COLUMNS = ("id, tenant_id, kind, approval_id, event_hash, url, payload_json, "
|
|
56
|
+
"status, attempts, created_ts_ms, next_attempt_ts_ms, "
|
|
57
|
+
"delivered_ts_ms, last_status, last_error")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _now_ms() -> int:
|
|
61
|
+
return int(time.time() * 1000)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def signed_message(ts_ms: int, body: bytes) -> bytes:
|
|
65
|
+
return f"{ts_ms}.".encode() + body
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def hmac_signature(secret: str, ts_ms: int, body: bytes) -> str:
|
|
69
|
+
return hmac.new(secret.encode("utf-8"), signed_message(ts_ms, body),
|
|
70
|
+
hashlib.sha256).hexdigest()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def verify_hmac(secret: str, header: str, body: bytes,
|
|
74
|
+
max_skew_s: float | None = 300.0,
|
|
75
|
+
now_ms: int | None = None) -> bool:
|
|
76
|
+
"""Reference receiver check for X-PrimaryPath-Signature."""
|
|
77
|
+
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
|
|
78
|
+
try:
|
|
79
|
+
ts = int(parts["t"])
|
|
80
|
+
given = parts["v1"]
|
|
81
|
+
except (KeyError, ValueError):
|
|
82
|
+
return False
|
|
83
|
+
if max_skew_s is not None:
|
|
84
|
+
now = now_ms if now_ms is not None else _now_ms()
|
|
85
|
+
if abs(now - ts) > max_skew_s * 1000:
|
|
86
|
+
return False
|
|
87
|
+
return hmac.compare_digest(hmac_signature(secret, ts, body), given)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def verify_ed25519(public_key_b64: str, header: str, ts_ms: int,
|
|
91
|
+
body: bytes) -> bool:
|
|
92
|
+
"""Reference receiver check for X-PrimaryPath-Signature-Ed25519 against
|
|
93
|
+
a key from GET /api/audit/keys (match `key_id`)."""
|
|
94
|
+
from cryptography.exceptions import InvalidSignature
|
|
95
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
96
|
+
|
|
97
|
+
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
|
|
98
|
+
try:
|
|
99
|
+
sig = base64.b64decode(parts["sig"])
|
|
100
|
+
key = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
101
|
+
base64.b64decode(public_key_b64))
|
|
102
|
+
key.verify(sig, signed_message(ts_ms, body))
|
|
103
|
+
return True
|
|
104
|
+
except (KeyError, ValueError, InvalidSignature):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class WebhookOutbox:
|
|
109
|
+
def __init__(self, pool, *, attempts: int = 10,
|
|
110
|
+
backoff_base: float = 0.5, backoff_max: float = 300.0,
|
|
111
|
+
poll_interval_s: float = 1.0, lease_s: float = 30.0,
|
|
112
|
+
secret: str | None = None, signer=None, metrics=None,
|
|
113
|
+
timeout_s: float = 10.0):
|
|
114
|
+
self.pool = pool
|
|
115
|
+
self.attempts = attempts
|
|
116
|
+
self.backoff_base = backoff_base
|
|
117
|
+
self.backoff_max = backoff_max
|
|
118
|
+
self.poll_interval_s = poll_interval_s
|
|
119
|
+
self.lease_s = lease_s
|
|
120
|
+
self.secret = secret
|
|
121
|
+
self.signer = signer
|
|
122
|
+
self.metrics = metrics
|
|
123
|
+
self.timeout_s = timeout_s
|
|
124
|
+
self._wake = asyncio.Event()
|
|
125
|
+
self._task: asyncio.Task | None = None
|
|
126
|
+
self._inflight = 0
|
|
127
|
+
# chat:<surface> -> async sender(payload); raising means "retry"
|
|
128
|
+
self.senders: dict[str, Callable[[dict], Awaitable[None]]] = {}
|
|
129
|
+
|
|
130
|
+
def register_sender(self, url: str,
|
|
131
|
+
send: Callable[[dict], Awaitable[None]]) -> None:
|
|
132
|
+
if not url.startswith(CHAT_SCHEME):
|
|
133
|
+
raise ValueError(f"a sender url must start with {CHAT_SCHEME!r}")
|
|
134
|
+
self.senders[url] = send
|
|
135
|
+
|
|
136
|
+
# -- enqueue -------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
async def enqueue_in_tx(self, conn, url: str, payload: dict, *,
|
|
139
|
+
tenant: str = DEFAULT_TENANT,
|
|
140
|
+
kind: str = "decision",
|
|
141
|
+
approval_id: str | None = None,
|
|
142
|
+
event_hash: str | None = None) -> int:
|
|
143
|
+
"""Write the row in the caller's transaction: it commits — or rolls
|
|
144
|
+
back — with whatever the caller is committing."""
|
|
145
|
+
now = _now_ms()
|
|
146
|
+
cur = await conn.execute(
|
|
147
|
+
"INSERT INTO webhook_outbox (tenant_id, kind, approval_id, "
|
|
148
|
+
"event_hash, url, payload_json, created_ts_ms, next_attempt_ts_ms) "
|
|
149
|
+
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id",
|
|
150
|
+
(tenant, kind, approval_id, event_hash, url, Jsonb(payload),
|
|
151
|
+
now, now))
|
|
152
|
+
row = await cur.fetchone()
|
|
153
|
+
self._nudge()
|
|
154
|
+
return row[0]
|
|
155
|
+
|
|
156
|
+
async def dispatch(self, url: str, payload: dict, *,
|
|
157
|
+
tenant: str = DEFAULT_TENANT,
|
|
158
|
+
kind: str = "notification",
|
|
159
|
+
approval_id: str | None = None,
|
|
160
|
+
event_hash: str | None = None) -> int:
|
|
161
|
+
"""Enqueue in its own transaction (for callers with no transaction of
|
|
162
|
+
their own, e.g. the SLA escalation webhook)."""
|
|
163
|
+
async with self.pool.connection() as conn:
|
|
164
|
+
async with conn.transaction():
|
|
165
|
+
return await self.enqueue_in_tx(
|
|
166
|
+
conn, url, payload, tenant=tenant, kind=kind,
|
|
167
|
+
approval_id=approval_id, event_hash=event_hash)
|
|
168
|
+
|
|
169
|
+
async def on_decision(self, conn, approval: dict, event: dict) -> None:
|
|
170
|
+
"""Ledger decision hook — runs inside the decision's transaction, for
|
|
171
|
+
every decision surface (web, chat, SLA auto-deny, policy/cost)."""
|
|
172
|
+
url = approval.get("callback_url")
|
|
173
|
+
if not url:
|
|
174
|
+
return
|
|
175
|
+
payload = {
|
|
176
|
+
"approval_id": approval["approval_id"],
|
|
177
|
+
"status": approval["status"],
|
|
178
|
+
"reviewer": approval["reviewer"],
|
|
179
|
+
"reason": approval["reason"],
|
|
180
|
+
"decided_ts_ms": approval["decided_ts_ms"],
|
|
181
|
+
}
|
|
182
|
+
if approval.get("grant"):
|
|
183
|
+
# the callback is the agent plane: it carries the execution
|
|
184
|
+
# grant the executor will verify and redeem
|
|
185
|
+
payload["grant"] = approval["grant"]
|
|
186
|
+
await self.enqueue_in_tx(
|
|
187
|
+
conn, url, payload,
|
|
188
|
+
tenant=event.get("tenant_id", DEFAULT_TENANT), kind="decision",
|
|
189
|
+
approval_id=approval["approval_id"], event_hash=event["hash"])
|
|
190
|
+
|
|
191
|
+
def _nudge(self) -> None:
|
|
192
|
+
# the row is not visible until the caller commits; a short delay
|
|
193
|
+
# lets the commit land, and the poll interval catches the rest
|
|
194
|
+
try:
|
|
195
|
+
loop = asyncio.get_running_loop()
|
|
196
|
+
except RuntimeError:
|
|
197
|
+
return
|
|
198
|
+
loop.call_later(0.05, self._wake.set)
|
|
199
|
+
|
|
200
|
+
# -- worker --------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def start(self) -> None:
|
|
203
|
+
if self.poll_interval_s <= 0 or self._task is not None:
|
|
204
|
+
return
|
|
205
|
+
self._task = asyncio.get_running_loop().create_task(self._run())
|
|
206
|
+
|
|
207
|
+
async def stop(self) -> None:
|
|
208
|
+
if self._task is None:
|
|
209
|
+
return
|
|
210
|
+
self._task.cancel()
|
|
211
|
+
await asyncio.gather(self._task, return_exceptions=True)
|
|
212
|
+
self._task = None
|
|
213
|
+
|
|
214
|
+
async def _run(self) -> None:
|
|
215
|
+
while True:
|
|
216
|
+
self._wake.clear()
|
|
217
|
+
try:
|
|
218
|
+
while await self.deliver_due() > 0:
|
|
219
|
+
pass
|
|
220
|
+
except asyncio.CancelledError:
|
|
221
|
+
raise
|
|
222
|
+
except Exception:
|
|
223
|
+
log.exception("webhook outbox pass failed")
|
|
224
|
+
try:
|
|
225
|
+
await asyncio.wait_for(self._wake.wait(),
|
|
226
|
+
timeout=self.poll_interval_s)
|
|
227
|
+
except TimeoutError:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
async def deliver_due(self, limit: int = 50) -> int:
|
|
231
|
+
"""One pass: lease every due row (skipping rows another replica
|
|
232
|
+
holds), deliver them concurrently, record outcomes. Returns the
|
|
233
|
+
number of rows attempted."""
|
|
234
|
+
now = _now_ms()
|
|
235
|
+
async with self.pool.connection() as conn:
|
|
236
|
+
async with conn.transaction():
|
|
237
|
+
cur = await conn.execute(
|
|
238
|
+
"UPDATE webhook_outbox SET attempts = attempts + 1, "
|
|
239
|
+
"next_attempt_ts_ms = %s WHERE id IN ("
|
|
240
|
+
"SELECT id FROM webhook_outbox WHERE status = 'pending' "
|
|
241
|
+
"AND next_attempt_ts_ms <= %s "
|
|
242
|
+
"ORDER BY next_attempt_ts_ms, id LIMIT %s "
|
|
243
|
+
"FOR UPDATE SKIP LOCKED) "
|
|
244
|
+
"RETURNING id, tenant_id, kind, approval_id, event_hash, "
|
|
245
|
+
"url, payload_json, attempts",
|
|
246
|
+
(now + int(self.lease_s * 1000), now, limit))
|
|
247
|
+
rows = await cur.fetchall()
|
|
248
|
+
if not rows:
|
|
249
|
+
return 0
|
|
250
|
+
self._inflight += len(rows)
|
|
251
|
+
try:
|
|
252
|
+
async with httpx.AsyncClient(timeout=self.timeout_s) as client:
|
|
253
|
+
await asyncio.gather(*(self._attempt(client, r) for r in rows))
|
|
254
|
+
finally:
|
|
255
|
+
self._inflight -= len(rows)
|
|
256
|
+
return len(rows)
|
|
257
|
+
|
|
258
|
+
def headers_for(self, body: bytes, ts_ms: int, delivery_id: int,
|
|
259
|
+
event_hash: str | None) -> dict[str, str]:
|
|
260
|
+
headers = {
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
"User-Agent": "primarypath-webhook",
|
|
263
|
+
"X-PrimaryPath-Delivery": str(delivery_id),
|
|
264
|
+
"X-PrimaryPath-Timestamp": str(ts_ms),
|
|
265
|
+
}
|
|
266
|
+
if event_hash:
|
|
267
|
+
headers["X-PrimaryPath-Event"] = event_hash
|
|
268
|
+
if self.secret:
|
|
269
|
+
headers["X-PrimaryPath-Signature"] = (
|
|
270
|
+
f"t={ts_ms},v1={hmac_signature(self.secret, ts_ms, body)}")
|
|
271
|
+
if self.signer is not None:
|
|
272
|
+
sig = self.signer.backend.sign(signed_message(ts_ms, body))
|
|
273
|
+
headers["X-PrimaryPath-Signature-Ed25519"] = (
|
|
274
|
+
f"key_id={self.signer.key_id},"
|
|
275
|
+
f"sig={base64.b64encode(sig).decode()}")
|
|
276
|
+
return headers
|
|
277
|
+
|
|
278
|
+
async def _attempt(self, client: httpx.AsyncClient, row: tuple) -> None:
|
|
279
|
+
(delivery_id, tenant, kind, approval_id, event_hash, url, payload,
|
|
280
|
+
attempts) = row
|
|
281
|
+
ts = _now_ms()
|
|
282
|
+
body = canonical({**payload, "delivery_id": delivery_id,
|
|
283
|
+
"event_id": event_hash, "kind": kind,
|
|
284
|
+
"ts_ms": ts, "attempt": attempts}).encode("utf-8")
|
|
285
|
+
status: int | None = None
|
|
286
|
+
error: str | None = None
|
|
287
|
+
if url.startswith(CHAT_SCHEME):
|
|
288
|
+
send = self.senders.get(url)
|
|
289
|
+
try:
|
|
290
|
+
if send is None:
|
|
291
|
+
raise LookupError(f"no sender registered for {url}")
|
|
292
|
+
await send(payload)
|
|
293
|
+
ok = True
|
|
294
|
+
except Exception as e:
|
|
295
|
+
ok = False
|
|
296
|
+
error = f"{type(e).__name__}: {e}"[:500]
|
|
297
|
+
else:
|
|
298
|
+
try:
|
|
299
|
+
r = await client.post(
|
|
300
|
+
url, content=body,
|
|
301
|
+
headers=self.headers_for(body, ts, delivery_id,
|
|
302
|
+
event_hash))
|
|
303
|
+
status = r.status_code
|
|
304
|
+
ok = status < 300
|
|
305
|
+
if not ok:
|
|
306
|
+
error = f"HTTP {status}"
|
|
307
|
+
except httpx.HTTPError as e:
|
|
308
|
+
ok = False
|
|
309
|
+
error = f"{type(e).__name__}: {e}"[:500]
|
|
310
|
+
|
|
311
|
+
now = _now_ms()
|
|
312
|
+
async with self.pool.connection() as conn:
|
|
313
|
+
if ok:
|
|
314
|
+
await conn.execute(
|
|
315
|
+
"UPDATE webhook_outbox SET status = 'delivered', "
|
|
316
|
+
"delivered_ts_ms = %s, last_status = %s, last_error = NULL "
|
|
317
|
+
"WHERE id = %s", (now, status, delivery_id))
|
|
318
|
+
log.info("delivered %s #%d to %s (attempt %d)",
|
|
319
|
+
approval_id, delivery_id, url, attempts)
|
|
320
|
+
self._count("delivered")
|
|
321
|
+
elif attempts >= self.attempts:
|
|
322
|
+
await conn.execute(
|
|
323
|
+
"UPDATE webhook_outbox SET status = 'dead', "
|
|
324
|
+
"last_status = %s, last_error = %s WHERE id = %s",
|
|
325
|
+
(status, error, delivery_id))
|
|
326
|
+
log.error("webhook #%d to %s dead after %d attempts (%s) "
|
|
327
|
+
"for %s — replay from /api/admin/webhooks",
|
|
328
|
+
delivery_id, url, attempts, error, approval_id)
|
|
329
|
+
self._count("failed")
|
|
330
|
+
else:
|
|
331
|
+
delay = min(self.backoff_base * (2 ** (attempts - 1)),
|
|
332
|
+
self.backoff_max)
|
|
333
|
+
delay *= 1 + random.random() * 0.1 # de-synchronize retries
|
|
334
|
+
await conn.execute(
|
|
335
|
+
"UPDATE webhook_outbox SET next_attempt_ts_ms = %s, "
|
|
336
|
+
"last_status = %s, last_error = %s WHERE id = %s",
|
|
337
|
+
(now + int(delay * 1000), status, error, delivery_id))
|
|
338
|
+
log.warning("webhook #%d to %s failed (%s), attempt %d/%d, "
|
|
339
|
+
"retry in %.1fs", delivery_id, url, error,
|
|
340
|
+
attempts, self.attempts, delay)
|
|
341
|
+
|
|
342
|
+
def _count(self, result: str) -> None:
|
|
343
|
+
if self.metrics is not None:
|
|
344
|
+
self.metrics.webhook_deliveries.labels(result=result).inc()
|
|
345
|
+
|
|
346
|
+
# -- admin ---------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
async def list(self, tenant: str = DEFAULT_TENANT,
|
|
349
|
+
status: str | None = None, limit: int = 100) -> list[dict]:
|
|
350
|
+
q = f"SELECT {_COLUMNS} FROM webhook_outbox WHERE tenant_id = %s"
|
|
351
|
+
params: list = [tenant]
|
|
352
|
+
if status is not None:
|
|
353
|
+
q += " AND status = %s"
|
|
354
|
+
params.append(status)
|
|
355
|
+
q += " ORDER BY id DESC LIMIT %s"
|
|
356
|
+
params.append(limit)
|
|
357
|
+
async with self.pool.connection() as conn:
|
|
358
|
+
cur = await conn.execute(q, params)
|
|
359
|
+
rows = await cur.fetchall()
|
|
360
|
+
keys = [k.strip() for k in _COLUMNS.split(",")]
|
|
361
|
+
out = []
|
|
362
|
+
for r in rows:
|
|
363
|
+
d = dict(zip(keys, r))
|
|
364
|
+
d["payload"] = d.pop("payload_json")
|
|
365
|
+
out.append(d)
|
|
366
|
+
return out
|
|
367
|
+
|
|
368
|
+
async def replay(self, delivery_id: int,
|
|
369
|
+
tenant: str = DEFAULT_TENANT) -> bool:
|
|
370
|
+
"""Re-queue a dead (or already delivered) row for delivery. Not a
|
|
371
|
+
pending one: that is the worker's."""
|
|
372
|
+
async with self.pool.connection() as conn:
|
|
373
|
+
cur = await conn.execute(
|
|
374
|
+
"UPDATE webhook_outbox SET status = 'pending', attempts = 0, "
|
|
375
|
+
"next_attempt_ts_ms = %s, last_error = NULL "
|
|
376
|
+
"WHERE id = %s AND tenant_id = %s AND status <> 'pending'",
|
|
377
|
+
(_now_ms(), delivery_id, tenant))
|
|
378
|
+
ok = cur.rowcount > 0
|
|
379
|
+
if ok:
|
|
380
|
+
self._nudge()
|
|
381
|
+
return ok
|
|
382
|
+
|
|
383
|
+
async def pending_count(self) -> int:
|
|
384
|
+
async with self.pool.connection() as conn:
|
|
385
|
+
cur = await conn.execute(
|
|
386
|
+
"SELECT count(*) FROM webhook_outbox WHERE status = 'pending'")
|
|
387
|
+
return (await cur.fetchone())[0]
|
|
388
|
+
|
|
389
|
+
async def drain(self, timeout: float = 30.0) -> None:
|
|
390
|
+
"""Deliver until nothing is pending (tests). Honors backoff."""
|
|
391
|
+
deadline = time.monotonic() + timeout
|
|
392
|
+
while True:
|
|
393
|
+
if await self.deliver_due():
|
|
394
|
+
continue
|
|
395
|
+
async with self.pool.connection() as conn:
|
|
396
|
+
cur = await conn.execute(
|
|
397
|
+
"SELECT min(next_attempt_ts_ms) FROM webhook_outbox "
|
|
398
|
+
"WHERE status = 'pending'")
|
|
399
|
+
nxt = (await cur.fetchone())[0]
|
|
400
|
+
if nxt is None and self._inflight == 0:
|
|
401
|
+
return
|
|
402
|
+
if time.monotonic() > deadline:
|
|
403
|
+
raise TimeoutError("webhook outbox did not drain")
|
|
404
|
+
wait = max(0.0, (nxt - _now_ms()) / 1000) if nxt else 0.05
|
|
405
|
+
await asyncio.sleep(min(max(wait, 0.02), 0.25))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: primarypath-server
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Primary Path: self-hosted approval inbox + tamper-evident audit trail for AI agents
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi>=0.115
|
|
9
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
10
|
+
Requires-Dist: psycopg[binary,pool]>=3.2
|
|
11
|
+
Requires-Dist: alembic>=1.13
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
13
|
+
Requires-Dist: pydantic>=2.7
|
|
14
|
+
Requires-Dist: pydantic-settings>=2.3
|
|
15
|
+
Requires-Dist: cryptography<51,>=48.0.1
|
|
16
|
+
Requires-Dist: anyio>=4.14.2
|
|
17
|
+
Requires-Dist: httpx<1,>=0.27
|
|
18
|
+
Requires-Dist: pyjwt>=2.8
|
|
19
|
+
Requires-Dist: prometheus-client>=0.20
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
26
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27; extra == "dev"
|
|
28
|
+
Requires-Dist: asgi-lifespan>=2.1; extra == "dev"
|
|
29
|
+
Requires-Dist: hypothesis>=6.100; extra == "dev"
|
|
30
|
+
Requires-Dist: mcp<2,>=1.23; extra == "dev"
|
|
31
|
+
Requires-Dist: openai-agents<1,>=0.22; extra == "dev"
|
|
32
|
+
Requires-Dist: langgraph<2,>=1; extra == "dev"
|
|
33
|
+
Requires-Dist: claude-agent-sdk<1,>=0.2; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
app/__init__.py,sha256=4QYITl9KmAzt8MVLNEZTxphYxSXQNHndRAWTy16uLwk,14681
|
|
2
|
+
app/_version.py,sha256=Cg6GRQlxTaokTj49Obpsi2uuvSVLrFA-e3lQ3IsfOUU,346
|
|
3
|
+
app/agents.py,sha256=lH3S5xaSRv_N58XbrQr1ZKmZbflRniizEHUcHSU17Dw,6941
|
|
4
|
+
app/alembic.ini,sha256=S3gNNl4QLmhZE55_RmrH2MoyJzCpmUK1ZNY436Ok7dc,606
|
|
5
|
+
app/auth.py,sha256=Vn_7s_WLsPnlYZ4phayeVxVNP_2cV08mq7u3Hy982PI,30328
|
|
6
|
+
app/chat.py,sha256=WJ3U2Gl2lJzMYyQkaYrtkZVIxwORdQGjBiFdssI_Z5Y,5563
|
|
7
|
+
app/config.py,sha256=yKnEkoLohC2eqD2BWmHc9Kp8nlxmOE9OQXXyYfSlYQk,11550
|
|
8
|
+
app/costgov.py,sha256=m-rf_sDgLmsOtMlHjTCJtIT0lbGcWYsna7z_TuDL3qM,15948
|
|
9
|
+
app/db.py,sha256=fPu5QRxOLtJ4_BGP-PfjPRTXxLnv6hNeeajAEDtd7Rg,1754
|
|
10
|
+
app/faults.py,sha256=XhGabak0xF_qdzYMY4te87fLFCrM0be5I4Ilj_uBhsk,1136
|
|
11
|
+
app/grants.py,sha256=JsKAsUKHA3kU3F_1-C3S_-R7YHH3bsWkgLvBVap-lxs,5362
|
|
12
|
+
app/idempotency.py,sha256=pnFtcrEnZjlFffPxeTxMKjCM4SmVl-EDbMAmsW-nKio,2035
|
|
13
|
+
app/jcs.py,sha256=PTIgOkSfk3xyP39TFl6F9GqF4lnb44QRCEIIkUZxI18,4414
|
|
14
|
+
app/main.py,sha256=5TLke615A5nmvkEc5WsFZsWzWzKmeSrvvKe32rxLDR8,1925
|
|
15
|
+
app/maintenance.py,sha256=yq1UW_ISQqw6nUH9a2UWhLE8OjhG3oZ_FVXDz2mUNxA,2128
|
|
16
|
+
app/metrics.py,sha256=BKIFjwItnwL3jBT-8hBqlhR0K9zo8rngtz7Y8jkVi7w,3720
|
|
17
|
+
app/netguard.py,sha256=TvhAZmmwdHAV5aVVK5n7ubNxRy6o8mHT8YYgS0qFZdg,2760
|
|
18
|
+
app/obslog.py,sha256=8bYkSIHZZFfb-f0tiMQ_z298mqmO5WnTRPc3Yz3T-K4,3489
|
|
19
|
+
app/policy.py,sha256=chAfN88QUKA1jxygNRoOt35tnwF7scs2Pq8PNrhzvTE,19134
|
|
20
|
+
app/ratelimit.py,sha256=lXFh4unrY9cqXr3wyIPHAgWyoylitwWWHoVKeRWc9Jk,2847
|
|
21
|
+
app/schemas.py,sha256=bDaVM_LU63cAuo27rUYMyE907H2dndoqVxf6rVoD0rk,10340
|
|
22
|
+
app/security.py,sha256=6pJac6iEhMrF4iVJneHGNrIpiZ4R1LWyRKdJzO8hU4s,8193
|
|
23
|
+
app/siem.py,sha256=FPSOaegXKQgSfOIzXumSaN7NSm0Ob6HedPErBtpAlVg,9547
|
|
24
|
+
app/signing.py,sha256=3cfjf5tJN7ZLyNZXLbQvGw3pElbgmYt1R3MHViiGN0E,13269
|
|
25
|
+
app/sla.py,sha256=H-pZ7dDYbAFmRoKmg8zTjD3qu6LxYBjYbuhnWIjbhuA,11402
|
|
26
|
+
app/slack.py,sha256=M6rbWNdpYViNhEO2e4XGa9aGHewoScxLf6BWk7Nclhk,12514
|
|
27
|
+
app/teams.py,sha256=HTokZNkAQZPPCgd7n-IWQcXF1OnWK0VF4BlZmbFNizM,11715
|
|
28
|
+
app/tokens.py,sha256=1drfaPqsvdI1aKt1oVSfmznXzEXGXD3WkRfo8ytPxJM,5515
|
|
29
|
+
app/traces.py,sha256=TtT5p7r7vm0p4AkPxrIadzgynFiu6Zh8cVp211thRh4,13904
|
|
30
|
+
app/webhooks.py,sha256=GvWSeo9XeARnrczJ0gc91Mzv5K1HBp1d97aH67ECO0w,17419
|
|
31
|
+
app/ledger/__init__.py,sha256=1hDqoZVEEud1zTFBMhYykvl65SmuDvF-VIBS3IfXIzI,864
|
|
32
|
+
app/ledger/fold.py,sha256=gPpyBUYcMAVaFbvPbqHKQhd5jlB611DgRN2BYhKbbr8,3180
|
|
33
|
+
app/ledger/hashing.py,sha256=qz_kTfv3cbohfyEWiRJ8RMr7zkEUQ1cXBWQwVpb9_L4,576
|
|
34
|
+
app/ledger/projection.py,sha256=FSzqJDmpbpF-59GZlKsXTE17HY9n0P1wlDeAFJu5c_k,17249
|
|
35
|
+
app/ledger/store.py,sha256=9YjJUVe48TRSrhMtextxOck7xCpUsPT3ljOPgreskEY,53524
|
|
36
|
+
app/ledger/verify.py,sha256=Z2GUrsPRNZlPPfzX1DgnjbzgoicD4OSia1d7-WOVZBQ,1287
|
|
37
|
+
app/migrations/env.py,sha256=iFfbmtdfLn7wyMyXQ4c9-qxUZFA1FxlBRVIdl7_HFoo,558
|
|
38
|
+
app/migrations/script.py.mako,sha256=i5nh9loqUb_EUYiPhJyR_SmPBELOud1uICJnMbK0dhU,476
|
|
39
|
+
app/migrations/versions/0001_baseline.py,sha256=OtGhWHCvFwCxptLa_C49RhaFB4ArAJXaYDdjdy-qof8,250
|
|
40
|
+
app/migrations/versions/0002_events_and_projection.py,sha256=1RsWnSEIlsYQTBb-pw5LB4rwrqk_MEvik5bvoYI5Fjk,2692
|
|
41
|
+
app/migrations/versions/0003_slack_messages.py,sha256=BD_UxEON6XjxzZjbtT-Fqe7LZe7TCbh89o7I02uKDII,539
|
|
42
|
+
app/migrations/versions/0004_sessions.py,sha256=rj67Ap1JRorG8eJy-3y9YndWhJFumv3Xi5h9DKdzoKQ,680
|
|
43
|
+
app/migrations/versions/0005_siem_cursor.py,sha256=yoSdCd_B8Xh2vapB_J6vjUmXnRF8KT0gn06tJxU9pDc,511
|
|
44
|
+
app/migrations/versions/0006_teams_messages.py,sha256=1yu131R_LiS628XXppxKeOrHduDNzeczU6ekQc0kOyM,551
|
|
45
|
+
app/migrations/versions/0007_tenant_scoping.py,sha256=Pp-6Vnz5LICIwHWJgnzmP8AHRYgIVL5FKn9QeZvoi_s,1849
|
|
46
|
+
app/migrations/versions/0008_policy_view.py,sha256=tJlyWUsdD2fxwHx8FBPs-Yzh5riCeyXGNcKfuKxGdfE,1203
|
|
47
|
+
app/migrations/versions/0009_api_tokens.py,sha256=7Y169CtVBH0YZHzB9PFEQOXOxuK4IgmbpzgfJ9pxkIc,1073
|
|
48
|
+
app/migrations/versions/0010_session_groups.py,sha256=GMIrKmJupqIAoEbW0AYby99LYqV3rKpW6GEBaySRlJY,534
|
|
49
|
+
app/migrations/versions/0011_budgets_and_evals.py,sha256=9oiWLFTsVQnuxihTgMEsv5DRwItbIl5AeQ2ewpbUD6U,2750
|
|
50
|
+
app/migrations/versions/0012_run_event_ts_index.py,sha256=EEKB1Y1FUtOE6Gz4IGwNGQhyF7NxDeVcZv3sljlUY6Q,546
|
|
51
|
+
app/migrations/versions/0013_agent_chains.py,sha256=vJ8j_YrUUs7a7VjcPbuA22ij-MT5UX_uw2jR5om2XJU,3895
|
|
52
|
+
app/migrations/versions/0014_dual_control.py,sha256=Z5JcCnvXNNksyFGhgiqM74gbiibd9nbNnM3KgH0Nf9U,1208
|
|
53
|
+
app/migrations/versions/0015_sampled_mode.py,sha256=_VIvIeDP-WQVawAl3k6SjDA_8EOf7T3S4isjAlD0bJI,618
|
|
54
|
+
app/migrations/versions/0016_sampling_salt.py,sha256=C5E_mToJghFaD6kZ2ed3HYM1Fo_Z6Sh0p1p8kWoAH5s,674
|
|
55
|
+
app/migrations/versions/0017_live_tokens_index.py,sha256=hNYOsK0U9FMCbcQg-4QU6KwipN1Fpxh5aSmo9Q7b_88,713
|
|
56
|
+
app/migrations/versions/0018_webhook_outbox.py,sha256=ToEXH58-Rg-u_BD6D_G6Svnu_hH6yDTPfEZUVbtgsHM,1591
|
|
57
|
+
app/migrations/versions/0019_usage_view_and_rollout_index.py,sha256=_3rsPRnRJMmwvajvdwgrMJCbz1aB2soo2z1REDpN2fk,2759
|
|
58
|
+
app/migrations/versions/0020_policy_metadata_and_lifecycle.py,sha256=3Y-rZRgU2qbCy08faiA_dwKi9OJ5K1O90-QD0VA6AOY,2799
|
|
59
|
+
app/migrations/versions/0021_execution_grants.py,sha256=6PrrAOC-7blq9kkfei0rBVa9NKhenXAdG0cwOSJyWXE,2286
|
|
60
|
+
app/migrations/versions/0022_digest_alg_and_executor_instance.py,sha256=7y_FgKXeL3bkXonI0z4waUDO0LztPiyBEDLzPRFRnlI,1668
|
|
61
|
+
app/migrations/versions/0023_idempotency_keys.py,sha256=IlA_bOOQurK920Fx23zAFLZ6LQ0GIZGWJ3qqI_ZsN1M,1113
|
|
62
|
+
app/migrations/versions/0024_approvals_view_keyset_index.py,sha256=bIxCstzJlMA8jdtJyE45WTw-KPi7dhCi0tG6h-Sko-I,725
|
|
63
|
+
app/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
+
app/routes/admin.py,sha256=CGrf5VkDnBkLp6Fbsdc4H3yTS2JJnY_-Y4132sK8zMs,13450
|
|
65
|
+
app/routes/agents.py,sha256=Szr_6sd6HmxE0tCUwZ7p7P2oMwCcWNK6bxYIfO9MJ8s,4567
|
|
66
|
+
app/routes/approvals.py,sha256=DFfzCf4b2jRfoptWWG8bLX8xD2XZ5kOdw4aNh4VJox0,27708
|
|
67
|
+
app/routes/audit.py,sha256=2__A_DKbMxVPbndp83VWQBkAPs27LlZsJSqtY13V9Mw,8815
|
|
68
|
+
app/routes/costgov.py,sha256=vI3ilwabWlCs3q2Yx9ys-Ebz44E5s5Gam3BKya5Gsos,6567
|
|
69
|
+
app/routes/health.py,sha256=yNx7TntZtWkIn8VhUtfPQ7kZDHf-ZQYn86MbE8XHscE,1861
|
|
70
|
+
app/routes/policy.py,sha256=z6XWlwknBHT7U7rXAC31nkE1UfFSD-aRndW61dM4RRY,3213
|
|
71
|
+
app/routes/runs.py,sha256=ckvhmQ2QvgW-Djc1-OHWeUiFJ4OV8PRVi6AmVUtRA7I,4974
|
|
72
|
+
app/routes/slack.py,sha256=3jhB8gBNRNohiJ261unLNfkTD0l6SAM4M6AI0vQDn80,1302
|
|
73
|
+
app/routes/teams.py,sha256=zhB2ll8gJ4fWi_l_HGgrlcUjw9wM_5MV1KujzTScq-g,879
|
|
74
|
+
app/routes/traces.py,sha256=83T4ilVtS2zcVx6BqWpFtwey2IaPYUr1KtysLpCiEbk,2318
|
|
75
|
+
app/web-dist/index.html,sha256=NPbDDOIpoGcD8pPtqNxqXp0LhMNlAMcLVPuCaK2mAow,1150
|
|
76
|
+
app/web-dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2,sha256=Ixf6S7KTycCxEOGDFdUpI1xHoN3TM4zqPYx5VekniZ4,7420
|
|
77
|
+
app/web-dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2,sha256=aJRDlpSUalidFX7OADCGlgpqQBPXSoE9q3YC79s9jAk,15084
|
|
78
|
+
app/web-dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2,sha256=gk9IW10m4vLaPCsjYTLs4byOTkM3NFKVC7DkBUi0MT8,16512
|
|
79
|
+
app/web-dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2,sha256=GfnJJUaqMAwxIjXjElrxuBOU2NuaS8SkJc1bZB0tVOE,29400
|
|
80
|
+
app/web-dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2,sha256=vRucqs6SBBnkCAXRGOtd80BlHM9g8ySmwPyWkYTFqaw,6180
|
|
81
|
+
app/web-dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2,sha256=hW40HQFyOSAf-j7aeh6XaNzRRd2aboLiTYcGjEnE9Yc,12876
|
|
82
|
+
app/web-dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2,sha256=Y-J9uhpbqnAPHieVk8Jeps_iTS3_W63S7LoXijWkm7k,14784
|
|
83
|
+
app/web-dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2,sha256=r2G5aef5mZafavV25YTuhdyjAaAIp2vhJR0XLVa5kEw,29896
|
|
84
|
+
app/web-dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2,sha256=Wq8X4B1EGZFHB0mzOpKlSS9shpf25-DxEpiyPzZSN0o,5804
|
|
85
|
+
app/web-dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2,sha256=vi2ZdZAhbK8ekcIUqQzvfPmGOjE0ydlmbuU9P8OYJHQ,7716
|
|
86
|
+
app/web-dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2,sha256=j6QOXSSCR3NeuXoL1ZO4hSRAQwYA1roBNkwx_gq8H-E,8004
|
|
87
|
+
app/web-dist/assets/index-B_JDe_G2.js,sha256=fFFDOFrnze6lJKMcDvgtpL_b52ZIaumZnnnXARz_lSQ,261684
|
|
88
|
+
app/web-dist/assets/index-J7J-YtwM.css,sha256=Va9mWZBoFVIsxgeRhUzPq8sYBJiVG5F0AHU96QZ44w8,30986
|
|
89
|
+
primarypath_server-1.0.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
90
|
+
primarypath_server-1.0.0.dist-info/METADATA,sha256=HPOedzkb7Zzp4wM0wP8U6kkfrywoguMNxMzE_CIt5ik,1339
|
|
91
|
+
primarypath_server-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
92
|
+
primarypath_server-1.0.0.dist-info/entry_points.txt,sha256=xdp2HF6GHEVnU6mBEuVdIl37nACSLOk2i66JmOE-PeI,95
|
|
93
|
+
primarypath_server-1.0.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
|
94
|
+
primarypath_server-1.0.0.dist-info/RECORD,,
|