@obsunified/collector 2.2.1 → 2.2.3
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.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +497 -96
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/autosre-closed-loop.test.ts +590 -0
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/autosre-findings.ts +169 -14
- package/src/lib/autosre-remediation-gate.ts +156 -26
- 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/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-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 +10 -0
- package/src/plugins/autosre-routes.ts +167 -16
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +142 -0
- package/src/plugins/connected-routes/manifest.ts +127 -7
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +3 -3
- package/src/plugins/connected-routes.ts +11 -8
- package/src/plugins/evidence-retrieval-routes.ts +30 -0
- package/dist/chunk-KMEZKGSO.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
randomHex,
|
|
10
10
|
sha256Hex,
|
|
11
11
|
sqlDbFor
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-2FALTDCE.js";
|
|
13
13
|
import {
|
|
14
14
|
LlmCallError,
|
|
15
15
|
aiEvaluationEvidenceReferences,
|
|
@@ -289,6 +289,38 @@ async function ingestAICallBatch(db, calls) {
|
|
|
289
289
|
});
|
|
290
290
|
await db.batch(batch);
|
|
291
291
|
}
|
|
292
|
+
function mapAICallRow(row) {
|
|
293
|
+
return {
|
|
294
|
+
projectId: row.project_id ?? "default",
|
|
295
|
+
callId: row.call_id,
|
|
296
|
+
traceId: row.trace_id,
|
|
297
|
+
spanId: row.span_id,
|
|
298
|
+
serviceName: row.service_name,
|
|
299
|
+
modelName: row.model_name,
|
|
300
|
+
provider: row.provider,
|
|
301
|
+
callType: row.call_type,
|
|
302
|
+
requestJson: row.request_json,
|
|
303
|
+
responseJson: row.response_json,
|
|
304
|
+
promptTokens: row.prompt_tokens,
|
|
305
|
+
completionTokens: row.completion_tokens,
|
|
306
|
+
totalCostUsd: row.total_cost_usd,
|
|
307
|
+
latencyMs: row.latency_ms,
|
|
308
|
+
isError: row.is_error === 1 || row.is_error === true,
|
|
309
|
+
errorMessage: row.error_message,
|
|
310
|
+
occurredAt: row.occurred_at,
|
|
311
|
+
receivedAt: row.received_at,
|
|
312
|
+
expiresAt: row.expires_at,
|
|
313
|
+
sessionId: row.session_id ?? null,
|
|
314
|
+
interactionId: row.interaction_id ?? null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function getAICallById(db, projectId, callId) {
|
|
318
|
+
if (!projectId) throw new Error("AIStore.getAICall: projectId is required");
|
|
319
|
+
const row = await db.prepare(
|
|
320
|
+
`SELECT * FROM ai_calls WHERE project_id = ? AND call_id = ? LIMIT 1`
|
|
321
|
+
).bind(projectId, callId).first();
|
|
322
|
+
return row ? mapAICallRow(row) : null;
|
|
323
|
+
}
|
|
292
324
|
async function getAICallsOverview(db, options) {
|
|
293
325
|
if (!options.projectId)
|
|
294
326
|
throw new Error("AIStore.getAICalls: projectId is required");
|
|
@@ -329,29 +361,7 @@ async function getAICallsOverview(db, options) {
|
|
|
329
361
|
const summaryParams = [options.projectId, hours];
|
|
330
362
|
if (options.service) summaryParams.push(options.service);
|
|
331
363
|
const summaryResult = await db.prepare(summarySql).bind(...summaryParams).first() || {};
|
|
332
|
-
const calls = (results.results || []).map(
|
|
333
|
-
projectId: row.project_id ?? "default",
|
|
334
|
-
callId: row.call_id,
|
|
335
|
-
traceId: row.trace_id,
|
|
336
|
-
spanId: row.span_id,
|
|
337
|
-
serviceName: row.service_name,
|
|
338
|
-
modelName: row.model_name,
|
|
339
|
-
provider: row.provider,
|
|
340
|
-
callType: row.call_type,
|
|
341
|
-
requestJson: row.request_json,
|
|
342
|
-
responseJson: row.response_json,
|
|
343
|
-
promptTokens: row.prompt_tokens,
|
|
344
|
-
completionTokens: row.completion_tokens,
|
|
345
|
-
totalCostUsd: row.total_cost_usd,
|
|
346
|
-
latencyMs: row.latency_ms,
|
|
347
|
-
isError: row.is_error === 1 || row.is_error === true,
|
|
348
|
-
errorMessage: row.error_message,
|
|
349
|
-
occurredAt: row.occurred_at,
|
|
350
|
-
receivedAt: row.received_at,
|
|
351
|
-
expiresAt: row.expires_at,
|
|
352
|
-
sessionId: row.session_id ?? null,
|
|
353
|
-
interactionId: row.interaction_id ?? null
|
|
354
|
-
}));
|
|
364
|
+
const calls = (results.results || []).map(mapAICallRow);
|
|
355
365
|
return {
|
|
356
366
|
calls,
|
|
357
367
|
summary: {
|
|
@@ -861,6 +871,9 @@ var AIStore = class {
|
|
|
861
871
|
async getAICalls(options) {
|
|
862
872
|
return getAICallsOverview(this.db, options);
|
|
863
873
|
}
|
|
874
|
+
async getAICall(projectId, callId) {
|
|
875
|
+
return getAICallById(this.db, projectId, callId);
|
|
876
|
+
}
|
|
864
877
|
async purgeExpired() {
|
|
865
878
|
return purgeExpiredAIData(this.db);
|
|
866
879
|
}
|
|
@@ -2494,7 +2507,8 @@ import { AgentAutonomyLevel } from "@obsunified/types/constants";
|
|
|
2494
2507
|
// src/lib/autosre-findings.ts
|
|
2495
2508
|
import {
|
|
2496
2509
|
buildCanonicalNodeId as buildCanonicalNodeId2,
|
|
2497
|
-
buildFindingFingerprint
|
|
2510
|
+
buildFindingFingerprint,
|
|
2511
|
+
OntologyNodeKindSchema
|
|
2498
2512
|
} from "@obsunified/types";
|
|
2499
2513
|
|
|
2500
2514
|
// src/lib/autosre-change-correlation.ts
|
|
@@ -3112,24 +3126,98 @@ function resolveSafeCausalProofTier(input, serverCohortVerdict) {
|
|
|
3112
3126
|
}
|
|
3113
3127
|
return "unverified_locus";
|
|
3114
3128
|
}
|
|
3129
|
+
async function loadFaultLocusSpan(db, projectId, environment, spanId, traceId) {
|
|
3130
|
+
if (!spanId) return null;
|
|
3131
|
+
return db.prepare(
|
|
3132
|
+
`SELECT service_name, span_name, status_message, error_signature
|
|
3133
|
+
FROM telemetry_spans
|
|
3134
|
+
WHERE project_id = ? AND environment = ? AND span_id = ?
|
|
3135
|
+
${traceId ? "AND trace_id = ?" : ""}
|
|
3136
|
+
ORDER BY start_time DESC
|
|
3137
|
+
LIMIT 1`
|
|
3138
|
+
).bind(
|
|
3139
|
+
...traceId ? [projectId, environment, spanId, traceId] : [projectId, environment, spanId]
|
|
3140
|
+
).first();
|
|
3141
|
+
}
|
|
3142
|
+
function looksLikeCanonicalNodeId(id) {
|
|
3143
|
+
const parts = id.split(":");
|
|
3144
|
+
return parts.length >= 4 && OntologyNodeKindSchema.options.includes(parts[0]);
|
|
3145
|
+
}
|
|
3146
|
+
function splitTraceSpanLocus(id) {
|
|
3147
|
+
if (looksLikeCanonicalNodeId(id)) return null;
|
|
3148
|
+
const parts = id.split(":");
|
|
3149
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
|
|
3150
|
+
return { traceId: parts[0], spanId: parts[1] };
|
|
3151
|
+
}
|
|
3152
|
+
function fingerprintInputFromSpan(span, environment) {
|
|
3153
|
+
return {
|
|
3154
|
+
environment,
|
|
3155
|
+
originatingNodeId: buildCanonicalNodeId2({
|
|
3156
|
+
kind: "service",
|
|
3157
|
+
environment,
|
|
3158
|
+
serviceName: span.service_name || "unknown-service"
|
|
3159
|
+
}).id,
|
|
3160
|
+
// Empty strings are "absent": returning undefined lets the caller's
|
|
3161
|
+
// `??` chain reach its title/summary fallbacks.
|
|
3162
|
+
normalizedOperation: span.span_name?.trim() || void 0,
|
|
3163
|
+
errorSignature: span.error_signature?.trim() || span.status_message?.trim() || void 0,
|
|
3164
|
+
downstreamTargetNodeId: null
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3115
3167
|
async function recordInvestigationFinding(db, projectId, input, options = {}) {
|
|
3116
3168
|
const logger = options.logger ?? consoleLogger;
|
|
3117
3169
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3118
3170
|
const environment = (input.environment ?? input.fingerprintInput?.environment ?? "default").trim().toLowerCase();
|
|
3119
|
-
const
|
|
3171
|
+
const idempotencyKey = input.idempotencyKey?.trim();
|
|
3172
|
+
let idempotencyId;
|
|
3173
|
+
if (idempotencyKey) {
|
|
3174
|
+
idempotencyId = `fnd_k${(await sha256Hex(`${projectId}\0${environment}\0${idempotencyKey}`)).slice(0, 20)}`;
|
|
3175
|
+
const replay = await getInvestigationFinding(
|
|
3176
|
+
db,
|
|
3177
|
+
projectId,
|
|
3178
|
+
idempotencyId,
|
|
3179
|
+
environment
|
|
3180
|
+
);
|
|
3181
|
+
if (replay) return replay;
|
|
3182
|
+
}
|
|
3183
|
+
const supplied = input.fingerprintInput ?? {};
|
|
3184
|
+
const locusId = input.faultLocusEntityId.trim();
|
|
3185
|
+
let spanDerived = {};
|
|
3186
|
+
const traceSpan = input.faultLocusEntityKind === "span" ? splitTraceSpanLocus(locusId) : null;
|
|
3187
|
+
if (traceSpan) {
|
|
3188
|
+
const spanRow = await loadFaultLocusSpan(
|
|
3189
|
+
db,
|
|
3190
|
+
projectId,
|
|
3191
|
+
environment,
|
|
3192
|
+
traceSpan.spanId,
|
|
3193
|
+
traceSpan.traceId
|
|
3194
|
+
);
|
|
3195
|
+
if (spanRow) spanDerived = fingerprintInputFromSpan(spanRow, environment);
|
|
3196
|
+
} else if (!locusId.includes(":") && input.faultLocusEntityKind !== "service") {
|
|
3197
|
+
const spanRow = await loadFaultLocusSpan(
|
|
3198
|
+
db,
|
|
3199
|
+
projectId,
|
|
3200
|
+
environment,
|
|
3201
|
+
locusId
|
|
3202
|
+
);
|
|
3203
|
+
if (spanRow) spanDerived = fingerprintInputFromSpan(spanRow, environment);
|
|
3204
|
+
}
|
|
3205
|
+
const defaultOriginNode = locusId.includes(":") ? traceSpan ? buildCanonicalNodeId2({
|
|
3120
3206
|
kind: "service",
|
|
3121
3207
|
environment,
|
|
3122
|
-
serviceName:
|
|
3208
|
+
serviceName: locusId
|
|
3209
|
+
}).id : locusId : buildCanonicalNodeId2({
|
|
3210
|
+
kind: "service",
|
|
3211
|
+
environment,
|
|
3212
|
+
serviceName: locusId
|
|
3123
3213
|
}).id;
|
|
3124
|
-
const fpResult = buildFindingFingerprint(
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
}
|
|
3132
|
-
);
|
|
3214
|
+
const fpResult = buildFindingFingerprint({
|
|
3215
|
+
environment: supplied.environment ?? environment,
|
|
3216
|
+
originatingNodeId: supplied.originatingNodeId ?? spanDerived.originatingNodeId ?? defaultOriginNode,
|
|
3217
|
+
normalizedOperation: supplied.normalizedOperation ?? spanDerived.normalizedOperation ?? input.title,
|
|
3218
|
+
errorSignature: supplied.errorSignature ?? spanDerived.errorSignature ?? input.summary.slice(0, 120),
|
|
3219
|
+
downstreamTargetNodeId: supplied.downstreamTargetNodeId ?? spanDerived.downstreamTargetNodeId ?? null
|
|
3220
|
+
});
|
|
3133
3221
|
const priorRow = await db.prepare(
|
|
3134
3222
|
`SELECT id, causal_verification_status FROM investigation_findings
|
|
3135
3223
|
WHERE project_id = ? AND environment = ? AND fingerprint = ?
|
|
@@ -3181,7 +3269,7 @@ async function recordInvestigationFinding(db, projectId, input, options = {}) {
|
|
|
3181
3269
|
priorRecurrenceBonus: input.confidenceBreakdown?.priorRecurrenceBonus ?? priorBonus,
|
|
3182
3270
|
finalConfidence: confidence
|
|
3183
3271
|
};
|
|
3184
|
-
const id = input.id?.trim() || `fnd_${randomHex(10)}
|
|
3272
|
+
const id = idempotencyId ?? (input.id?.trim() || `fnd_${randomHex(10)}`);
|
|
3185
3273
|
const expiresAt = new Date(Date.now() + 90 * 24 * 36e5).toISOString();
|
|
3186
3274
|
await db.prepare(
|
|
3187
3275
|
`INSERT INTO investigation_findings (
|
|
@@ -3880,10 +3968,14 @@ async function dispatchIncidentWebhook(db, projectId, incidentId, webhook) {
|
|
|
3880
3968
|
webhook.environment
|
|
3881
3969
|
);
|
|
3882
3970
|
if (!incident) {
|
|
3883
|
-
return { delivered: false, error: "Incident not found" };
|
|
3971
|
+
return { delivered: false, error: "Incident not found", attempts: 0 };
|
|
3884
3972
|
}
|
|
3885
3973
|
if (!webhook.grant && incident.dispatchAttempts >= 5) {
|
|
3886
|
-
return {
|
|
3974
|
+
return {
|
|
3975
|
+
delivered: false,
|
|
3976
|
+
error: "MAX_DISPATCH_ATTEMPTS (5) exceeded",
|
|
3977
|
+
attempts: 0
|
|
3978
|
+
};
|
|
3887
3979
|
}
|
|
3888
3980
|
let safeUrl;
|
|
3889
3981
|
try {
|
|
@@ -3912,7 +4004,7 @@ async function dispatchIncidentWebhook(db, projectId, incidentId, webhook) {
|
|
|
3912
4004
|
incidentId,
|
|
3913
4005
|
error
|
|
3914
4006
|
});
|
|
3915
|
-
return { delivered: false, error };
|
|
4007
|
+
return { delivered: false, error, attempts: 0 };
|
|
3916
4008
|
}
|
|
3917
4009
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3918
4010
|
const body = JSON.stringify({
|
|
@@ -3944,37 +4036,53 @@ async function dispatchIncidentWebhook(db, projectId, incidentId, webhook) {
|
|
|
3944
4036
|
headers["X-Obs-Signature-256"] = `sha256=${hmac}`;
|
|
3945
4037
|
}
|
|
3946
4038
|
const fetchFn = webhook.fetchImpl ?? fetch;
|
|
4039
|
+
const sleep = webhook.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4040
|
+
const maxAttempts = webhook.grant ? GRANT_DELIVERY_BACKOFF_MS.length + 1 : 1;
|
|
3947
4041
|
let delivered = false;
|
|
3948
4042
|
let status;
|
|
3949
4043
|
let errorMsg;
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
4044
|
+
let attempts = 0;
|
|
4045
|
+
while (attempts < maxAttempts) {
|
|
4046
|
+
attempts++;
|
|
4047
|
+
let retryable = false;
|
|
4048
|
+
status = void 0;
|
|
4049
|
+
errorMsg = void 0;
|
|
4050
|
+
try {
|
|
4051
|
+
const res = await fetchFn(safeUrl.toString(), {
|
|
4052
|
+
method: "POST",
|
|
4053
|
+
headers,
|
|
4054
|
+
body,
|
|
4055
|
+
// Never follow redirects: a 3xx could re-aim the POST (and its
|
|
4056
|
+
// signed body, which may carry a grant token) at another host.
|
|
4057
|
+
redirect: "manual",
|
|
4058
|
+
signal: AbortSignal.timeout(5e3)
|
|
4059
|
+
});
|
|
4060
|
+
status = res.status;
|
|
4061
|
+
if (res.status >= 300 && res.status < 400) {
|
|
4062
|
+
errorMsg = `Redirect (HTTP ${res.status}) refused`;
|
|
4063
|
+
} else {
|
|
4064
|
+
delivered = res.ok;
|
|
4065
|
+
if (!res.ok) {
|
|
4066
|
+
errorMsg = `HTTP ${res.status}`;
|
|
4067
|
+
retryable = res.status >= 500;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
errorMsg = err instanceof Error ? err.message : String(err);
|
|
4072
|
+
retryable = true;
|
|
3966
4073
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
4074
|
+
if (delivered || !retryable || attempts >= maxAttempts) break;
|
|
4075
|
+
await sleep(GRANT_DELIVERY_BACKOFF_MS[attempts - 1]);
|
|
3969
4076
|
}
|
|
3970
4077
|
if (!delivered) {
|
|
3971
4078
|
logger.warn("[autosre] incident webhook delivery failed", {
|
|
3972
4079
|
incidentId,
|
|
3973
|
-
error: errorMsg
|
|
4080
|
+
error: errorMsg,
|
|
4081
|
+
attempts
|
|
3974
4082
|
});
|
|
3975
4083
|
}
|
|
3976
4084
|
if (webhook.grant) {
|
|
3977
|
-
return { delivered, status, error: errorMsg };
|
|
4085
|
+
return { delivered, status, error: errorMsg, attempts };
|
|
3978
4086
|
}
|
|
3979
4087
|
await db.prepare(
|
|
3980
4088
|
`UPDATE incident_investigations
|
|
@@ -3991,7 +4099,44 @@ async function dispatchIncidentWebhook(db, projectId, incidentId, webhook) {
|
|
|
3991
4099
|
incident.environment,
|
|
3992
4100
|
incident.id
|
|
3993
4101
|
).run();
|
|
3994
|
-
return { delivered, status, error: errorMsg };
|
|
4102
|
+
return { delivered, status, error: errorMsg, attempts };
|
|
4103
|
+
}
|
|
4104
|
+
var GRANT_DELIVERY_BACKOFF_MS = [500, 1500];
|
|
4105
|
+
async function recordCallbackDelivery(db, projectId, environment, proposalId, outcome) {
|
|
4106
|
+
const record = {
|
|
4107
|
+
status: outcome.delivered ? "delivered" : "failed",
|
|
4108
|
+
attempts: outcome.attempts,
|
|
4109
|
+
lastError: outcome.delivered ? null : outcome.error ?? null,
|
|
4110
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4111
|
+
};
|
|
4112
|
+
await db.prepare(
|
|
4113
|
+
`UPDATE remediation_proposals SET callback_delivery = ?
|
|
4114
|
+
WHERE project_id = ? AND environment = ? AND id = ?`
|
|
4115
|
+
).bind(JSON.stringify(record), projectId, environment, proposalId).run();
|
|
4116
|
+
return record;
|
|
4117
|
+
}
|
|
4118
|
+
function callbackHostOf(url) {
|
|
4119
|
+
if (!url) return null;
|
|
4120
|
+
try {
|
|
4121
|
+
return new URL(url).host || null;
|
|
4122
|
+
} catch {
|
|
4123
|
+
return null;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
function parseCallbackDelivery(raw) {
|
|
4127
|
+
if (!raw) return null;
|
|
4128
|
+
try {
|
|
4129
|
+
const v = JSON.parse(raw);
|
|
4130
|
+
if (v.status !== "delivered" && v.status !== "failed") return null;
|
|
4131
|
+
return {
|
|
4132
|
+
status: v.status,
|
|
4133
|
+
attempts: Number(v.attempts ?? 0),
|
|
4134
|
+
lastError: typeof v.lastError === "string" ? v.lastError : null,
|
|
4135
|
+
at: String(v.at ?? "")
|
|
4136
|
+
};
|
|
4137
|
+
} catch {
|
|
4138
|
+
return null;
|
|
4139
|
+
}
|
|
3995
4140
|
}
|
|
3996
4141
|
var mapProposalRow = (row, grantToken) => ({
|
|
3997
4142
|
id: row.id,
|
|
@@ -4021,6 +4166,8 @@ var mapProposalRow = (row, grantToken) => ({
|
|
|
4021
4166
|
remediationAgentRunId: row.remediation_agent_run_id,
|
|
4022
4167
|
resultingChangeId: row.resulting_change_id,
|
|
4023
4168
|
failureReason: row.failure_reason,
|
|
4169
|
+
callbackHost: callbackHostOf(row.callback_url),
|
|
4170
|
+
callbackDelivery: parseCallbackDelivery(row.callback_delivery),
|
|
4024
4171
|
createdAt: row.created_at,
|
|
4025
4172
|
updatedAt: row.updated_at,
|
|
4026
4173
|
expiresAt: row.expires_at,
|
|
@@ -4185,6 +4332,7 @@ async function proposeRemediationAction(db, projectId, input, options = {}) {
|
|
|
4185
4332
|
} else if (effectiveAutonomy === AgentAutonomyLevel.BlockedByPolicy) {
|
|
4186
4333
|
status = "rejected";
|
|
4187
4334
|
}
|
|
4335
|
+
const storedCallback = status === "pending_human_approval" && options.callback?.url ? options.callback : null;
|
|
4188
4336
|
const id = `rem_${randomHex(10)}`;
|
|
4189
4337
|
const expiresAt = new Date(Date.now() + 30 * 24 * 36e5).toISOString();
|
|
4190
4338
|
if (effectiveAutonomy === AgentAutonomyLevel.BlockedByPolicy) {
|
|
@@ -4202,8 +4350,9 @@ async function proposeRemediationAction(db, projectId, input, options = {}) {
|
|
|
4202
4350
|
expected_current_state_json, desired_target_state_json,
|
|
4203
4351
|
requested_autonomy, effective_autonomy, policy_decision_reason,
|
|
4204
4352
|
status, grant_token_hash, grant_expires_at,
|
|
4353
|
+
callback_url, callback_secret,
|
|
4205
4354
|
created_at, updated_at, expires_at
|
|
4206
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4355
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4207
4356
|
ON CONFLICT DO NOTHING`
|
|
4208
4357
|
).bind(
|
|
4209
4358
|
projectId,
|
|
@@ -4222,6 +4371,8 @@ async function proposeRemediationAction(db, projectId, input, options = {}) {
|
|
|
4222
4371
|
status,
|
|
4223
4372
|
grantTokenHash,
|
|
4224
4373
|
grantExpiresAt,
|
|
4374
|
+
storedCallback?.url ?? null,
|
|
4375
|
+
storedCallback?.secret ?? null,
|
|
4225
4376
|
nowIso,
|
|
4226
4377
|
nowIso,
|
|
4227
4378
|
expiresAt
|
|
@@ -4284,6 +4435,14 @@ async function listRemediationProposals(db, options) {
|
|
|
4284
4435
|
).bind(...binds).all();
|
|
4285
4436
|
return rows.results.map((r) => mapProposalRow(r));
|
|
4286
4437
|
}
|
|
4438
|
+
async function getRemediationCallback(db, projectId, proposalId, environment) {
|
|
4439
|
+
const row = await loadProposalRow(db, projectId, proposalId, environment);
|
|
4440
|
+
if (!row?.callback_url) return null;
|
|
4441
|
+
return {
|
|
4442
|
+
url: row.callback_url,
|
|
4443
|
+
...row.callback_secret ? { secret: row.callback_secret } : {}
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4287
4446
|
async function approveRemediationProposal(db, projectId, proposalId, options) {
|
|
4288
4447
|
const row = await loadProposalRow(
|
|
4289
4448
|
db,
|
|
@@ -4311,6 +4470,8 @@ async function approveRemediationProposal(db, projectId, proposalId, options) {
|
|
|
4311
4470
|
grant_expires_at = ?,
|
|
4312
4471
|
approved_by_user_id = ?,
|
|
4313
4472
|
approved_at = ?,
|
|
4473
|
+
callback_url = NULL,
|
|
4474
|
+
callback_secret = NULL,
|
|
4314
4475
|
updated_at = ?
|
|
4315
4476
|
WHERE project_id = ? AND environment = ? AND id = ?
|
|
4316
4477
|
AND status = 'pending_human_approval'`
|
|
@@ -4358,6 +4519,8 @@ async function rejectRemediationProposal(db, projectId, proposalId, options) {
|
|
|
4358
4519
|
`UPDATE remediation_proposals
|
|
4359
4520
|
SET status = 'rejected',
|
|
4360
4521
|
grant_token_hash = NULL,
|
|
4522
|
+
callback_url = NULL,
|
|
4523
|
+
callback_secret = NULL,
|
|
4361
4524
|
failure_reason = ?,
|
|
4362
4525
|
updated_at = ?
|
|
4363
4526
|
WHERE project_id = ? AND environment = ? AND id = ?
|
|
@@ -8125,14 +8288,20 @@ var UsageStore = class {
|
|
|
8125
8288
|
SUM(CASE WHEN event_type = 'interaction' THEN 1 ELSE 0 END) AS interaction_count,
|
|
8126
8289
|
SUM(CASE WHEN event_type = 'frontend_error' OR severity = 'error' THEN 1 ELSE 0 END) AS error_count,
|
|
8127
8290
|
MAX(CASE WHEN event_type = 'page_view' THEN CAST(COALESCE(${loadMsExpr}, ${loadTimeMsExpr}, ${durationMsExpr}) AS REAL) ELSE NULL END) AS max_load_ms,
|
|
8128
|
-
(SELECT referrer FROM usage_events ref WHERE ref.project_id =
|
|
8129
|
-
(SELECT page_path FROM usage_events lp WHERE lp.project_id =
|
|
8291
|
+
(SELECT referrer FROM usage_events ref WHERE ref.project_id = ? AND ref.session_id = ue.session_id ORDER BY occurred_at ASC LIMIT 1) AS referrer,
|
|
8292
|
+
(SELECT page_path FROM usage_events lp WHERE lp.project_id = ? AND lp.session_id = ue.session_id ORDER BY occurred_at DESC LIMIT 1) AS last_path
|
|
8130
8293
|
FROM usage_events ue
|
|
8131
8294
|
WHERE project_id = ? AND received_at >= ?
|
|
8132
8295
|
GROUP BY session_id
|
|
8133
8296
|
ORDER BY last_seen DESC
|
|
8134
8297
|
LIMIT ?`
|
|
8135
|
-
).bind(
|
|
8298
|
+
).bind(
|
|
8299
|
+
options.projectId,
|
|
8300
|
+
options.projectId,
|
|
8301
|
+
options.projectId,
|
|
8302
|
+
cutoff,
|
|
8303
|
+
limit * 3
|
|
8304
|
+
).all();
|
|
8136
8305
|
const raw = (result.results ?? []).map((r) => ({
|
|
8137
8306
|
sessionId: r.session_id,
|
|
8138
8307
|
visitorId: r.visitor_id,
|
|
@@ -11512,6 +11681,13 @@ var aiReceiverPlugin = {
|
|
|
11512
11681
|
});
|
|
11513
11682
|
return c.json(response);
|
|
11514
11683
|
});
|
|
11684
|
+
app.get("/internal/ai/calls/:callId", async (c) => {
|
|
11685
|
+
const projectId = getProjectId(c);
|
|
11686
|
+
const store = new AIStore(sqlDbFor(c.env));
|
|
11687
|
+
const call = await store.getAICall(projectId, c.req.param("callId"));
|
|
11688
|
+
if (!call) return c.json({ error: "AI call not found" }, 404);
|
|
11689
|
+
return c.json({ call, traceId: call.traceId, spanId: call.spanId });
|
|
11690
|
+
});
|
|
11515
11691
|
app.get("/internal/ai/spans", async (c) => {
|
|
11516
11692
|
const projectId = getProjectId(c);
|
|
11517
11693
|
const store = new AIStore(sqlDbFor(c.env));
|
|
@@ -13050,22 +13226,74 @@ async function requireHumanDashboardSession(c) {
|
|
|
13050
13226
|
var autosreRoutesPlugin = {
|
|
13051
13227
|
name: "autosre-routes",
|
|
13052
13228
|
register(app, runtime) {
|
|
13053
|
-
const deliverGrant = async (env, db, projectId, body, proposal) => {
|
|
13054
|
-
const
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13229
|
+
const deliverGrant = async (env, db, projectId, body, proposal, stored) => {
|
|
13230
|
+
const bodyUrl = typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
|
|
13231
|
+
const target = bodyUrl ? {
|
|
13232
|
+
url: bodyUrl,
|
|
13233
|
+
secret: typeof body.secret === "string" ? body.secret : void 0
|
|
13234
|
+
} : stored ?? null;
|
|
13235
|
+
const notAttempted = {
|
|
13236
|
+
attempted: false,
|
|
13237
|
+
delivered: false,
|
|
13238
|
+
attempts: 0,
|
|
13239
|
+
host: null
|
|
13240
|
+
};
|
|
13241
|
+
if (!target || !proposal.grantToken) {
|
|
13242
|
+
return { webhookDelivery: void 0, grantDelivery: notAttempted };
|
|
13243
|
+
}
|
|
13244
|
+
if (proposal.status !== "grant_issued") {
|
|
13245
|
+
return { webhookDelivery: void 0, grantDelivery: notAttempted };
|
|
13246
|
+
}
|
|
13247
|
+
let host = null;
|
|
13248
|
+
try {
|
|
13249
|
+
host = new URL(target.url).host || null;
|
|
13250
|
+
} catch {
|
|
13251
|
+
host = null;
|
|
13252
|
+
}
|
|
13253
|
+
const webhookDelivery = await dispatchIncidentWebhook(
|
|
13254
|
+
db,
|
|
13255
|
+
projectId,
|
|
13256
|
+
proposal.incidentId,
|
|
13257
|
+
{
|
|
13258
|
+
url: target.url,
|
|
13259
|
+
secret: target.secret,
|
|
13260
|
+
environment: proposal.environment,
|
|
13261
|
+
allowPrivateNetworks: env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
13262
|
+
logger: runtime.logger,
|
|
13263
|
+
grant: {
|
|
13264
|
+
proposalId: proposal.id,
|
|
13265
|
+
status: proposal.status,
|
|
13266
|
+
grantToken: proposal.grantToken
|
|
13267
|
+
}
|
|
13067
13268
|
}
|
|
13068
|
-
|
|
13269
|
+
);
|
|
13270
|
+
let error = webhookDelivery.error ? webhookDelivery.error.replace(
|
|
13271
|
+
/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi,
|
|
13272
|
+
"<url>"
|
|
13273
|
+
) : void 0;
|
|
13274
|
+
if (error && target.secret) {
|
|
13275
|
+
error = error.split(target.secret).join("<redacted>");
|
|
13276
|
+
}
|
|
13277
|
+
const record = await recordCallbackDelivery(
|
|
13278
|
+
db,
|
|
13279
|
+
projectId,
|
|
13280
|
+
proposal.environment,
|
|
13281
|
+
proposal.id,
|
|
13282
|
+
{
|
|
13283
|
+
delivered: webhookDelivery.delivered,
|
|
13284
|
+
attempts: webhookDelivery.attempts,
|
|
13285
|
+
error
|
|
13286
|
+
}
|
|
13287
|
+
);
|
|
13288
|
+
proposal.callbackDelivery = record;
|
|
13289
|
+
const grantDelivery = {
|
|
13290
|
+
attempted: true,
|
|
13291
|
+
delivered: webhookDelivery.delivered,
|
|
13292
|
+
attempts: webhookDelivery.attempts,
|
|
13293
|
+
host,
|
|
13294
|
+
...webhookDelivery.delivered ? {} : { error: error ?? "unknown error" }
|
|
13295
|
+
};
|
|
13296
|
+
return { webhookDelivery, grantDelivery };
|
|
13069
13297
|
};
|
|
13070
13298
|
app.post("/v1/changes", async (c) => {
|
|
13071
13299
|
const projectId = getProjectId(c);
|
|
@@ -13321,6 +13549,7 @@ var autosreRoutesPlugin = {
|
|
|
13321
13549
|
const db = sqlDbFor(c.env);
|
|
13322
13550
|
const input = {
|
|
13323
13551
|
id: typeof body.id === "string" ? body.id : void 0,
|
|
13552
|
+
idempotencyKey: typeof body.idempotencyKey === "string" && body.idempotencyKey.trim() ? body.idempotencyKey.trim() : void 0,
|
|
13324
13553
|
environment: typeof body.environment === "string" ? body.environment : void 0,
|
|
13325
13554
|
incidentId: typeof body.incidentId === "string" ? body.incidentId : null,
|
|
13326
13555
|
fingerprintInput: body.fingerprintInput && typeof body.fingerprintInput === "object" ? body.fingerprintInput : void 0,
|
|
@@ -13341,6 +13570,7 @@ var autosreRoutesPlugin = {
|
|
|
13341
13570
|
citedEvidenceIds: Array.isArray(body.citedEvidenceIds) ? body.citedEvidenceIds.filter(
|
|
13342
13571
|
(id) => typeof id === "string"
|
|
13343
13572
|
) : [],
|
|
13573
|
+
codeReferences: Array.isArray(body.codeReferences) ? body.codeReferences : void 0,
|
|
13344
13574
|
agentRunId: typeof body.agentRunId === "string" ? body.agentRunId : null
|
|
13345
13575
|
};
|
|
13346
13576
|
const finding = await recordInvestigationFinding(db, projectId, input, {
|
|
@@ -13393,6 +13623,27 @@ var autosreRoutesPlugin = {
|
|
|
13393
13623
|
return c.json({ verified: false, error: message }, 400);
|
|
13394
13624
|
}
|
|
13395
13625
|
});
|
|
13626
|
+
app.get("/internal/evals/runs/:id", async (c) => {
|
|
13627
|
+
const projectId = getProjectId(c);
|
|
13628
|
+
const runId = c.req.param("id");
|
|
13629
|
+
const db = sqlDbFor(c.env);
|
|
13630
|
+
const row = await db.prepare(
|
|
13631
|
+
`SELECT id, eval_case_id, service_version, commit_sha,
|
|
13632
|
+
pass_count, fail_count
|
|
13633
|
+
FROM eval_runs WHERE project_id = ? AND id = ? LIMIT 1`
|
|
13634
|
+
).bind(projectId, runId).first();
|
|
13635
|
+
if (!row) return c.json({ error: "Eval run not found" }, 404);
|
|
13636
|
+
return c.json({
|
|
13637
|
+
run: {
|
|
13638
|
+
id: row.id,
|
|
13639
|
+
evalCaseId: row.eval_case_id,
|
|
13640
|
+
serviceVersion: row.service_version,
|
|
13641
|
+
commitSha: row.commit_sha,
|
|
13642
|
+
passCount: Number(row.pass_count ?? 0),
|
|
13643
|
+
failCount: Number(row.fail_count ?? 0)
|
|
13644
|
+
}
|
|
13645
|
+
});
|
|
13646
|
+
});
|
|
13396
13647
|
app.get("/internal/remediations", async (c) => {
|
|
13397
13648
|
const projectId = getProjectId(c);
|
|
13398
13649
|
const db = sqlDbFor(c.env);
|
|
@@ -13431,6 +13682,21 @@ var autosreRoutesPlugin = {
|
|
|
13431
13682
|
body.requestedAutonomy
|
|
13432
13683
|
) ? body.requestedAutonomy : "autonomous_write";
|
|
13433
13684
|
const db = sqlDbFor(c.env);
|
|
13685
|
+
const callbackUrl = typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
|
|
13686
|
+
if (callbackUrl) {
|
|
13687
|
+
try {
|
|
13688
|
+
assertSafeWebhookUrl(callbackUrl, {
|
|
13689
|
+
allowPrivateNetworks: c.env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true"
|
|
13690
|
+
});
|
|
13691
|
+
} catch (err) {
|
|
13692
|
+
return c.json(
|
|
13693
|
+
{
|
|
13694
|
+
error: `webhookUrl rejected: ${err instanceof Error ? err.message : String(err)}`
|
|
13695
|
+
},
|
|
13696
|
+
400
|
|
13697
|
+
);
|
|
13698
|
+
}
|
|
13699
|
+
}
|
|
13434
13700
|
const input = {
|
|
13435
13701
|
environment: typeof body.environment === "string" ? body.environment : void 0,
|
|
13436
13702
|
incidentId,
|
|
@@ -13444,9 +13710,13 @@ var autosreRoutesPlugin = {
|
|
|
13444
13710
|
};
|
|
13445
13711
|
try {
|
|
13446
13712
|
const proposal = await proposeRemediationAction(db, projectId, input, {
|
|
13447
|
-
logger: runtime.logger
|
|
13713
|
+
logger: runtime.logger,
|
|
13714
|
+
callback: callbackUrl ? {
|
|
13715
|
+
url: callbackUrl,
|
|
13716
|
+
secret: typeof body.secret === "string" && body.secret ? body.secret : void 0
|
|
13717
|
+
} : void 0
|
|
13448
13718
|
});
|
|
13449
|
-
const webhookDelivery = await deliverGrant(
|
|
13719
|
+
const { webhookDelivery, grantDelivery } = await deliverGrant(
|
|
13450
13720
|
c.env,
|
|
13451
13721
|
db,
|
|
13452
13722
|
projectId,
|
|
@@ -13458,6 +13728,7 @@ var autosreRoutesPlugin = {
|
|
|
13458
13728
|
proposal,
|
|
13459
13729
|
...proposal,
|
|
13460
13730
|
grantToken: proposal.grantToken ?? null,
|
|
13731
|
+
grantDelivery,
|
|
13461
13732
|
...webhookDelivery ? { webhookDelivery } : {}
|
|
13462
13733
|
},
|
|
13463
13734
|
201
|
|
@@ -13483,6 +13754,12 @@ var autosreRoutesPlugin = {
|
|
|
13483
13754
|
const approvedByUserId = sessionCheck.sessionActor;
|
|
13484
13755
|
const db = sqlDbFor(c.env);
|
|
13485
13756
|
try {
|
|
13757
|
+
const storedCallback = await getRemediationCallback(
|
|
13758
|
+
db,
|
|
13759
|
+
projectId,
|
|
13760
|
+
proposalId,
|
|
13761
|
+
environment
|
|
13762
|
+
);
|
|
13486
13763
|
const proposal = await approveRemediationProposal(
|
|
13487
13764
|
db,
|
|
13488
13765
|
projectId,
|
|
@@ -13492,17 +13769,19 @@ var autosreRoutesPlugin = {
|
|
|
13492
13769
|
approvedByUserId
|
|
13493
13770
|
}
|
|
13494
13771
|
);
|
|
13495
|
-
const webhookDelivery = await deliverGrant(
|
|
13772
|
+
const { webhookDelivery, grantDelivery } = await deliverGrant(
|
|
13496
13773
|
c.env,
|
|
13497
13774
|
db,
|
|
13498
13775
|
projectId,
|
|
13499
13776
|
body,
|
|
13500
|
-
proposal
|
|
13777
|
+
proposal,
|
|
13778
|
+
storedCallback
|
|
13501
13779
|
);
|
|
13502
13780
|
return c.json({
|
|
13503
13781
|
proposal,
|
|
13504
13782
|
...proposal,
|
|
13505
13783
|
grantToken: proposal.grantToken ?? null,
|
|
13784
|
+
grantDelivery,
|
|
13506
13785
|
...webhookDelivery ? { webhookDelivery } : {}
|
|
13507
13786
|
});
|
|
13508
13787
|
} catch (err) {
|
|
@@ -13994,7 +14273,9 @@ var linksFromAi = (calls, prefix) => {
|
|
|
13994
14273
|
label: prefix,
|
|
13995
14274
|
links: calls.map((c) => ({
|
|
13996
14275
|
label: `${c.provider} \xB7 ${c.modelName}`,
|
|
13997
|
-
href: `#/ai
|
|
14276
|
+
href: `#/ai/calls/${encodeURIComponent(c.callId)}`,
|
|
14277
|
+
entityKind: "ai_call",
|
|
14278
|
+
entityId: c.callId
|
|
13998
14279
|
}))
|
|
13999
14280
|
};
|
|
14000
14281
|
};
|
|
@@ -14067,7 +14348,7 @@ var linksFromToolCalls = (calls, prefix, metadataForAction) => {
|
|
|
14067
14348
|
links: [
|
|
14068
14349
|
{
|
|
14069
14350
|
label: `${calls.length} tool calls`,
|
|
14070
|
-
href: `#/
|
|
14351
|
+
href: `#/tool-calls`,
|
|
14071
14352
|
count: calls.length,
|
|
14072
14353
|
sample: truncate(calls[0].toolName),
|
|
14073
14354
|
entityKind: "tool_call",
|
|
@@ -14100,7 +14381,8 @@ var linksFromRetrievalEvents = (events, prefix) => {
|
|
|
14100
14381
|
label: prefix,
|
|
14101
14382
|
links: events.map((r) => ({
|
|
14102
14383
|
label: `retrieve: ${r.retrieverName} (${r.totalResults} docs)`,
|
|
14103
|
-
|
|
14384
|
+
// No dedicated retrieval tab: the owning action's detail page lists it.
|
|
14385
|
+
href: `#/actions/${encodeURIComponent(r.actionId)}`
|
|
14104
14386
|
}))
|
|
14105
14387
|
};
|
|
14106
14388
|
};
|
|
@@ -14116,7 +14398,9 @@ var linksFromEvalResults = (results, prefix, metadataForAction) => {
|
|
|
14116
14398
|
label: prefix,
|
|
14117
14399
|
links: results.map((e) => ({
|
|
14118
14400
|
label: `eval: ${e.evaluatorName} (${e.passed ? "passed" : "failed"})`,
|
|
14119
|
-
|
|
14401
|
+
// The Evaluations tab resolves this id to an eval case, falling back
|
|
14402
|
+
// to the case saved from the same action.
|
|
14403
|
+
href: `#/evaluations/${encodeURIComponent(e.id)}`,
|
|
14120
14404
|
entityKind: "eval",
|
|
14121
14405
|
entityId: e.id,
|
|
14122
14406
|
...metadataForAction?.(e.actionId)
|
|
@@ -14135,10 +14419,95 @@ var linksFromArtifacts = (arts, prefix) => {
|
|
|
14135
14419
|
label: prefix,
|
|
14136
14420
|
links: arts.map((a) => ({
|
|
14137
14421
|
label: `artifact: ${a.artifactName} (${a.artifactType})`,
|
|
14138
|
-
|
|
14422
|
+
// No dedicated artifact tab: the owning action's detail page lists it.
|
|
14423
|
+
href: `#/actions/${encodeURIComponent(a.actionId)}`
|
|
14139
14424
|
}))
|
|
14140
14425
|
};
|
|
14141
14426
|
};
|
|
14427
|
+
var hrefForEntity = (kind, id) => {
|
|
14428
|
+
const enc = encodeURIComponent;
|
|
14429
|
+
switch (kind) {
|
|
14430
|
+
case "trace":
|
|
14431
|
+
return `#/traces?trace=${enc(id)}`;
|
|
14432
|
+
case "span": {
|
|
14433
|
+
const [traceId, spanId] = id.split(":");
|
|
14434
|
+
return spanId && traceId ? `#/traces?trace=${enc(traceId)}&span=${enc(spanId)}` : `#/evidence?kind=span&id=${enc(id)}`;
|
|
14435
|
+
}
|
|
14436
|
+
case "alert":
|
|
14437
|
+
return `#/alerts?id=${enc(id)}`;
|
|
14438
|
+
case "analysis":
|
|
14439
|
+
return "#/health";
|
|
14440
|
+
case "service":
|
|
14441
|
+
return `#/service-map?service=${enc(id)}`;
|
|
14442
|
+
case "agent_run":
|
|
14443
|
+
return `#/agent-runs/${enc(id)}`;
|
|
14444
|
+
case "action":
|
|
14445
|
+
return `#/actions/${enc(id)}`;
|
|
14446
|
+
case "tool_call":
|
|
14447
|
+
return `#/tool-calls/${enc(id)}`;
|
|
14448
|
+
case "ai_call":
|
|
14449
|
+
return `#/ai/calls/${enc(id)}`;
|
|
14450
|
+
case "profile":
|
|
14451
|
+
return `#/profiles/${enc(id)}`;
|
|
14452
|
+
case "change":
|
|
14453
|
+
return `#/changes/${enc(id)}`;
|
|
14454
|
+
case "finding":
|
|
14455
|
+
return `#/findings/${enc(id)}`;
|
|
14456
|
+
case "incident":
|
|
14457
|
+
return `#/incidents/${enc(id)}`;
|
|
14458
|
+
case "eval":
|
|
14459
|
+
return `#/evaluations/${enc(id)}`;
|
|
14460
|
+
case "log":
|
|
14461
|
+
return `#/logs?id=${enc(id)}`;
|
|
14462
|
+
case "replay":
|
|
14463
|
+
return `#/replay?session=${enc(id)}`;
|
|
14464
|
+
default:
|
|
14465
|
+
return `#/evidence?kind=${enc(kind)}&id=${enc(id)}`;
|
|
14466
|
+
}
|
|
14467
|
+
};
|
|
14468
|
+
var HEX_SPAN_PAIR = /^[0-9a-f]{32}:[0-9a-f]{16}$/i;
|
|
14469
|
+
var HEX_SPAN_ID = /^[0-9a-f]{16}$/i;
|
|
14470
|
+
var NODE_KINDS = /* @__PURE__ */ new Set([
|
|
14471
|
+
"service",
|
|
14472
|
+
"endpoint",
|
|
14473
|
+
"database",
|
|
14474
|
+
"queue",
|
|
14475
|
+
"messaging_topic",
|
|
14476
|
+
"topic",
|
|
14477
|
+
"cache",
|
|
14478
|
+
"external"
|
|
14479
|
+
]);
|
|
14480
|
+
var resolveFaultLocusTarget = (faultLocusEntityId) => {
|
|
14481
|
+
const id = faultLocusEntityId;
|
|
14482
|
+
if (HEX_SPAN_PAIR.test(id)) {
|
|
14483
|
+
return {
|
|
14484
|
+
entityKind: "span",
|
|
14485
|
+
entityId: id,
|
|
14486
|
+
href: hrefForEntity("span", id)
|
|
14487
|
+
};
|
|
14488
|
+
}
|
|
14489
|
+
if (HEX_SPAN_ID.test(id)) {
|
|
14490
|
+
return {
|
|
14491
|
+
entityKind: "span",
|
|
14492
|
+
entityId: id,
|
|
14493
|
+
href: hrefForEntity("span", id)
|
|
14494
|
+
};
|
|
14495
|
+
}
|
|
14496
|
+
const segments = id.split(":");
|
|
14497
|
+
if (segments.length > 1 && NODE_KINDS.has(segments[0] ?? "")) {
|
|
14498
|
+
const name = segments[3] || segments[segments.length - 1] || id;
|
|
14499
|
+
return {
|
|
14500
|
+
entityKind: "service",
|
|
14501
|
+
entityId: id,
|
|
14502
|
+
href: hrefForEntity("service", name)
|
|
14503
|
+
};
|
|
14504
|
+
}
|
|
14505
|
+
return {
|
|
14506
|
+
entityKind: "service",
|
|
14507
|
+
entityId: id,
|
|
14508
|
+
href: hrefForEntity("service", id)
|
|
14509
|
+
};
|
|
14510
|
+
};
|
|
14142
14511
|
|
|
14143
14512
|
// src/plugins/connected-routes.ts
|
|
14144
14513
|
var emptySignalActionContext = () => ({
|
|
@@ -14936,10 +15305,11 @@ var connectedRoutesPlugin = {
|
|
|
14936
15305
|
}
|
|
14937
15306
|
];
|
|
14938
15307
|
const spans = changeRow.version ? await db.prepare(
|
|
14939
|
-
`SELECT
|
|
15308
|
+
`SELECT trace_id, span_id, service_name, span_name
|
|
14940
15309
|
FROM telemetry_spans
|
|
14941
15310
|
WHERE project_id = ? AND environment = ? AND service_version = ? AND status_code = 2
|
|
14942
|
-
|
|
15311
|
+
GROUP BY trace_id, span_id, service_name, span_name
|
|
15312
|
+
ORDER BY MAX(start_time) DESC LIMIT 5`
|
|
14943
15313
|
).bind(projectId, changeRow.environment, changeRow.version).all() : { results: [] };
|
|
14944
15314
|
manifest.across = [
|
|
14945
15315
|
{
|
|
@@ -15004,7 +15374,7 @@ var connectedRoutesPlugin = {
|
|
|
15004
15374
|
links: [
|
|
15005
15375
|
{
|
|
15006
15376
|
label: `fault locus: ${findingRow.faultLocusEntityId}`,
|
|
15007
|
-
|
|
15377
|
+
...resolveFaultLocusTarget(findingRow.faultLocusEntityId),
|
|
15008
15378
|
relationshipCertainty: "exact",
|
|
15009
15379
|
causalProofTier: proofTier
|
|
15010
15380
|
},
|
|
@@ -15052,8 +15422,11 @@ var connectedRoutesPlugin = {
|
|
|
15052
15422
|
links: [
|
|
15053
15423
|
{
|
|
15054
15424
|
label: `${incRow.symptomAnchorKind}: ${incRow.symptomAnchorId}`,
|
|
15055
|
-
href:
|
|
15056
|
-
|
|
15425
|
+
href: hrefForEntity(
|
|
15426
|
+
incRow.symptomAnchorKind,
|
|
15427
|
+
incRow.symptomAnchorId
|
|
15428
|
+
),
|
|
15429
|
+
entityKind: incRow.symptomAnchorKind,
|
|
15057
15430
|
entityId: incRow.symptomAnchorId,
|
|
15058
15431
|
relationshipCertainty: "exact"
|
|
15059
15432
|
}
|
|
@@ -16275,6 +16648,9 @@ var evalCasesRoutesPlugin = {
|
|
|
16275
16648
|
}
|
|
16276
16649
|
};
|
|
16277
16650
|
|
|
16651
|
+
// src/plugins/evidence-retrieval-routes.ts
|
|
16652
|
+
import { buildFindingFingerprint as buildFindingFingerprint2 } from "@obsunified/types";
|
|
16653
|
+
|
|
16278
16654
|
// src/lib/parse-pprof.ts
|
|
16279
16655
|
import {
|
|
16280
16656
|
decodePprof,
|
|
@@ -17648,6 +18024,30 @@ var attachAutoSreTriageContext = async (db, projectId, traceId, bundle, logger)
|
|
|
17648
18024
|
limit: 5
|
|
17649
18025
|
});
|
|
17650
18026
|
}
|
|
18027
|
+
if (faultLocus?.faultLocusSpan) {
|
|
18028
|
+
const spanFp = fingerprintInputFromSpan(
|
|
18029
|
+
faultLocus.faultLocusSpan,
|
|
18030
|
+
environment
|
|
18031
|
+
);
|
|
18032
|
+
const fingerprint = buildFindingFingerprint2({
|
|
18033
|
+
...spanFp,
|
|
18034
|
+
normalizedOperation: spanFp.normalizedOperation ?? "",
|
|
18035
|
+
errorSignature: spanFp.errorSignature ?? ""
|
|
18036
|
+
}).fingerprint;
|
|
18037
|
+
const byFingerprint = await searchInvestigationFindings(db, {
|
|
18038
|
+
projectId,
|
|
18039
|
+
environment,
|
|
18040
|
+
fingerprint,
|
|
18041
|
+
limit: 5
|
|
18042
|
+
});
|
|
18043
|
+
const seen = new Set(priorFindingsList.map((f) => f.id));
|
|
18044
|
+
for (const f of byFingerprint) {
|
|
18045
|
+
if (!seen.has(f.id) && priorFindingsList.length < 5) {
|
|
18046
|
+
priorFindingsList.push(f);
|
|
18047
|
+
seen.add(f.id);
|
|
18048
|
+
}
|
|
18049
|
+
}
|
|
18050
|
+
}
|
|
17651
18051
|
const priorFindings = priorFindingsList.map((f) => ({
|
|
17652
18052
|
findingId: f.id,
|
|
17653
18053
|
title: f.title,
|
|
@@ -22064,6 +22464,7 @@ function createDashboardAuth(config) {
|
|
|
22064
22464
|
const registerRoutes = (app) => {
|
|
22065
22465
|
const limiter = config?.loginRateLimit === false ? null : new LoginRateLimiter(config?.loginRateLimit ?? {});
|
|
22066
22466
|
let warnedUnkeyedClient = false;
|
|
22467
|
+
const isSecureRequest = (c, limitConfig) => c.req.url.startsWith("https:") || limitConfig?.trustProxyHeaders === true && c.req.header("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase() === "https";
|
|
22067
22468
|
app.post("/auth/login", async (c) => {
|
|
22068
22469
|
const configuredPassword = config?.password ?? c.env.DASHBOARD_PASSWORD;
|
|
22069
22470
|
if (!configuredPassword) {
|
|
@@ -22099,7 +22500,7 @@ function createDashboardAuth(config) {
|
|
|
22099
22500
|
const payload = createSessionToken(expiresAt);
|
|
22100
22501
|
const sig = await sign(payload, configuredPassword);
|
|
22101
22502
|
const cookieValue = `${payload}.${sig}`;
|
|
22102
|
-
const isSecure = c
|
|
22503
|
+
const isSecure = isSecureRequest(c, config?.loginRateLimit || void 0);
|
|
22103
22504
|
c.header(
|
|
22104
22505
|
"Set-Cookie",
|
|
22105
22506
|
`${SESSION_COOKIE}=${encodeURIComponent(cookieValue)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_MAX_AGE}${isSecure ? "; Secure" : ""}`
|
|
@@ -22147,7 +22548,7 @@ function createDashboardAuth(config) {
|
|
|
22147
22548
|
return c.json({ authenticated: true });
|
|
22148
22549
|
});
|
|
22149
22550
|
app.post("/auth/logout", (c) => {
|
|
22150
|
-
const isSecure = c
|
|
22551
|
+
const isSecure = isSecureRequest(c, config?.loginRateLimit || void 0);
|
|
22151
22552
|
c.header(
|
|
22152
22553
|
"Set-Cookie",
|
|
22153
22554
|
`${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0${isSecure ? "; Secure" : ""}`
|