@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.
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
- package/dist/chunk-RASSTGNB.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2892 -2240
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -1
- package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
- package/package.json +4 -3
- package/src/__tests__/autosre-closed-loop.test.ts +700 -0
- package/src/__tests__/db-parity.integration.test.ts +961 -0
- package/src/__tests__/internal-contracts-routes.test.ts +451 -0
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/framework/env.ts +105 -0
- package/src/index.ts +13 -4
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store/evaluations.test.ts +3 -3
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/analyses-store.test.ts +6 -6
- package/src/lib/autosre-findings.ts +169 -14
- package/src/lib/autosre-remediation-gate.ts +156 -26
- package/src/lib/db-parity.ts +883 -0
- package/src/lib/eval-cases-store.test.ts +1 -1
- package/src/lib/eval-cases-store.ts +16 -4
- package/src/lib/evidence-references.ts +30 -22
- package/src/lib/projects-store.test.ts +38 -1
- package/src/lib/projects-store.ts +23 -4
- package/src/lib/sql-db-postgres.integration.test.ts +75 -2
- package/src/lib/stage6.test.ts +1 -1
- package/src/lib/usage-store.ts +9 -3
- package/src/migrations/052_remediation_callback.sql +10 -0
- package/src/migrations/053_remediation_callback_delivery.sql +7 -0
- package/src/migrations/parity.test.ts +105 -3
- package/src/migrations-postgres/052_remediation_callback.sql +10 -0
- package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
- package/src/plugins/ai-call-by-id.test.ts +73 -0
- package/src/plugins/ai-receiver.ts +15 -0
- package/src/plugins/autosre-routes.ts +409 -147
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +224 -0
- package/src/plugins/connected-routes/manifest.ts +131 -28
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +13 -11
- package/src/plugins/connected-routes.ts +53 -20
- package/src/plugins/eval-cases-routes.ts +33 -10
- package/src/plugins/evidence-retrieval-routes.ts +89 -24
- package/dist/chunk-KMEZKGSO.js.map +0 -1
- package/dist/chunk-ZSF5XP3B.js.map +0 -1
- /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<{
|
|
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 {
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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: {
|
|
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 = ?
|