@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.
Files changed (33) hide show
  1. package/dist/auth.js +1 -1
  2. package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
  3. package/dist/chunk-2FALTDCE.js.map +1 -0
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.js +497 -96
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -3
  8. package/src/__tests__/autosre-closed-loop.test.ts +590 -0
  9. package/src/auth/dashboard-auth.test.ts +61 -0
  10. package/src/auth/dashboard-auth.ts +17 -2
  11. package/src/lib/ai-store/calls.ts +43 -23
  12. package/src/lib/ai-store.ts +12 -1
  13. package/src/lib/autosre-findings.ts +169 -14
  14. package/src/lib/autosre-remediation-gate.ts +156 -26
  15. package/src/lib/projects-store.test.ts +38 -1
  16. package/src/lib/projects-store.ts +23 -4
  17. package/src/lib/sql-db-postgres.integration.test.ts +75 -2
  18. package/src/lib/usage-store.ts +9 -3
  19. package/src/migrations/052_remediation_callback.sql +10 -0
  20. package/src/migrations/053_remediation_callback_delivery.sql +7 -0
  21. package/src/migrations-postgres/052_remediation_callback.sql +10 -0
  22. package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
  23. package/src/plugins/ai-call-by-id.test.ts +73 -0
  24. package/src/plugins/ai-receiver.ts +10 -0
  25. package/src/plugins/autosre-routes.ts +167 -16
  26. package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
  27. package/src/plugins/connected-routes/manifest.test.ts +142 -0
  28. package/src/plugins/connected-routes/manifest.ts +127 -7
  29. package/src/plugins/connected-routes-links.test.ts +96 -0
  30. package/src/plugins/connected-routes.test.ts +3 -3
  31. package/src/plugins/connected-routes.ts +11 -8
  32. package/src/plugins/evidence-retrieval-routes.ts +30 -0
  33. 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-KMEZKGSO.js";
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((row) => ({
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 defaultOriginNode = input.faultLocusEntityId.includes(":") ? input.faultLocusEntityId : buildCanonicalNodeId2({
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: input.faultLocusEntityId
3208
+ serviceName: locusId
3209
+ }).id : locusId : buildCanonicalNodeId2({
3210
+ kind: "service",
3211
+ environment,
3212
+ serviceName: locusId
3123
3213
  }).id;
3124
- const fpResult = buildFindingFingerprint(
3125
- input.fingerprintInput ?? {
3126
- environment,
3127
- originatingNodeId: defaultOriginNode,
3128
- normalizedOperation: input.title,
3129
- errorSignature: input.summary.slice(0, 120),
3130
- downstreamTargetNodeId: null
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 { delivered: false, error: "MAX_DISPATCH_ATTEMPTS (5) exceeded" };
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
- try {
3951
- const res = await fetchFn(safeUrl.toString(), {
3952
- method: "POST",
3953
- headers,
3954
- body,
3955
- // Never follow redirects: a 3xx could re-aim the POST (and its
3956
- // signed body, which may carry a grant token) at another host.
3957
- redirect: "manual",
3958
- signal: AbortSignal.timeout(5e3)
3959
- });
3960
- status = res.status;
3961
- if (res.status >= 300 && res.status < 400) {
3962
- errorMsg = `Redirect (HTTP ${res.status}) refused`;
3963
- } else {
3964
- delivered = res.ok;
3965
- if (!res.ok) errorMsg = `HTTP ${res.status}`;
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
- } catch (err) {
3968
- errorMsg = err instanceof Error ? err.message : String(err);
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 = ue.project_id AND ref.session_id = ue.session_id ORDER BY occurred_at ASC LIMIT 1) AS referrer,
8129
- (SELECT page_path FROM usage_events lp WHERE lp.project_id = ue.project_id AND lp.session_id = ue.session_id ORDER BY occurred_at DESC LIMIT 1) AS last_path
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(options.projectId, cutoff, limit * 3).all();
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 webhookUrl = typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
13055
- if (!webhookUrl || !proposal.grantToken) return void 0;
13056
- if (proposal.status !== "grant_issued") return void 0;
13057
- return dispatchIncidentWebhook(db, projectId, proposal.incidentId, {
13058
- url: webhookUrl,
13059
- secret: typeof body.secret === "string" ? body.secret : void 0,
13060
- environment: proposal.environment,
13061
- allowPrivateNetworks: env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
13062
- logger: runtime.logger,
13063
- grant: {
13064
- proposalId: proposal.id,
13065
- status: proposal.status,
13066
- grantToken: proposal.grantToken
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?id=${c.callId}`
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: `#/tools`,
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
- href: `#/retrieval-events/${r.id}`
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
- href: `#/evals/${e.id}`,
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
- href: `#/artifacts/${a.id}`
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 DISTINCT trace_id, span_id, service_name, span_name
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
- ORDER BY start_time DESC LIMIT 5`
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
- href: `#/traces?q=${encodeURIComponent(findingRow.faultLocusEntityId)}`,
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: `#/alerts?id=${encodeURIComponent(incRow.symptomAnchorId)}`,
15056
- entityKind: incRow.symptomAnchorKind === "analysis" ? "analysis" : "alert",
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.req.url.startsWith("https:");
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.req.url.startsWith("https:");
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" : ""}`