@obsunified/collector 2.2.0 → 2.2.1
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/index.d.ts +53 -3
- package/dist/index.js +10459 -9696
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/autosre-closed-loop.test.ts +964 -17
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +68 -30
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +52 -28
- package/src/lib/autosre-remediation-gate.ts +799 -335
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/store/ingest.ts +8 -56
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +223 -108
- package/src/plugins/connected-routes/manifest.ts +1 -4
- package/src/plugins/connected-routes.ts +48 -78
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +372 -30
|
@@ -11,11 +11,18 @@ import type {
|
|
|
11
11
|
RemediationProposalStatus,
|
|
12
12
|
RemediationStateSpec,
|
|
13
13
|
} from "@obsunified/types";
|
|
14
|
+
import {
|
|
15
|
+
ChangeEventKindSchema,
|
|
16
|
+
EvidencePredicateSchema,
|
|
17
|
+
RemediationActionKindSchema,
|
|
18
|
+
} from "@obsunified/types";
|
|
14
19
|
import { AgentAutonomyLevel } from "@obsunified/types/constants";
|
|
15
20
|
import type { CollectorPlugin } from "../framework/collector";
|
|
21
|
+
import type { CollectorEnv } from "../framework/env";
|
|
16
22
|
import {
|
|
17
23
|
computeChangeBlastRadius,
|
|
18
24
|
getChangeEvent,
|
|
25
|
+
InvalidTimestampError,
|
|
19
26
|
listChangeEvents,
|
|
20
27
|
recordChangeEvent,
|
|
21
28
|
} from "../lib/autosre-change-correlation";
|
|
@@ -47,28 +54,16 @@ import {
|
|
|
47
54
|
RemediationGateError,
|
|
48
55
|
rejectRemediationProposal,
|
|
49
56
|
} from "../lib/autosre-remediation-gate";
|
|
57
|
+
import { sha256Hex } from "../lib/hash";
|
|
50
58
|
import { sqlDbFor } from "../lib/sql-db";
|
|
51
59
|
import { getProjectId } from "./_context";
|
|
52
60
|
import { readJsonObject } from "./_json";
|
|
53
61
|
|
|
54
|
-
const CHANGE_KINDS = new Set<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"prompt_rollout",
|
|
60
|
-
"agent_version",
|
|
61
|
-
"schema_migration",
|
|
62
|
-
"infra_scale",
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
const REMEDIATION_ACTION_KINDS = new Set<RemediationActionKind>([
|
|
66
|
-
"open_code_pr",
|
|
67
|
-
"toggle_feature_flag",
|
|
68
|
-
"rollback_deploy",
|
|
69
|
-
"apply_config",
|
|
70
|
-
"promote_agent_prompt",
|
|
71
|
-
]);
|
|
62
|
+
const CHANGE_KINDS = new Set<string>(ChangeEventKindSchema.options);
|
|
63
|
+
|
|
64
|
+
const REMEDIATION_ACTION_KINDS = new Set<string>(
|
|
65
|
+
RemediationActionKindSchema.options,
|
|
66
|
+
);
|
|
72
67
|
|
|
73
68
|
const parseLimit = (raw: string | undefined, fallback = 50): number => {
|
|
74
69
|
const parsed = Number.parseInt(raw ?? String(fallback), 10);
|
|
@@ -85,9 +80,11 @@ const isAgentAutonomyLevel = (value: unknown): value is AgentAutonomyLevel =>
|
|
|
85
80
|
* require an authenticated human dashboard session (`obs_session` cookie) and
|
|
86
81
|
* reject programmatic agent/MCP bearer tokens (`403 Forbidden`).
|
|
87
82
|
*/
|
|
88
|
-
function requireHumanDashboardSession(c: {
|
|
83
|
+
async function requireHumanDashboardSession(c: {
|
|
89
84
|
req: { header: (name: string) => string | undefined };
|
|
90
|
-
}):
|
|
85
|
+
}): Promise<
|
|
86
|
+
{ ok: true; sessionActor: string } | { ok: false; message: string }
|
|
87
|
+
> {
|
|
91
88
|
const authHeader = c.req.header("Authorization");
|
|
92
89
|
if (authHeader) {
|
|
93
90
|
return {
|
|
@@ -98,23 +95,69 @@ function requireHumanDashboardSession(c: {
|
|
|
98
95
|
}
|
|
99
96
|
const cookieHeader = c.req.header("Cookie") ?? "";
|
|
100
97
|
const match = cookieHeader.match(/(?:^|;\s*)obs_session=([^;]*)/);
|
|
101
|
-
if (!match
|
|
98
|
+
if (!match?.[1]) {
|
|
102
99
|
return {
|
|
103
100
|
ok: false,
|
|
104
101
|
message:
|
|
105
102
|
"Human approval requires an authenticated human dashboard session (obs_session cookie).",
|
|
106
103
|
};
|
|
107
104
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
// The recorded approver is derived from the session itself. Client-supplied
|
|
106
|
+
// identities (X-Obs-User-Actor, body.approvedByUserId) are never trusted:
|
|
107
|
+
// they would let any session holder attribute an approval to someone else.
|
|
108
|
+
// Only a hash prefix is stored so the cookie value never reaches the DB.
|
|
109
|
+
let rawSession = match[1];
|
|
110
|
+
try {
|
|
111
|
+
rawSession = decodeURIComponent(rawSession);
|
|
112
|
+
} catch {
|
|
113
|
+
// keep the raw value; it is only hashed
|
|
114
|
+
}
|
|
115
|
+
const sessionActor = `dashboard_session:${(await sha256Hex(rawSession)).slice(0, 16)}`;
|
|
112
116
|
return { ok: true, sessionActor };
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export const autosreRoutesPlugin: CollectorPlugin = {
|
|
116
120
|
name: "autosre-routes",
|
|
117
121
|
register(app, runtime) {
|
|
122
|
+
/**
|
|
123
|
+
* Grant handoff: the plaintext grant token exists only in the response of
|
|
124
|
+
* the request that mints it (propose with an autonomous grant, or human
|
|
125
|
+
* approve). Incidents do not store a webhook, so the caller may pass
|
|
126
|
+
* `webhookUrl` (+ optional `secret`) in that same request and the token is
|
|
127
|
+
* pushed to the runner over the SSRF-guarded, HMAC-signed incident webhook.
|
|
128
|
+
* Without a `webhookUrl` the token is simply returned in the response.
|
|
129
|
+
*/
|
|
130
|
+
const deliverGrant = async (
|
|
131
|
+
env: CollectorEnv | undefined,
|
|
132
|
+
db: ReturnType<typeof sqlDbFor>,
|
|
133
|
+
projectId: string,
|
|
134
|
+
body: Record<string, unknown>,
|
|
135
|
+
proposal: {
|
|
136
|
+
id: string;
|
|
137
|
+
incidentId: string;
|
|
138
|
+
environment: string;
|
|
139
|
+
status: string;
|
|
140
|
+
grantToken?: string;
|
|
141
|
+
},
|
|
142
|
+
) => {
|
|
143
|
+
const webhookUrl =
|
|
144
|
+
typeof body.webhookUrl === "string" ? body.webhookUrl.trim() : "";
|
|
145
|
+
if (!webhookUrl || !proposal.grantToken) return undefined;
|
|
146
|
+
if (proposal.status !== "grant_issued") return undefined;
|
|
147
|
+
return dispatchIncidentWebhook(db, projectId, proposal.incidentId, {
|
|
148
|
+
url: webhookUrl,
|
|
149
|
+
secret: typeof body.secret === "string" ? body.secret : undefined,
|
|
150
|
+
environment: proposal.environment,
|
|
151
|
+
allowPrivateNetworks: env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
152
|
+
logger: runtime.logger,
|
|
153
|
+
grant: {
|
|
154
|
+
proposalId: proposal.id,
|
|
155
|
+
status: proposal.status,
|
|
156
|
+
grantToken: proposal.grantToken,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
118
161
|
// 1. Change Ingestion (Ingest Auth /v1/changes)
|
|
119
162
|
app.post("/v1/changes", async (c) => {
|
|
120
163
|
const projectId = getProjectId(c);
|
|
@@ -122,7 +165,8 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
122
165
|
if (!body) {
|
|
123
166
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
124
167
|
}
|
|
125
|
-
const rawKind =
|
|
168
|
+
const rawKind =
|
|
169
|
+
typeof body.kind === "string" ? body.kind.trim() : "deploy";
|
|
126
170
|
const normalizedKind = (
|
|
127
171
|
rawKind === "agent_prompt_release" ? "prompt_rollout" : rawKind
|
|
128
172
|
) as ChangeEventKind;
|
|
@@ -181,47 +225,58 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
181
225
|
? body.actor
|
|
182
226
|
: null;
|
|
183
227
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
typeof body.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
let changeEvent: Awaited<ReturnType<typeof recordChangeEvent>>;
|
|
229
|
+
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
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err instanceof InvalidTimestampError) {
|
|
276
|
+
return c.json({ error: err.message }, 400);
|
|
277
|
+
}
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
225
280
|
|
|
226
281
|
return c.json({ changeEvent, change: changeEvent }, 201);
|
|
227
282
|
});
|
|
@@ -254,23 +309,27 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
254
309
|
const rawKind = c.req.query("kind");
|
|
255
310
|
const normalizedKind =
|
|
256
311
|
rawKind === "agent_prompt_release" ? "prompt_rollout" : rawKind;
|
|
257
|
-
if (
|
|
258
|
-
normalizedKind &&
|
|
259
|
-
!CHANGE_KINDS.has(normalizedKind as ChangeEventKind)
|
|
260
|
-
) {
|
|
312
|
+
if (normalizedKind && !CHANGE_KINDS.has(normalizedKind)) {
|
|
261
313
|
return c.json({ error: `Invalid kind: ${rawKind}` }, 400);
|
|
262
314
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
try {
|
|
316
|
+
const changes = await listChangeEvents(db, {
|
|
317
|
+
projectId,
|
|
318
|
+
environment: c.req.query("environment") || undefined,
|
|
319
|
+
serviceName:
|
|
320
|
+
c.req.query("serviceName") || c.req.query("service") || undefined,
|
|
321
|
+
targetNodeId: c.req.query("targetNodeId") || undefined,
|
|
322
|
+
kind: normalizedKind || undefined,
|
|
323
|
+
since: c.req.query("since") || undefined,
|
|
324
|
+
limit: parseLimit(c.req.query("limit"), 50),
|
|
325
|
+
});
|
|
326
|
+
return c.json({ changes });
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (err instanceof InvalidTimestampError) {
|
|
329
|
+
return c.json({ error: err.message }, 400);
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
274
333
|
});
|
|
275
334
|
|
|
276
335
|
app.get("/internal/changes/:id/blast-radius", async (c) => {
|
|
@@ -394,10 +453,13 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
394
453
|
typeof body.leaseDurationSeconds === "number" &&
|
|
395
454
|
Number.isFinite(body.leaseDurationSeconds)
|
|
396
455
|
? body.leaseDurationSeconds
|
|
397
|
-
: typeof body.
|
|
398
|
-
Number.isFinite(body.
|
|
399
|
-
?
|
|
400
|
-
:
|
|
456
|
+
: typeof body.ttlSeconds === "number" &&
|
|
457
|
+
Number.isFinite(body.ttlSeconds)
|
|
458
|
+
? body.ttlSeconds
|
|
459
|
+
: typeof body.leaseDurationMs === "number" &&
|
|
460
|
+
Number.isFinite(body.leaseDurationMs)
|
|
461
|
+
? Math.ceil(body.leaseDurationMs / 1000)
|
|
462
|
+
: undefined;
|
|
401
463
|
const result = await acquireIncidentLease(db, projectId, incidentId, {
|
|
402
464
|
environment:
|
|
403
465
|
typeof body.environment === "string" ? body.environment : undefined,
|
|
@@ -424,9 +486,12 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
424
486
|
const result = await dispatchIncidentWebhook(db, projectId, incidentId, {
|
|
425
487
|
url: webhookUrl,
|
|
426
488
|
secret: typeof body?.secret === "string" ? body.secret : undefined,
|
|
427
|
-
|
|
428
|
-
body?.
|
|
429
|
-
|
|
489
|
+
environment:
|
|
490
|
+
typeof body?.environment === "string" ? body.environment : undefined,
|
|
491
|
+
// Operator-level setting only: a request body must never be able to
|
|
492
|
+
// turn off the SSRF guard.
|
|
493
|
+
allowPrivateNetworks: c.env?.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
494
|
+
logger: runtime.logger,
|
|
430
495
|
});
|
|
431
496
|
return c.json(result, result.delivered ? 200 : 502);
|
|
432
497
|
});
|
|
@@ -497,6 +562,26 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
497
562
|
);
|
|
498
563
|
}
|
|
499
564
|
|
|
565
|
+
if (body.causalChain !== undefined && !Array.isArray(body.causalChain)) {
|
|
566
|
+
return c.json({ error: "causalChain must be an array" }, 400);
|
|
567
|
+
}
|
|
568
|
+
for (const [i, triple] of (
|
|
569
|
+
(body.causalChain as unknown[] | undefined) ?? []
|
|
570
|
+
).entries()) {
|
|
571
|
+
const predicate =
|
|
572
|
+
triple && typeof triple === "object"
|
|
573
|
+
? (triple as Record<string, unknown>).predicate
|
|
574
|
+
: undefined;
|
|
575
|
+
if (!EvidencePredicateSchema.safeParse(predicate).success) {
|
|
576
|
+
return c.json(
|
|
577
|
+
{
|
|
578
|
+
error: `causalChain[${i}].predicate is not a known evidence predicate: ${String(predicate)}`,
|
|
579
|
+
},
|
|
580
|
+
400,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
500
585
|
const db = sqlDbFor(c.env);
|
|
501
586
|
const input: InvestigationFindingInput = {
|
|
502
587
|
id: typeof body.id === "string" ? body.id : undefined,
|
|
@@ -552,7 +637,9 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
552
637
|
typeof body.agentRunId === "string" ? body.agentRunId : null,
|
|
553
638
|
};
|
|
554
639
|
|
|
555
|
-
const finding = await recordInvestigationFinding(db, projectId, input
|
|
640
|
+
const finding = await recordInvestigationFinding(db, projectId, input, {
|
|
641
|
+
logger: runtime.logger,
|
|
642
|
+
});
|
|
556
643
|
return c.json({ finding, id: finding.id }, 201);
|
|
557
644
|
});
|
|
558
645
|
|
|
@@ -701,12 +788,22 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
701
788
|
};
|
|
702
789
|
|
|
703
790
|
try {
|
|
704
|
-
const proposal = await proposeRemediationAction(db, projectId, input
|
|
791
|
+
const proposal = await proposeRemediationAction(db, projectId, input, {
|
|
792
|
+
logger: runtime.logger,
|
|
793
|
+
});
|
|
794
|
+
const webhookDelivery = await deliverGrant(
|
|
795
|
+
c.env,
|
|
796
|
+
db,
|
|
797
|
+
projectId,
|
|
798
|
+
body,
|
|
799
|
+
proposal,
|
|
800
|
+
);
|
|
705
801
|
return c.json(
|
|
706
802
|
{
|
|
707
803
|
proposal,
|
|
708
804
|
...proposal,
|
|
709
805
|
grantToken: proposal.grantToken ?? null,
|
|
806
|
+
...(webhookDelivery ? { webhookDelivery } : {}),
|
|
710
807
|
},
|
|
711
808
|
201,
|
|
712
809
|
);
|
|
@@ -721,7 +818,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
721
818
|
});
|
|
722
819
|
|
|
723
820
|
app.post("/internal/remediations/:id/approve", async (c) => {
|
|
724
|
-
const sessionCheck = requireHumanDashboardSession(c);
|
|
821
|
+
const sessionCheck = await requireHumanDashboardSession(c);
|
|
725
822
|
if (!sessionCheck.ok) {
|
|
726
823
|
return c.json(
|
|
727
824
|
{ error: "Forbidden", message: sessionCheck.message },
|
|
@@ -735,14 +832,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
735
832
|
{};
|
|
736
833
|
const environment =
|
|
737
834
|
typeof body.environment === "string" ? body.environment : undefined;
|
|
738
|
-
const approvedByUserId =
|
|
739
|
-
typeof body.approvedByUserId === "string" &&
|
|
740
|
-
body.approvedByUserId.trim()
|
|
741
|
-
? body.approvedByUserId.trim()
|
|
742
|
-
: typeof body.approvedBySession === "string" &&
|
|
743
|
-
body.approvedBySession.trim()
|
|
744
|
-
? body.approvedBySession.trim()
|
|
745
|
-
: sessionCheck.sessionActor;
|
|
835
|
+
const approvedByUserId = sessionCheck.sessionActor;
|
|
746
836
|
|
|
747
837
|
const db = sqlDbFor(c.env);
|
|
748
838
|
try {
|
|
@@ -755,10 +845,18 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
755
845
|
approvedByUserId,
|
|
756
846
|
},
|
|
757
847
|
);
|
|
848
|
+
const webhookDelivery = await deliverGrant(
|
|
849
|
+
c.env,
|
|
850
|
+
db,
|
|
851
|
+
projectId,
|
|
852
|
+
body,
|
|
853
|
+
proposal,
|
|
854
|
+
);
|
|
758
855
|
return c.json({
|
|
759
856
|
proposal,
|
|
760
857
|
...proposal,
|
|
761
858
|
grantToken: proposal.grantToken ?? null,
|
|
859
|
+
...(webhookDelivery ? { webhookDelivery } : {}),
|
|
762
860
|
});
|
|
763
861
|
} catch (err) {
|
|
764
862
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -771,7 +869,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
771
869
|
});
|
|
772
870
|
|
|
773
871
|
app.post("/internal/remediations/:id/reject", async (c) => {
|
|
774
|
-
const sessionCheck = requireHumanDashboardSession(c);
|
|
872
|
+
const sessionCheck = await requireHumanDashboardSession(c);
|
|
775
873
|
if (!sessionCheck.ok) {
|
|
776
874
|
return c.json(
|
|
777
875
|
{ error: "Forbidden", message: sessionCheck.message },
|
|
@@ -785,8 +883,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
785
883
|
{};
|
|
786
884
|
const environment =
|
|
787
885
|
typeof body.environment === "string" ? body.environment : undefined;
|
|
788
|
-
const reason =
|
|
789
|
-
typeof body.reason === "string" ? body.reason : undefined;
|
|
886
|
+
const reason = typeof body.reason === "string" ? body.reason : undefined;
|
|
790
887
|
|
|
791
888
|
const db = sqlDbFor(c.env);
|
|
792
889
|
try {
|
|
@@ -837,6 +934,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
837
934
|
grantToken,
|
|
838
935
|
runnerId:
|
|
839
936
|
typeof body.runnerId === "string" ? body.runnerId : undefined,
|
|
937
|
+
logger: runtime.logger,
|
|
840
938
|
},
|
|
841
939
|
);
|
|
842
940
|
return c.json({ claimed: true, proposal, ...proposal });
|
|
@@ -846,7 +944,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
846
944
|
err instanceof RemediationGateError
|
|
847
945
|
? (err.statusCode as 400 | 403 | 404 | 409 | 410)
|
|
848
946
|
: 409;
|
|
849
|
-
return c.json(
|
|
947
|
+
return c.json(
|
|
948
|
+
{ claimed: false, error: message, reason: message },
|
|
949
|
+
status,
|
|
950
|
+
);
|
|
850
951
|
}
|
|
851
952
|
});
|
|
852
953
|
|
|
@@ -856,6 +957,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
856
957
|
const body =
|
|
857
958
|
(await readJsonObject<Record<string, unknown>>(c).catch(() => null)) ??
|
|
858
959
|
{};
|
|
960
|
+
const runnerId =
|
|
961
|
+
typeof body.runnerId === "string" && body.runnerId.trim()
|
|
962
|
+
? body.runnerId.trim()
|
|
963
|
+
: "autosre-runner";
|
|
859
964
|
const db = sqlDbFor(c.env);
|
|
860
965
|
try {
|
|
861
966
|
const proposal = await completeRemediationProposal(
|
|
@@ -867,6 +972,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
867
972
|
typeof body.environment === "string"
|
|
868
973
|
? body.environment
|
|
869
974
|
: undefined,
|
|
975
|
+
runnerId,
|
|
870
976
|
resultingChangeId:
|
|
871
977
|
typeof body.resultingChangeId === "string"
|
|
872
978
|
? body.resultingChangeId
|
|
@@ -894,6 +1000,10 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
894
1000
|
const body =
|
|
895
1001
|
(await readJsonObject<Record<string, unknown>>(c).catch(() => null)) ??
|
|
896
1002
|
{};
|
|
1003
|
+
const runnerId =
|
|
1004
|
+
typeof body.runnerId === "string" && body.runnerId.trim()
|
|
1005
|
+
? body.runnerId.trim()
|
|
1006
|
+
: "autosre-runner";
|
|
897
1007
|
const db = sqlDbFor(c.env);
|
|
898
1008
|
try {
|
|
899
1009
|
const proposal = await failRemediationProposal(
|
|
@@ -905,6 +1015,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
905
1015
|
typeof body.environment === "string"
|
|
906
1016
|
? body.environment
|
|
907
1017
|
: undefined,
|
|
1018
|
+
runnerId,
|
|
908
1019
|
failureReason:
|
|
909
1020
|
typeof body.failureReason === "string"
|
|
910
1021
|
? body.failureReason
|
|
@@ -916,7 +1027,7 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
916
1027
|
const message = err instanceof Error ? err.message : String(err);
|
|
917
1028
|
const status =
|
|
918
1029
|
err instanceof RemediationGateError
|
|
919
|
-
? (err.statusCode as 400 | 404)
|
|
1030
|
+
? (err.statusCode as 400 | 404 | 409)
|
|
920
1031
|
: 400;
|
|
921
1032
|
return c.json({ error: message }, status);
|
|
922
1033
|
}
|
|
@@ -927,12 +1038,16 @@ export const autosreRoutesPlugin: CollectorPlugin = {
|
|
|
927
1038
|
const body =
|
|
928
1039
|
(await readJsonObject<Record<string, unknown>>(c).catch(() => null)) ??
|
|
929
1040
|
{};
|
|
1041
|
+
// Scoped to the caller's project; the clock is never client-controlled
|
|
1042
|
+
// and the traffic floor can only be raised, never lowered, by a caller.
|
|
930
1043
|
const summary = await evaluateRemediationWatchdogs(db, {
|
|
931
|
-
|
|
1044
|
+
projectId: getProjectId(c),
|
|
932
1045
|
minComparableRequests:
|
|
933
|
-
typeof body.minComparableRequests === "number"
|
|
934
|
-
|
|
1046
|
+
typeof body.minComparableRequests === "number" &&
|
|
1047
|
+
Number.isFinite(body.minComparableRequests)
|
|
1048
|
+
? Math.max(50, body.minComparableRequests)
|
|
935
1049
|
: undefined,
|
|
1050
|
+
logger: runtime.logger,
|
|
936
1051
|
});
|
|
937
1052
|
return c.json({ summary, ...summary });
|
|
938
1053
|
});
|