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.
Files changed (94) hide show
  1. app/__init__.py +351 -0
  2. app/_version.py +8 -0
  3. app/agents.py +150 -0
  4. app/alembic.ini +37 -0
  5. app/auth.py +714 -0
  6. app/chat.py +122 -0
  7. app/config.py +258 -0
  8. app/costgov.py +345 -0
  9. app/db.py +56 -0
  10. app/faults.py +26 -0
  11. app/grants.py +122 -0
  12. app/idempotency.py +58 -0
  13. app/jcs.py +117 -0
  14. app/ledger/__init__.py +41 -0
  15. app/ledger/fold.py +74 -0
  16. app/ledger/hashing.py +19 -0
  17. app/ledger/projection.py +389 -0
  18. app/ledger/store.py +1042 -0
  19. app/ledger/verify.py +31 -0
  20. app/main.py +50 -0
  21. app/maintenance.py +56 -0
  22. app/metrics.py +89 -0
  23. app/migrations/env.py +20 -0
  24. app/migrations/script.py.mako +23 -0
  25. app/migrations/versions/0001_baseline.py +19 -0
  26. app/migrations/versions/0002_events_and_projection.py +78 -0
  27. app/migrations/versions/0003_slack_messages.py +27 -0
  28. app/migrations/versions/0004_sessions.py +31 -0
  29. app/migrations/versions/0005_siem_cursor.py +26 -0
  30. app/migrations/versions/0006_teams_messages.py +27 -0
  31. app/migrations/versions/0007_tenant_scoping.py +50 -0
  32. app/migrations/versions/0008_policy_view.py +39 -0
  33. app/migrations/versions/0009_api_tokens.py +37 -0
  34. app/migrations/versions/0010_session_groups.py +23 -0
  35. app/migrations/versions/0011_budgets_and_evals.py +75 -0
  36. app/migrations/versions/0012_run_event_ts_index.py +23 -0
  37. app/migrations/versions/0013_agent_chains.py +102 -0
  38. app/migrations/versions/0014_dual_control.py +33 -0
  39. app/migrations/versions/0015_sampled_mode.py +24 -0
  40. app/migrations/versions/0016_sampling_salt.py +25 -0
  41. app/migrations/versions/0017_live_tokens_index.py +25 -0
  42. app/migrations/versions/0018_webhook_outbox.py +47 -0
  43. app/migrations/versions/0019_usage_view_and_rollout_index.py +68 -0
  44. app/migrations/versions/0020_policy_metadata_and_lifecycle.py +67 -0
  45. app/migrations/versions/0021_execution_grants.py +59 -0
  46. app/migrations/versions/0022_digest_alg_and_executor_instance.py +45 -0
  47. app/migrations/versions/0023_idempotency_keys.py +38 -0
  48. app/migrations/versions/0024_approvals_view_keyset_index.py +26 -0
  49. app/netguard.py +73 -0
  50. app/obslog.py +105 -0
  51. app/policy.py +390 -0
  52. app/ratelimit.py +78 -0
  53. app/routes/__init__.py +0 -0
  54. app/routes/admin.py +312 -0
  55. app/routes/agents.py +111 -0
  56. app/routes/approvals.py +576 -0
  57. app/routes/audit.py +213 -0
  58. app/routes/costgov.py +150 -0
  59. app/routes/health.py +53 -0
  60. app/routes/policy.py +72 -0
  61. app/routes/runs.py +115 -0
  62. app/routes/slack.py +40 -0
  63. app/routes/teams.py +29 -0
  64. app/routes/traces.py +55 -0
  65. app/schemas.py +248 -0
  66. app/security.py +203 -0
  67. app/siem.py +248 -0
  68. app/signing.py +342 -0
  69. app/sla.py +234 -0
  70. app/slack.py +282 -0
  71. app/teams.py +276 -0
  72. app/tokens.py +117 -0
  73. app/traces.py +309 -0
  74. app/web-dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  75. app/web-dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  76. app/web-dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  77. app/web-dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  78. app/web-dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
  79. app/web-dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
  80. app/web-dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
  81. app/web-dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
  82. app/web-dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
  83. app/web-dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
  84. app/web-dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  85. app/web-dist/assets/index-B_JDe_G2.js +10 -0
  86. app/web-dist/assets/index-J7J-YtwM.css +1 -0
  87. app/web-dist/index.html +24 -0
  88. app/webhooks.py +405 -0
  89. primarypath_server-1.0.0.dist-info/METADATA +35 -0
  90. primarypath_server-1.0.0.dist-info/RECORD +94 -0
  91. primarypath_server-1.0.0.dist-info/WHEEL +5 -0
  92. primarypath_server-1.0.0.dist-info/entry_points.txt +3 -0
  93. primarypath_server-1.0.0.dist-info/licenses/LICENSE +202 -0
  94. 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}
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ primarypath-migrate = app.db:migrate_main
3
+ primarypath-server = app.main:main