@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,9 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
CausalProofTier,
|
|
3
2
|
CausalVerificationStatus,
|
|
4
3
|
ChangeEventKind,
|
|
5
|
-
CohortAttributionVerdict,
|
|
6
4
|
EvidenceEntityKind,
|
|
5
|
+
GrantCallbackDelivery,
|
|
6
|
+
GrantDeliveryResult,
|
|
7
|
+
InternalEndpointKey,
|
|
8
|
+
InternalEndpoints,
|
|
7
9
|
InvestigationFindingInput,
|
|
8
10
|
JsonValue,
|
|
9
11
|
RemediationActionKind,
|
|
@@ -12,11 +14,16 @@ import type {
|
|
|
12
14
|
RemediationStateSpec,
|
|
13
15
|
} from "@obsunified/types";
|
|
14
16
|
import {
|
|
17
|
+
ChangeEventInputSchema,
|
|
15
18
|
ChangeEventKindSchema,
|
|
16
19
|
EvidencePredicateSchema,
|
|
20
|
+
InvestigationFindingHttpInputSchema,
|
|
17
21
|
RemediationActionKindSchema,
|
|
22
|
+
RemediationProposalInputSchema,
|
|
23
|
+
validateContractResponse,
|
|
18
24
|
} from "@obsunified/types";
|
|
19
25
|
import { AgentAutonomyLevel } from "@obsunified/types/constants";
|
|
26
|
+
import type { Context } from "hono";
|
|
20
27
|
import type { CollectorPlugin } from "../framework/collector";
|
|
21
28
|
import type { CollectorEnv } from "../framework/env";
|
|
22
29
|
import {
|
|
@@ -46,19 +53,37 @@ import {
|
|
|
46
53
|
evaluateRemediationWatchdogs,
|
|
47
54
|
failRemediationProposal,
|
|
48
55
|
getIncidentInvestigation,
|
|
56
|
+
getRemediationCallback,
|
|
49
57
|
getRemediationProposal,
|
|
50
58
|
listIncidentInvestigations,
|
|
51
59
|
listRemediationProposals,
|
|
52
60
|
openOrDeduplicateIncident,
|
|
53
61
|
proposeRemediationAction,
|
|
54
62
|
RemediationGateError,
|
|
63
|
+
recordCallbackDelivery,
|
|
55
64
|
rejectRemediationProposal,
|
|
56
65
|
} from "../lib/autosre-remediation-gate";
|
|
57
66
|
import { sha256Hex } from "../lib/hash";
|
|
58
67
|
import { sqlDbFor } from "../lib/sql-db";
|
|
68
|
+
import { assertSafeWebhookUrl } from "../lib/webhook-url";
|
|
59
69
|
import { getProjectId } from "./_context";
|
|
60
70
|
import { readJsonObject } from "./_json";
|
|
61
71
|
|
|
72
|
+
export function respondWithContract<
|
|
73
|
+
K extends InternalEndpointKey,
|
|
74
|
+
C extends {
|
|
75
|
+
json: (data: unknown, status?: 200 | 201 | 202) => Response;
|
|
76
|
+
},
|
|
77
|
+
>(
|
|
78
|
+
c: C,
|
|
79
|
+
endpoint: K,
|
|
80
|
+
payload: InternalEndpoints[K]["response"],
|
|
81
|
+
status: 200 | 201 | 202 = 200,
|
|
82
|
+
): Response {
|
|
83
|
+
validateContractResponse(endpoint, payload, status);
|
|
84
|
+
return c.json(payload, status);
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
const CHANGE_KINDS = new Set<string>(ChangeEventKindSchema.options);
|
|
63
88
|
|
|
64
89
|
const REMEDIATION_ACTION_KINDS = new Set<string>(
|
|
@@ -126,6 +151,12 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
126
151
|
* `webhookUrl` (+ optional `secret`) in that same request and the token is
|
|
127
152
|
* pushed to the runner over the SSRF-guarded, HMAC-signed incident webhook.
|
|
128
153
|
* Without a `webhookUrl` the token is simply returned in the response.
|
|
154
|
+
*
|
|
155
|
+
* A `webhookUrl` (+ `secret`) given to `propose` for a proposal that is
|
|
156
|
+
* parked for human approval is persisted on the proposal and used when
|
|
157
|
+
* the approve request carries none. A callback on the approve request
|
|
158
|
+
* wins over the stored one. The stored callback is cleared on approve
|
|
159
|
+
* or reject, and only its host is ever returned by GET.
|
|
129
160
|
*/
|
|
130
161
|
const deliverGrant = async (
|
|
131
162
|
env: CollectorEnv | undefined,
|
|
@@ -139,23 +170,87 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
139
170
|
status: string;
|
|
140
171
|
grantToken?: string;
|
|
141
172
|
},
|
|
173
|
+
/** Callback persisted at propose time; used when the request has none. */
|
|
174
|
+
stored?: { url: string; secret?: string } | null,
|
|
142
175
|
) => {
|
|
143
|
-
const
|
|
176
|
+
const bodyUrl =
|
|
144
177
|
typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
178
|
+
const target = bodyUrl
|
|
179
|
+
? {
|
|
180
|
+
url: bodyUrl,
|
|
181
|
+
secret: typeof body.secret === "string" ? body.secret : undefined,
|
|
182
|
+
}
|
|
183
|
+
: (stored ?? null);
|
|
184
|
+
const notAttempted: GrantDeliveryResult = {
|
|
185
|
+
attempted: false,
|
|
186
|
+
delivered: false,
|
|
187
|
+
attempts: 0,
|
|
188
|
+
host: null,
|
|
189
|
+
};
|
|
190
|
+
if (!target || !proposal.grantToken) {
|
|
191
|
+
return { webhookDelivery: undefined, grantDelivery: notAttempted };
|
|
192
|
+
}
|
|
193
|
+
if (proposal.status !== "grant_issued") {
|
|
194
|
+
return { webhookDelivery: undefined, grantDelivery: notAttempted };
|
|
195
|
+
}
|
|
196
|
+
let host: string | null = null;
|
|
197
|
+
try {
|
|
198
|
+
host = new URL(target.url).host || null;
|
|
199
|
+
} catch {
|
|
200
|
+
host = null;
|
|
201
|
+
}
|
|
202
|
+
const webhookDelivery = await dispatchIncidentWebhook(
|
|
203
|
+
db,
|
|
204
|
+
projectId,
|
|
205
|
+
proposal.incidentId,
|
|
206
|
+
{
|
|
207
|
+
url: target.url,
|
|
208
|
+
secret: target.secret,
|
|
209
|
+
environment: proposal.environment,
|
|
210
|
+
allowPrivateNetworks: env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
211
|
+
logger: runtime.logger,
|
|
212
|
+
grant: {
|
|
213
|
+
proposalId: proposal.id,
|
|
214
|
+
status: proposal.status,
|
|
215
|
+
grantToken: proposal.grantToken,
|
|
216
|
+
},
|
|
157
217
|
},
|
|
158
|
-
|
|
218
|
+
);
|
|
219
|
+
// Error text is surfaced to the human: strip anything URL-shaped and
|
|
220
|
+
// the secret so neither the query string nor the HMAC key can leak.
|
|
221
|
+
let error = webhookDelivery.error
|
|
222
|
+
? webhookDelivery.error.replace(
|
|
223
|
+
/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi,
|
|
224
|
+
"<url>",
|
|
225
|
+
)
|
|
226
|
+
: undefined;
|
|
227
|
+
if (error && target.secret) {
|
|
228
|
+
error = error.split(target.secret).join("<redacted>");
|
|
229
|
+
}
|
|
230
|
+
const record = await recordCallbackDelivery(
|
|
231
|
+
db,
|
|
232
|
+
projectId,
|
|
233
|
+
proposal.environment,
|
|
234
|
+
proposal.id,
|
|
235
|
+
{
|
|
236
|
+
delivered: webhookDelivery.delivered,
|
|
237
|
+
attempts: webhookDelivery.attempts,
|
|
238
|
+
error,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
(
|
|
242
|
+
proposal as { callbackDelivery?: GrantCallbackDelivery | null }
|
|
243
|
+
).callbackDelivery = record;
|
|
244
|
+
const grantDelivery: GrantDeliveryResult = {
|
|
245
|
+
attempted: true,
|
|
246
|
+
delivered: webhookDelivery.delivered,
|
|
247
|
+
attempts: webhookDelivery.attempts,
|
|
248
|
+
host,
|
|
249
|
+
...(webhookDelivery.delivered
|
|
250
|
+
? {}
|
|
251
|
+
: { error: error ?? "unknown error" }),
|
|
252
|
+
};
|
|
253
|
+
return { webhookDelivery, grantDelivery };
|
|
159
254
|
};
|
|
160
255
|
|
|
161
256
|
// 1. Change Ingestion (Ingest Auth /v1/changes)
|
|
@@ -225,52 +320,63 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
225
320
|
? body.actor
|
|
226
321
|
: null;
|
|
227
322
|
|
|
323
|
+
const rawInput = {
|
|
324
|
+
id: typeof body.id === "string" ? body.id : undefined,
|
|
325
|
+
environment:
|
|
326
|
+
typeof body.environment === "string" ? body.environment : undefined,
|
|
327
|
+
kind: normalizedKind,
|
|
328
|
+
source:
|
|
329
|
+
body.source === "inferred_telemetry" ||
|
|
330
|
+
body.source === "autosre_remediation" ||
|
|
331
|
+
body.source === "ci_webhook"
|
|
332
|
+
? body.source
|
|
333
|
+
: ("explicit_api" as const),
|
|
334
|
+
targetNodeId:
|
|
335
|
+
typeof body.targetNodeId === "string" ? body.targetNodeId : undefined,
|
|
336
|
+
serviceName,
|
|
337
|
+
namespace,
|
|
338
|
+
version: typeof body.version === "string" ? body.version : null,
|
|
339
|
+
previousVersion:
|
|
340
|
+
typeof body.previousVersion === "string"
|
|
341
|
+
? body.previousVersion
|
|
342
|
+
: null,
|
|
343
|
+
commitSha: typeof body.commitSha === "string" ? body.commitSha : null,
|
|
344
|
+
previousCommitSha:
|
|
345
|
+
typeof body.previousCommitSha === "string"
|
|
346
|
+
? body.previousCommitSha
|
|
347
|
+
: null,
|
|
348
|
+
configDigest:
|
|
349
|
+
typeof body.configDigest === "string" ? body.configDigest : null,
|
|
350
|
+
summary,
|
|
351
|
+
actorId,
|
|
352
|
+
repositoryUrl,
|
|
353
|
+
pullRequestUrl,
|
|
354
|
+
causedByActionId:
|
|
355
|
+
typeof body.causedByActionId === "string"
|
|
356
|
+
? body.causedByActionId
|
|
357
|
+
: null,
|
|
358
|
+
agentRunId:
|
|
359
|
+
typeof body.agentRunId === "string" ? body.agentRunId : null,
|
|
360
|
+
touchedPaths,
|
|
361
|
+
attributes,
|
|
362
|
+
occurredAt:
|
|
363
|
+
typeof body.occurredAt === "string" ? body.occurredAt : undefined,
|
|
364
|
+
};
|
|
365
|
+
const parsedInput = ChangeEventInputSchema.safeParse(rawInput);
|
|
366
|
+
if (!parsedInput.success) {
|
|
367
|
+
return c.json(
|
|
368
|
+
{
|
|
369
|
+
error: parsedInput.error.issues
|
|
370
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
371
|
+
.join("; "),
|
|
372
|
+
},
|
|
373
|
+
400,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
228
377
|
let changeEvent: Awaited<ReturnType<typeof recordChangeEvent>>;
|
|
229
378
|
try {
|
|
230
|
-
changeEvent = await recordChangeEvent(db, projectId,
|
|
231
|
-
id: typeof body.id === "string" ? body.id : undefined,
|
|
232
|
-
environment:
|
|
233
|
-
typeof body.environment === "string" ? body.environment : undefined,
|
|
234
|
-
kind: normalizedKind,
|
|
235
|
-
source:
|
|
236
|
-
body.source === "inferred_telemetry" ||
|
|
237
|
-
body.source === "autosre_remediation" ||
|
|
238
|
-
body.source === "ci_webhook"
|
|
239
|
-
? body.source
|
|
240
|
-
: "explicit_api",
|
|
241
|
-
targetNodeId:
|
|
242
|
-
typeof body.targetNodeId === "string"
|
|
243
|
-
? body.targetNodeId
|
|
244
|
-
: undefined,
|
|
245
|
-
serviceName,
|
|
246
|
-
namespace,
|
|
247
|
-
version: typeof body.version === "string" ? body.version : null,
|
|
248
|
-
previousVersion:
|
|
249
|
-
typeof body.previousVersion === "string"
|
|
250
|
-
? body.previousVersion
|
|
251
|
-
: null,
|
|
252
|
-
commitSha: typeof body.commitSha === "string" ? body.commitSha : null,
|
|
253
|
-
previousCommitSha:
|
|
254
|
-
typeof body.previousCommitSha === "string"
|
|
255
|
-
? body.previousCommitSha
|
|
256
|
-
: null,
|
|
257
|
-
configDigest:
|
|
258
|
-
typeof body.configDigest === "string" ? body.configDigest : null,
|
|
259
|
-
summary,
|
|
260
|
-
actorId,
|
|
261
|
-
repositoryUrl,
|
|
262
|
-
pullRequestUrl,
|
|
263
|
-
causedByActionId:
|
|
264
|
-
typeof body.causedByActionId === "string"
|
|
265
|
-
? body.causedByActionId
|
|
266
|
-
: null,
|
|
267
|
-
agentRunId:
|
|
268
|
-
typeof body.agentRunId === "string" ? body.agentRunId : null,
|
|
269
|
-
touchedPaths,
|
|
270
|
-
attributes,
|
|
271
|
-
occurredAt:
|
|
272
|
-
typeof body.occurredAt === "string" ? body.occurredAt : undefined,
|
|
273
|
-
});
|
|
379
|
+
changeEvent = await recordChangeEvent(db, projectId, parsedInput.data);
|
|
274
380
|
} catch (err) {
|
|
275
381
|
if (err instanceof InvalidTimestampError) {
|
|
276
382
|
return c.json({ error: err.message }, 400);
|
|
@@ -278,7 +384,12 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
278
384
|
throw err;
|
|
279
385
|
}
|
|
280
386
|
|
|
281
|
-
return
|
|
387
|
+
return respondWithContract(
|
|
388
|
+
c,
|
|
389
|
+
"POST /v1/changes",
|
|
390
|
+
{ ...changeEvent, changeEvent, change: changeEvent },
|
|
391
|
+
201,
|
|
392
|
+
);
|
|
282
393
|
});
|
|
283
394
|
|
|
284
395
|
// 2. Ontology Graph & Fault Localization
|
|
@@ -296,7 +407,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
296
407
|
? await loadTraceFaultLocusFromDb(db, projectId, traceId)
|
|
297
408
|
: null;
|
|
298
409
|
|
|
299
|
-
return c
|
|
410
|
+
return respondWithContract(c, "GET /internal/ontology", {
|
|
300
411
|
...graph,
|
|
301
412
|
faultLocus,
|
|
302
413
|
});
|
|
@@ -323,7 +434,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
323
434
|
since: c.req.query("since") || undefined,
|
|
324
435
|
limit: parseLimit(c.req.query("limit"), 50),
|
|
325
436
|
});
|
|
326
|
-
return c
|
|
437
|
+
return respondWithContract(c, "GET /internal/changes", { changes });
|
|
327
438
|
} catch (err) {
|
|
328
439
|
if (err instanceof InvalidTimestampError) {
|
|
329
440
|
return c.json({ error: err.message }, 400);
|
|
@@ -359,7 +470,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
359
470
|
if (!blastRadius) {
|
|
360
471
|
return c.json({ error: "Change event not found" }, 404);
|
|
361
472
|
}
|
|
362
|
-
return c
|
|
473
|
+
return respondWithContract(c, "GET /internal/changes/:id/blast-radius", {
|
|
474
|
+
blastRadius,
|
|
475
|
+
...blastRadius,
|
|
476
|
+
});
|
|
363
477
|
});
|
|
364
478
|
|
|
365
479
|
// 4. Incident Investigations & Lease Coordination
|
|
@@ -372,10 +486,12 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
372
486
|
status: c.req.query("status") || undefined,
|
|
373
487
|
limit: parseLimit(c.req.query("limit"), 50),
|
|
374
488
|
});
|
|
375
|
-
return c
|
|
489
|
+
return respondWithContract(c, "GET /internal/incidents", { incidents });
|
|
376
490
|
});
|
|
377
491
|
|
|
378
|
-
|
|
492
|
+
const openIncidentHandler = async (
|
|
493
|
+
c: Context<{ Bindings: CollectorEnv }>,
|
|
494
|
+
) => {
|
|
379
495
|
const projectId = getProjectId(c);
|
|
380
496
|
const body = await readJsonObject<Record<string, unknown>>(c);
|
|
381
497
|
if (!body) {
|
|
@@ -429,8 +545,15 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
429
545
|
? (body.latestBundle as Record<string, unknown>)
|
|
430
546
|
: null,
|
|
431
547
|
});
|
|
432
|
-
return
|
|
433
|
-
|
|
548
|
+
return respondWithContract(
|
|
549
|
+
c,
|
|
550
|
+
"POST /internal/incidents",
|
|
551
|
+
result,
|
|
552
|
+
result.deduplicated ? 200 : 201,
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
app.post("/internal/incidents", openIncidentHandler);
|
|
556
|
+
app.post("/internal/incidents/open", openIncidentHandler);
|
|
434
557
|
|
|
435
558
|
app.post("/internal/incidents/:id/lease", async (c) => {
|
|
436
559
|
const projectId = getProjectId(c);
|
|
@@ -444,7 +567,9 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
444
567
|
? body.runnerId.trim()
|
|
445
568
|
: typeof body.leaseOwner === "string"
|
|
446
569
|
? body.leaseOwner.trim()
|
|
447
|
-
: ""
|
|
570
|
+
: typeof body.leaseOwnerId === "string"
|
|
571
|
+
? body.leaseOwnerId.trim()
|
|
572
|
+
: "";
|
|
448
573
|
if (!runnerId) {
|
|
449
574
|
return c.json({ error: "runnerId is required" }, 400);
|
|
450
575
|
}
|
|
@@ -456,17 +581,28 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
456
581
|
: typeof body.ttlSeconds === "number" &&
|
|
457
582
|
Number.isFinite(body.ttlSeconds)
|
|
458
583
|
? body.ttlSeconds
|
|
459
|
-
: typeof body.
|
|
460
|
-
Number.isFinite(body.
|
|
461
|
-
?
|
|
462
|
-
:
|
|
584
|
+
: typeof body.leaseSeconds === "number" &&
|
|
585
|
+
Number.isFinite(body.leaseSeconds)
|
|
586
|
+
? body.leaseSeconds
|
|
587
|
+
: typeof body.leaseDurationMs === "number" &&
|
|
588
|
+
Number.isFinite(body.leaseDurationMs)
|
|
589
|
+
? Math.ceil(body.leaseDurationMs / 1000)
|
|
590
|
+
: undefined;
|
|
463
591
|
const result = await acquireIncidentLease(db, projectId, incidentId, {
|
|
464
592
|
environment:
|
|
465
593
|
typeof body.environment === "string" ? body.environment : undefined,
|
|
466
594
|
runnerId,
|
|
467
595
|
leaseDurationSeconds,
|
|
468
596
|
});
|
|
469
|
-
|
|
597
|
+
if (!result.acquired) {
|
|
598
|
+
return c.json(result, 409);
|
|
599
|
+
}
|
|
600
|
+
return respondWithContract(
|
|
601
|
+
c,
|
|
602
|
+
"POST /internal/incidents/:id/lease",
|
|
603
|
+
result,
|
|
604
|
+
200,
|
|
605
|
+
);
|
|
470
606
|
});
|
|
471
607
|
|
|
472
608
|
app.post("/internal/incidents/:id/dispatch", async (c) => {
|
|
@@ -493,7 +629,15 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
493
629
|
allowPrivateNetworks: c.env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
494
630
|
logger: runtime.logger,
|
|
495
631
|
});
|
|
496
|
-
|
|
632
|
+
if (!result.delivered) {
|
|
633
|
+
return c.json({ ...result, dispatched: result.delivered }, 502);
|
|
634
|
+
}
|
|
635
|
+
return respondWithContract(
|
|
636
|
+
c,
|
|
637
|
+
"POST /internal/incidents/:id/dispatch",
|
|
638
|
+
{ ...result, dispatched: result.delivered },
|
|
639
|
+
200,
|
|
640
|
+
);
|
|
497
641
|
});
|
|
498
642
|
|
|
499
643
|
// 5. Investigation Findings & Counterfactual Verification
|
|
@@ -516,7 +660,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
516
660
|
incidentId: c.req.query("incidentId") || undefined,
|
|
517
661
|
limit: parseLimit(c.req.query("limit"), 20),
|
|
518
662
|
});
|
|
519
|
-
return c
|
|
663
|
+
return respondWithContract(c, "GET /internal/findings", { findings });
|
|
520
664
|
});
|
|
521
665
|
|
|
522
666
|
app.post("/internal/findings", async (c) => {
|
|
@@ -582,65 +726,67 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
582
726
|
}
|
|
583
727
|
}
|
|
584
728
|
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
id: typeof body.id === "string" ? body.id : undefined,
|
|
588
|
-
environment:
|
|
589
|
-
typeof body.environment === "string" ? body.environment : undefined,
|
|
590
|
-
incidentId:
|
|
591
|
-
typeof body.incidentId === "string" ? body.incidentId : null,
|
|
592
|
-
fingerprintInput:
|
|
593
|
-
body.fingerprintInput && typeof body.fingerprintInput === "object"
|
|
594
|
-
? (body.fingerprintInput as InvestigationFindingInput["fingerprintInput"])
|
|
595
|
-
: undefined,
|
|
729
|
+
const parsed = InvestigationFindingHttpInputSchema.safeParse({
|
|
730
|
+
...body,
|
|
596
731
|
title,
|
|
597
732
|
summary,
|
|
598
|
-
|
|
599
|
-
typeof body.causalProofTier === "string"
|
|
600
|
-
? (body.causalProofTier as CausalProofTier)
|
|
601
|
-
: undefined,
|
|
602
|
-
confidence:
|
|
603
|
-
typeof body.confidence === "number" ? body.confidence : undefined,
|
|
604
|
-
symptomEntityKind: symptomEntityKind as EvidenceEntityKind,
|
|
733
|
+
symptomEntityKind,
|
|
605
734
|
symptomEntityId,
|
|
606
|
-
faultLocusEntityKind
|
|
735
|
+
faultLocusEntityKind,
|
|
607
736
|
faultLocusEntityId,
|
|
608
|
-
|
|
609
|
-
typeof body.
|
|
610
|
-
?
|
|
611
|
-
:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
737
|
+
idempotencyKey:
|
|
738
|
+
typeof body.idempotencyKey === "string"
|
|
739
|
+
? body.idempotencyKey.trim() || undefined
|
|
740
|
+
: body.idempotencyKey,
|
|
741
|
+
});
|
|
742
|
+
if (!parsed.success) {
|
|
743
|
+
return c.json(
|
|
744
|
+
{
|
|
745
|
+
error: parsed.error.issues
|
|
746
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
747
|
+
.join("; "),
|
|
748
|
+
},
|
|
749
|
+
400,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const data = parsed.data;
|
|
753
|
+
|
|
754
|
+
const db = sqlDbFor(c.env);
|
|
755
|
+
const input: InvestigationFindingInput = {
|
|
756
|
+
id: data.id,
|
|
757
|
+
idempotencyKey: data.idempotencyKey,
|
|
758
|
+
environment: data.environment,
|
|
759
|
+
incidentId: data.incidentId ?? null,
|
|
760
|
+
fingerprintInput: data.fingerprintInput,
|
|
761
|
+
title: data.title,
|
|
762
|
+
summary: data.summary,
|
|
763
|
+
causalProofTier: data.causalProofTier,
|
|
764
|
+
confidence: data.confidence,
|
|
765
|
+
symptomEntityKind: data.symptomEntityKind,
|
|
766
|
+
symptomEntityId: data.symptomEntityId,
|
|
767
|
+
faultLocusEntityKind: data.faultLocusEntityKind,
|
|
768
|
+
faultLocusEntityId: data.faultLocusEntityId,
|
|
769
|
+
rootCauseEntityKind: data.rootCauseEntityKind ?? null,
|
|
770
|
+
rootCauseEntityId: data.rootCauseEntityId ?? null,
|
|
771
|
+
contributingChangeId: data.contributingChangeId ?? null,
|
|
772
|
+
cohortVerdict: data.cohortVerdict ?? null,
|
|
773
|
+
contendingTraceId: data.contendingTraceId ?? null,
|
|
774
|
+
causalChain: (data.causalChain ??
|
|
775
|
+
[]) as InvestigationFindingInput["causalChain"],
|
|
776
|
+
citedEvidenceIds: data.citedEvidenceIds ?? [],
|
|
777
|
+
codeReferences: data.codeReferences,
|
|
778
|
+
agentRunId: data.agentRunId ?? null,
|
|
638
779
|
};
|
|
639
780
|
|
|
640
781
|
const finding = await recordInvestigationFinding(db, projectId, input, {
|
|
641
782
|
logger: runtime.logger,
|
|
642
783
|
});
|
|
643
|
-
return
|
|
784
|
+
return respondWithContract(
|
|
785
|
+
c,
|
|
786
|
+
"POST /internal/findings",
|
|
787
|
+
{ ...finding, finding, id: finding.id },
|
|
788
|
+
201,
|
|
789
|
+
);
|
|
644
790
|
});
|
|
645
791
|
|
|
646
792
|
app.post("/internal/findings/:id/counterfactual", async (c) => {
|
|
@@ -688,7 +834,15 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
688
834
|
: undefined,
|
|
689
835
|
},
|
|
690
836
|
);
|
|
691
|
-
return
|
|
837
|
+
return respondWithContract(
|
|
838
|
+
c,
|
|
839
|
+
"POST /internal/findings/:id/counterfactual",
|
|
840
|
+
{
|
|
841
|
+
verified: true,
|
|
842
|
+
finding,
|
|
843
|
+
proof: finding.counterfactualProof,
|
|
844
|
+
},
|
|
845
|
+
);
|
|
692
846
|
} catch (err) {
|
|
693
847
|
const message = err instanceof Error ? err.message : String(err);
|
|
694
848
|
if (err instanceof CounterfactualVerificationError) {
|
|
@@ -702,6 +856,40 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
702
856
|
}
|
|
703
857
|
});
|
|
704
858
|
|
|
859
|
+
// Lightweight existence check so callers can validate counterfactual
|
|
860
|
+
// prerequisites before persisting a finding.
|
|
861
|
+
app.get("/internal/evals/runs/:id", async (c) => {
|
|
862
|
+
const projectId = getProjectId(c);
|
|
863
|
+
const runId = c.req.param("id");
|
|
864
|
+
const db = sqlDbFor(c.env);
|
|
865
|
+
const row = await db
|
|
866
|
+
.prepare(
|
|
867
|
+
`SELECT id, eval_case_id, service_version, commit_sha,
|
|
868
|
+
pass_count, fail_count
|
|
869
|
+
FROM eval_runs WHERE project_id = ? AND id = ? LIMIT 1`,
|
|
870
|
+
)
|
|
871
|
+
.bind(projectId, runId)
|
|
872
|
+
.first<{
|
|
873
|
+
id: string;
|
|
874
|
+
eval_case_id: string | null;
|
|
875
|
+
service_version: string | null;
|
|
876
|
+
commit_sha: string | null;
|
|
877
|
+
pass_count: number | null;
|
|
878
|
+
fail_count: number | null;
|
|
879
|
+
}>();
|
|
880
|
+
if (!row) return c.json({ error: "Eval run not found" }, 404);
|
|
881
|
+
return respondWithContract(c, "GET /internal/evals/runs/:id", {
|
|
882
|
+
run: {
|
|
883
|
+
id: row.id,
|
|
884
|
+
evalCaseId: row.eval_case_id,
|
|
885
|
+
serviceVersion: row.service_version,
|
|
886
|
+
commitSha: row.commit_sha,
|
|
887
|
+
passCount: Number(row.pass_count ?? 0),
|
|
888
|
+
failCount: Number(row.fail_count ?? 0),
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
705
893
|
// 6. Remediation Proposals, Human Approval, Claim Grant, Watchdog
|
|
706
894
|
app.get("/internal/remediations", async (c) => {
|
|
707
895
|
const projectId = getProjectId(c);
|
|
@@ -716,7 +904,9 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
716
904
|
undefined,
|
|
717
905
|
limit: parseLimit(c.req.query("limit"), 50),
|
|
718
906
|
});
|
|
719
|
-
return c
|
|
907
|
+
return respondWithContract(c, "GET /internal/remediations", {
|
|
908
|
+
proposals,
|
|
909
|
+
});
|
|
720
910
|
});
|
|
721
911
|
|
|
722
912
|
app.post("/internal/remediations/propose", async (c) => {
|
|
@@ -774,7 +964,23 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
774
964
|
: "autonomous_write";
|
|
775
965
|
|
|
776
966
|
const db = sqlDbFor(c.env);
|
|
777
|
-
const
|
|
967
|
+
const callbackUrl =
|
|
968
|
+
typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
|
|
969
|
+
if (callbackUrl) {
|
|
970
|
+
try {
|
|
971
|
+
assertSafeWebhookUrl(callbackUrl, {
|
|
972
|
+
allowPrivateNetworks: c.env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
973
|
+
});
|
|
974
|
+
} catch (err) {
|
|
975
|
+
return c.json(
|
|
976
|
+
{
|
|
977
|
+
error: `webhookUrl rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
978
|
+
},
|
|
979
|
+
400,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const rawInput: RemediationProposalInput = {
|
|
778
984
|
environment:
|
|
779
985
|
typeof body.environment === "string" ? body.environment : undefined,
|
|
780
986
|
incidentId,
|
|
@@ -786,23 +992,39 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
786
992
|
desiredTargetState,
|
|
787
993
|
requestedAutonomy,
|
|
788
994
|
};
|
|
995
|
+
const parsedInput = RemediationProposalInputSchema.safeParse(rawInput);
|
|
996
|
+
const input: RemediationProposalInput = parsedInput.success
|
|
997
|
+
? parsedInput.data
|
|
998
|
+
: rawInput;
|
|
789
999
|
|
|
790
1000
|
try {
|
|
791
1001
|
const proposal = await proposeRemediationAction(db, projectId, input, {
|
|
792
1002
|
logger: runtime.logger,
|
|
1003
|
+
callback: callbackUrl
|
|
1004
|
+
? {
|
|
1005
|
+
url: callbackUrl,
|
|
1006
|
+
secret:
|
|
1007
|
+
typeof body.secret === "string" && body.secret
|
|
1008
|
+
? body.secret
|
|
1009
|
+
: undefined,
|
|
1010
|
+
}
|
|
1011
|
+
: undefined,
|
|
793
1012
|
});
|
|
794
|
-
const webhookDelivery = await deliverGrant(
|
|
1013
|
+
const { webhookDelivery, grantDelivery } = await deliverGrant(
|
|
795
1014
|
c.env,
|
|
796
1015
|
db,
|
|
797
1016
|
projectId,
|
|
798
1017
|
body,
|
|
799
1018
|
proposal,
|
|
800
1019
|
);
|
|
801
|
-
return
|
|
1020
|
+
return respondWithContract(
|
|
1021
|
+
c,
|
|
1022
|
+
"POST /internal/remediations/propose",
|
|
802
1023
|
{
|
|
803
1024
|
proposal,
|
|
804
1025
|
...proposal,
|
|
805
1026
|
grantToken: proposal.grantToken ?? null,
|
|
1027
|
+
grantDelivery,
|
|
806
1028
|
...(webhookDelivery ? { webhookDelivery } : {}),
|
|
807
1029
|
},
|
|
808
1030
|
201,
|
|
@@ -836,6 +1058,13 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
836
1058
|
|
|
837
1059
|
const db = sqlDbFor(c.env);
|
|
838
1060
|
try {
|
|
1061
|
+
// Read before approving: the approve UPDATE clears the stored callback.
|
|
1062
|
+
const storedCallback = await getRemediationCallback(
|
|
1063
|
+
db,
|
|
1064
|
+
projectId,
|
|
1065
|
+
proposalId,
|
|
1066
|
+
environment,
|
|
1067
|
+
);
|
|
839
1068
|
const proposal = await approveRemediationProposal(
|
|
840
1069
|
db,
|
|
841
1070
|
projectId,
|
|
@@ -845,19 +1074,25 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
845
1074
|
approvedByUserId,
|
|
846
1075
|
},
|
|
847
1076
|
);
|
|
848
|
-
const webhookDelivery = await deliverGrant(
|
|
1077
|
+
const { webhookDelivery, grantDelivery } = await deliverGrant(
|
|
849
1078
|
c.env,
|
|
850
1079
|
db,
|
|
851
1080
|
projectId,
|
|
852
1081
|
body,
|
|
853
1082
|
proposal,
|
|
1083
|
+
storedCallback,
|
|
1084
|
+
);
|
|
1085
|
+
return respondWithContract(
|
|
1086
|
+
c,
|
|
1087
|
+
"POST /internal/remediations/:id/approve",
|
|
1088
|
+
{
|
|
1089
|
+
proposal,
|
|
1090
|
+
...proposal,
|
|
1091
|
+
grantToken: proposal.grantToken ?? null,
|
|
1092
|
+
grantDelivery,
|
|
1093
|
+
...(webhookDelivery ? { webhookDelivery } : {}),
|
|
1094
|
+
},
|
|
854
1095
|
);
|
|
855
|
-
return c.json({
|
|
856
|
-
proposal,
|
|
857
|
-
...proposal,
|
|
858
|
-
grantToken: proposal.grantToken ?? null,
|
|
859
|
-
...(webhookDelivery ? { webhookDelivery } : {}),
|
|
860
|
-
});
|
|
861
1096
|
} catch (err) {
|
|
862
1097
|
const message = err instanceof Error ? err.message : String(err);
|
|
863
1098
|
const status =
|
|
@@ -897,7 +1132,11 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
897
1132
|
reason,
|
|
898
1133
|
},
|
|
899
1134
|
);
|
|
900
|
-
return
|
|
1135
|
+
return respondWithContract(
|
|
1136
|
+
c,
|
|
1137
|
+
"POST /internal/remediations/:id/reject",
|
|
1138
|
+
{ proposal, ...proposal },
|
|
1139
|
+
);
|
|
901
1140
|
} catch (err) {
|
|
902
1141
|
const message = err instanceof Error ? err.message : String(err);
|
|
903
1142
|
const status =
|
|
@@ -937,7 +1176,11 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
937
1176
|
logger: runtime.logger,
|
|
938
1177
|
},
|
|
939
1178
|
);
|
|
940
|
-
return c
|
|
1179
|
+
return respondWithContract(c, "POST /internal/remediations/:id/claim", {
|
|
1180
|
+
claimed: true,
|
|
1181
|
+
proposal,
|
|
1182
|
+
...proposal,
|
|
1183
|
+
});
|
|
941
1184
|
} catch (err) {
|
|
942
1185
|
const message = err instanceof Error ? err.message : String(err);
|
|
943
1186
|
const status =
|
|
@@ -983,7 +1226,11 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
983
1226
|
: null,
|
|
984
1227
|
},
|
|
985
1228
|
);
|
|
986
|
-
return
|
|
1229
|
+
return respondWithContract(
|
|
1230
|
+
c,
|
|
1231
|
+
"POST /internal/remediations/:id/complete",
|
|
1232
|
+
{ proposal, ...proposal },
|
|
1233
|
+
);
|
|
987
1234
|
} catch (err) {
|
|
988
1235
|
const message = err instanceof Error ? err.message : String(err);
|
|
989
1236
|
const status =
|
|
@@ -1022,7 +1269,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1022
1269
|
: "Execution failed",
|
|
1023
1270
|
},
|
|
1024
1271
|
);
|
|
1025
|
-
return c
|
|
1272
|
+
return respondWithContract(c, "POST /internal/remediations/:id/fail", {
|
|
1273
|
+
proposal,
|
|
1274
|
+
...proposal,
|
|
1275
|
+
});
|
|
1026
1276
|
} catch (err) {
|
|
1027
1277
|
const message = err instanceof Error ? err.message : String(err);
|
|
1028
1278
|
const status =
|
|
@@ -1049,7 +1299,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1049
1299
|
: undefined,
|
|
1050
1300
|
logger: runtime.logger,
|
|
1051
1301
|
});
|
|
1052
|
-
return c
|
|
1302
|
+
return respondWithContract(c, "POST /internal/remediations/watchdog", {
|
|
1303
|
+
summary,
|
|
1304
|
+
...summary,
|
|
1305
|
+
});
|
|
1053
1306
|
});
|
|
1054
1307
|
|
|
1055
1308
|
app.get("/internal/remediations/:id", async (c) => {
|
|
@@ -1065,7 +1318,9 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1065
1318
|
if (!proposal) {
|
|
1066
1319
|
return c.json({ error: "Proposal not found" }, 404);
|
|
1067
1320
|
}
|
|
1068
|
-
return c
|
|
1321
|
+
return respondWithContract(c, "GET /internal/remediations/:id", {
|
|
1322
|
+
proposal,
|
|
1323
|
+
});
|
|
1069
1324
|
});
|
|
1070
1325
|
|
|
1071
1326
|
app.get("/internal/incidents/:id", async (c) => {
|
|
@@ -1096,7 +1351,11 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1096
1351
|
limit: 20,
|
|
1097
1352
|
}),
|
|
1098
1353
|
]);
|
|
1099
|
-
return c
|
|
1354
|
+
return respondWithContract(c, "GET /internal/incidents/:id", {
|
|
1355
|
+
incident,
|
|
1356
|
+
findings,
|
|
1357
|
+
proposals,
|
|
1358
|
+
});
|
|
1100
1359
|
});
|
|
1101
1360
|
|
|
1102
1361
|
app.get("/internal/findings/:id", async (c) => {
|
|
@@ -1112,7 +1371,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1112
1371
|
if (!finding) {
|
|
1113
1372
|
return c.json({ error: "Finding not found" }, 404);
|
|
1114
1373
|
}
|
|
1115
|
-
return c
|
|
1374
|
+
return respondWithContract(c, "GET /internal/findings/:id", { finding });
|
|
1116
1375
|
});
|
|
1117
1376
|
|
|
1118
1377
|
app.get("/internal/changes/:id", async (c) => {
|
|
@@ -1128,7 +1387,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
1128
1387
|
if (!changeEvent) {
|
|
1129
1388
|
return c.json({ error: "Change event not found" }, 404);
|
|
1130
1389
|
}
|
|
1131
|
-
return c
|
|
1390
|
+
return respondWithContract(c, "GET /internal/changes/:id", {
|
|
1391
|
+
changeEvent,
|
|
1392
|
+
change: changeEvent,
|
|
1393
|
+
});
|
|
1132
1394
|
});
|
|
1133
1395
|
},
|
|
1134
1396
|
};
|