@obsunified/collector 2.2.1 → 2.3.0

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 (53) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.js +1 -1
  3. package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
  4. package/dist/chunk-2FALTDCE.js.map +1 -0
  5. package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
  6. package/dist/chunk-RASSTGNB.js.map +1 -0
  7. package/dist/index.d.ts +10 -2
  8. package/dist/index.js +2892 -2240
  9. package/dist/index.js.map +1 -1
  10. package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -1
  11. package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
  12. package/package.json +4 -3
  13. package/src/__tests__/autosre-closed-loop.test.ts +700 -0
  14. package/src/__tests__/db-parity.integration.test.ts +961 -0
  15. package/src/__tests__/internal-contracts-routes.test.ts +451 -0
  16. package/src/auth/dashboard-auth.test.ts +61 -0
  17. package/src/auth/dashboard-auth.ts +17 -2
  18. package/src/framework/env.ts +105 -0
  19. package/src/index.ts +13 -4
  20. package/src/lib/ai-store/calls.ts +43 -23
  21. package/src/lib/ai-store/evaluations.test.ts +3 -3
  22. package/src/lib/ai-store.ts +12 -1
  23. package/src/lib/analyses-store.test.ts +6 -6
  24. package/src/lib/autosre-findings.ts +169 -14
  25. package/src/lib/autosre-remediation-gate.ts +156 -26
  26. package/src/lib/db-parity.ts +883 -0
  27. package/src/lib/eval-cases-store.test.ts +1 -1
  28. package/src/lib/eval-cases-store.ts +16 -4
  29. package/src/lib/evidence-references.ts +30 -22
  30. package/src/lib/projects-store.test.ts +38 -1
  31. package/src/lib/projects-store.ts +23 -4
  32. package/src/lib/sql-db-postgres.integration.test.ts +75 -2
  33. package/src/lib/stage6.test.ts +1 -1
  34. package/src/lib/usage-store.ts +9 -3
  35. package/src/migrations/052_remediation_callback.sql +10 -0
  36. package/src/migrations/053_remediation_callback_delivery.sql +7 -0
  37. package/src/migrations/parity.test.ts +105 -3
  38. package/src/migrations-postgres/052_remediation_callback.sql +10 -0
  39. package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
  40. package/src/plugins/ai-call-by-id.test.ts +73 -0
  41. package/src/plugins/ai-receiver.ts +15 -0
  42. package/src/plugins/autosre-routes.ts +409 -147
  43. package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
  44. package/src/plugins/connected-routes/manifest.test.ts +224 -0
  45. package/src/plugins/connected-routes/manifest.ts +131 -28
  46. package/src/plugins/connected-routes-links.test.ts +96 -0
  47. package/src/plugins/connected-routes.test.ts +13 -11
  48. package/src/plugins/connected-routes.ts +53 -20
  49. package/src/plugins/eval-cases-routes.ts +33 -10
  50. package/src/plugins/evidence-retrieval-routes.ts +89 -24
  51. package/dist/chunk-KMEZKGSO.js.map +0 -1
  52. package/dist/chunk-ZSF5XP3B.js.map +0 -1
  53. /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CausalTriple,
3
3
  EvidenceEntityKind,
4
+ GrantCallbackDelivery,
4
5
  IncidentInvestigation,
5
6
  IncidentStatus,
6
7
  RemediationActionKind,
@@ -538,8 +539,15 @@ export async function dispatchIncidentWebhook(
538
539
  fetchImpl?: typeof fetch;
539
540
  logger?: Logger;
540
541
  grant?: { proposalId: string; status: string; grantToken: string };
542
+ /** Test hook: replaces the real backoff sleep between grant attempts. */
543
+ sleep?: (ms: number) => Promise<void>;
541
544
  },
542
- ): Promise<{ delivered: boolean; status?: number; error?: string }> {
545
+ ): Promise<{
546
+ delivered: boolean;
547
+ status?: number;
548
+ error?: string;
549
+ attempts: number;
550
+ }> {
543
551
  const logger = webhook.logger ?? consoleLogger;
544
552
  const incident = await getIncidentInvestigation(
545
553
  db,
@@ -548,10 +556,14 @@ export async function dispatchIncidentWebhook(
548
556
  webhook.environment,
549
557
  );
550
558
  if (!incident) {
551
- return { delivered: false, error: "Incident not found" };
559
+ return { delivered: false, error: "Incident not found", attempts: 0 };
552
560
  }
553
561
  if (!webhook.grant && incident.dispatchAttempts >= 5) {
554
- return { delivered: false, error: "MAX_DISPATCH_ATTEMPTS (5) exceeded" };
562
+ return {
563
+ delivered: false,
564
+ error: "MAX_DISPATCH_ATTEMPTS (5) exceeded",
565
+ attempts: 0,
566
+ };
555
567
  }
556
568
 
557
569
  let safeUrl: URL;
@@ -581,7 +593,7 @@ export async function dispatchIncidentWebhook(
581
593
  incidentId,
582
594
  error,
583
595
  });
584
- return { delivered: false, error };
596
+ return { delivered: false, error, attempts: 0 };
585
597
  }
586
598
 
587
599
  const nowIso = new Date().toISOString();
@@ -620,40 +632,59 @@ export async function dispatchIncidentWebhook(
620
632
  }
621
633
 
622
634
  const fetchFn = webhook.fetchImpl ?? fetch;
635
+ const sleep =
636
+ webhook.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)));
637
+ // Grant handoffs retry on network error, timeout and 5xx (never 4xx or a
638
+ // redirect); each attempt keeps the manual-redirect and 5 s timeout rules.
639
+ const maxAttempts = webhook.grant ? GRANT_DELIVERY_BACKOFF_MS.length + 1 : 1;
623
640
  let delivered = false;
624
641
  let status: number | undefined;
625
642
  let errorMsg: string | undefined;
643
+ let attempts = 0;
626
644
 
627
- try {
628
- const res = await fetchFn(safeUrl.toString(), {
629
- method: "POST",
630
- headers,
631
- body,
632
- // Never follow redirects: a 3xx could re-aim the POST (and its
633
- // signed body, which may carry a grant token) at another host.
634
- redirect: "manual",
635
- signal: AbortSignal.timeout(5000),
636
- });
637
- status = res.status;
638
- if (res.status >= 300 && res.status < 400) {
639
- errorMsg = `Redirect (HTTP ${res.status}) refused`;
640
- } else {
641
- delivered = res.ok;
642
- if (!res.ok) errorMsg = `HTTP ${res.status}`;
645
+ while (attempts < maxAttempts) {
646
+ attempts++;
647
+ let retryable = false;
648
+ status = undefined;
649
+ errorMsg = undefined;
650
+ try {
651
+ const res = await fetchFn(safeUrl.toString(), {
652
+ method: "POST",
653
+ headers,
654
+ body,
655
+ // Never follow redirects: a 3xx could re-aim the POST (and its
656
+ // signed body, which may carry a grant token) at another host.
657
+ redirect: "manual",
658
+ signal: AbortSignal.timeout(5000),
659
+ });
660
+ status = res.status;
661
+ if (res.status >= 300 && res.status < 400) {
662
+ errorMsg = `Redirect (HTTP ${res.status}) refused`;
663
+ } else {
664
+ delivered = res.ok;
665
+ if (!res.ok) {
666
+ errorMsg = `HTTP ${res.status}`;
667
+ retryable = res.status >= 500;
668
+ }
669
+ }
670
+ } catch (err) {
671
+ errorMsg = err instanceof Error ? err.message : String(err);
672
+ retryable = true;
643
673
  }
644
- } catch (err) {
645
- errorMsg = err instanceof Error ? err.message : String(err);
674
+ if (delivered || !retryable || attempts >= maxAttempts) break;
675
+ await sleep(GRANT_DELIVERY_BACKOFF_MS[attempts - 1]);
646
676
  }
647
677
 
648
678
  if (!delivered) {
649
679
  logger.warn("[autosre] incident webhook delivery failed", {
650
680
  incidentId,
651
681
  error: errorMsg,
682
+ attempts,
652
683
  });
653
684
  }
654
685
 
655
686
  if (webhook.grant) {
656
- return { delivered, status, error: errorMsg };
687
+ return { delivered, status, error: errorMsg, attempts };
657
688
  }
658
689
 
659
690
  await db
@@ -675,7 +706,37 @@ export async function dispatchIncidentWebhook(
675
706
  )
676
707
  .run();
677
708
 
678
- return { delivered, status, error: errorMsg };
709
+ return { delivered, status, error: errorMsg, attempts };
710
+ }
711
+
712
+ /** Backoff before attempt 2 and 3 of a grant webhook delivery. */
713
+ const GRANT_DELIVERY_BACKOFF_MS = [500, 1500];
714
+
715
+ /**
716
+ * Persists the last grant-webhook delivery outcome on the proposal. The JSON
717
+ * holds only status/attempts/error/time, never the URL or secret.
718
+ */
719
+ export async function recordCallbackDelivery(
720
+ db: SqlDb,
721
+ projectId: string,
722
+ environment: string,
723
+ proposalId: string,
724
+ outcome: { delivered: boolean; attempts: number; error?: string },
725
+ ): Promise<GrantCallbackDelivery> {
726
+ const record: GrantCallbackDelivery = {
727
+ status: outcome.delivered ? "delivered" : "failed",
728
+ attempts: outcome.attempts,
729
+ lastError: outcome.delivered ? null : (outcome.error ?? null),
730
+ at: new Date().toISOString(),
731
+ };
732
+ await db
733
+ .prepare(
734
+ `UPDATE remediation_proposals SET callback_delivery = ?
735
+ WHERE project_id = ? AND environment = ? AND id = ?`,
736
+ )
737
+ .bind(JSON.stringify(record), projectId, environment, proposalId)
738
+ .run();
739
+ return record;
679
740
  }
680
741
 
681
742
  interface ProposalRow {
@@ -703,11 +764,42 @@ interface ProposalRow {
703
764
  remediation_agent_run_id: string | null;
704
765
  resulting_change_id: string | null;
705
766
  failure_reason: string | null;
767
+ callback_url?: string | null;
768
+ callback_secret?: string | null;
769
+ callback_delivery?: string | null;
706
770
  created_at: string;
707
771
  updated_at: string;
708
772
  expires_at: string | null;
709
773
  }
710
774
 
775
+ /** Host (never path, query or credentials) of a stored grant callback URL. */
776
+ function callbackHostOf(url: string | null | undefined): string | null {
777
+ if (!url) return null;
778
+ try {
779
+ return new URL(url).host || null;
780
+ } catch {
781
+ return null;
782
+ }
783
+ }
784
+
785
+ function parseCallbackDelivery(
786
+ raw: string | null | undefined,
787
+ ): GrantCallbackDelivery | null {
788
+ if (!raw) return null;
789
+ try {
790
+ const v = JSON.parse(raw) as Partial<GrantCallbackDelivery>;
791
+ if (v.status !== "delivered" && v.status !== "failed") return null;
792
+ return {
793
+ status: v.status,
794
+ attempts: Number(v.attempts ?? 0),
795
+ lastError: typeof v.lastError === "string" ? v.lastError : null,
796
+ at: String(v.at ?? ""),
797
+ };
798
+ } catch {
799
+ return null;
800
+ }
801
+ }
802
+
711
803
  export const mapProposalRow = (
712
804
  row: ProposalRow,
713
805
  grantToken?: string,
@@ -739,6 +831,8 @@ export const mapProposalRow = (
739
831
  remediationAgentRunId: row.remediation_agent_run_id,
740
832
  resultingChangeId: row.resulting_change_id,
741
833
  failureReason: row.failure_reason,
834
+ callbackHost: callbackHostOf(row.callback_url),
835
+ callbackDelivery: parseCallbackDelivery(row.callback_delivery),
742
836
  createdAt: row.created_at,
743
837
  updatedAt: row.updated_at,
744
838
  expiresAt: row.expires_at,
@@ -847,7 +941,11 @@ export async function proposeRemediationAction(
847
941
  db: SqlDb,
848
942
  projectId: string,
849
943
  input: RemediationProposalInput,
850
- options: { logger?: Logger } = {},
944
+ options: {
945
+ logger?: Logger;
946
+ /** Persisted on a pending proposal and used when it is approved later. */
947
+ callback?: { url: string; secret?: string };
948
+ } = {},
851
949
  ): Promise<RemediationProposal> {
852
950
  const logger = options.logger ?? consoleLogger;
853
951
  const nowIso = new Date().toISOString();
@@ -977,6 +1075,12 @@ export async function proposeRemediationAction(
977
1075
  status = "rejected";
978
1076
  }
979
1077
 
1078
+ // Only a proposal that still awaits a human can use a stored callback; an
1079
+ // autonomous grant is delivered inline by the propose request itself.
1080
+ const storedCallback =
1081
+ status === "pending_human_approval" && options.callback?.url
1082
+ ? options.callback
1083
+ : null;
980
1084
  const id = `rem_${randomHex(10)}`;
981
1085
  const expiresAt = new Date(Date.now() + 30 * 24 * 3600_000).toISOString();
982
1086
 
@@ -997,8 +1101,9 @@ export async function proposeRemediationAction(
997
1101
  expected_current_state_json, desired_target_state_json,
998
1102
  requested_autonomy, effective_autonomy, policy_decision_reason,
999
1103
  status, grant_token_hash, grant_expires_at,
1104
+ callback_url, callback_secret,
1000
1105
  created_at, updated_at, expires_at
1001
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1002
1107
  ON CONFLICT DO NOTHING`,
1003
1108
  )
1004
1109
  .bind(
@@ -1018,6 +1123,8 @@ export async function proposeRemediationAction(
1018
1123
  status,
1019
1124
  grantTokenHash,
1020
1125
  grantExpiresAt,
1126
+ storedCallback?.url ?? null,
1127
+ storedCallback?.secret ?? null,
1021
1128
  nowIso,
1022
1129
  nowIso,
1023
1130
  expiresAt,
@@ -1123,6 +1230,25 @@ export async function listRemediationProposals(
1123
1230
  return rows.results.map((r) => mapProposalRow(r));
1124
1231
  }
1125
1232
 
1233
+ /**
1234
+ * Reads the grant callback persisted at propose time. Server-side use only:
1235
+ * the secret is never mapped into API responses. Call before
1236
+ * `approveRemediationProposal`, which clears the stored callback.
1237
+ */
1238
+ export async function getRemediationCallback(
1239
+ db: SqlDb,
1240
+ projectId: string,
1241
+ proposalId: string,
1242
+ environment?: string,
1243
+ ): Promise<{ url: string; secret?: string } | null> {
1244
+ const row = await loadProposalRow(db, projectId, proposalId, environment);
1245
+ if (!row?.callback_url) return null;
1246
+ return {
1247
+ url: row.callback_url,
1248
+ ...(row.callback_secret ? { secret: row.callback_secret } : {}),
1249
+ };
1250
+ }
1251
+
1126
1252
  /**
1127
1253
  * Human dashboard session approval for `pending_human_approval` proposals
1128
1254
  * (RFC 0013 § 6.B & P1 #4). Mints a single-use `grantToken`.
@@ -1169,6 +1295,8 @@ export async function approveRemediationProposal(
1169
1295
  grant_expires_at = ?,
1170
1296
  approved_by_user_id = ?,
1171
1297
  approved_at = ?,
1298
+ callback_url = NULL,
1299
+ callback_secret = NULL,
1172
1300
  updated_at = ?
1173
1301
  WHERE project_id = ? AND environment = ? AND id = ?
1174
1302
  AND status = 'pending_human_approval'`,
@@ -1241,6 +1369,8 @@ export async function rejectRemediationProposal(
1241
1369
  `UPDATE remediation_proposals
1242
1370
  SET status = 'rejected',
1243
1371
  grant_token_hash = NULL,
1372
+ callback_url = NULL,
1373
+ callback_secret = NULL,
1244
1374
  failure_reason = ?,
1245
1375
  updated_at = ?
1246
1376
  WHERE project_id = ? AND environment = ? AND id = ?