@sema-agent/server 1.313.0 → 1.315.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/auth-bridge.d.ts +3 -2
- package/dist/bench/s1/live-deps.js +2 -2
- package/dist/budget.d.ts +4 -76
- package/dist/budget.js +4 -83
- package/dist/capabilities/sandbox-file-send.d.ts +2 -1
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/config-types.d.ts +1 -1
- package/dist/elicitation.d.ts +5 -3
- package/dist/elicitation.js +3 -2
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/fleet-lease.js +1 -1
- package/dist/hooks/hook-llm.d.ts +5 -4
- package/dist/hooks/hook-runner.d.ts +1 -1
- package/dist/hooks/hook-runner.js +19 -10
- package/dist/http/server.d.ts +95 -69
- package/dist/http/server.js +75 -86
- package/dist/index.d.ts +1 -1
- package/dist/key-resolver.d.ts +1 -1
- package/dist/key-resolver.js +1 -1
- package/dist/leader/grader-env-factory.d.ts +1 -1
- package/dist/leader/grader-env-factory.js +2 -2
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +1 -1
- package/dist/leader/wire.js +170 -166
- package/dist/main.js +487 -476
- package/dist/memory-scope.d.ts +18 -0
- package/dist/memory-scope.js +38 -0
- package/dist/observability/cost-taxonomy.d.ts +34 -0
- package/dist/observability/cost-taxonomy.js +26 -0
- package/dist/observability/prompt-manifest.d.ts +75 -0
- package/dist/observability/prompt-manifest.js +82 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/web-search.d.ts +4 -3
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/question.d.ts +2 -1
- package/dist/question.js +3 -2
- package/dist/run-local.js +2 -2
- package/dist/runtime-caps-resolver.d.ts +7 -7
- package/dist/runtime-caps-resolver.js +3 -3
- package/dist/security.d.ts +4 -74
- package/dist/security.js +6 -155
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/dist/tool-approval.d.ts +2 -1
- package/dist/tool-approval.js +3 -2
- package/package.json +1 -1
|
@@ -1,350 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
|
|
4
|
-
import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-contracts.js";
|
|
1
|
+
export { TiDBImageBake, SqlImageBake } from "./image-bake-store-sql.js";
|
|
5
2
|
export { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS } from "./store-contracts.js";
|
|
6
|
-
function isDupKey(err) {
|
|
7
|
-
return Boolean(err) && err.code === "ER_DUP_ENTRY";
|
|
8
|
-
}
|
|
9
|
-
function dupKeyName(err) {
|
|
10
|
-
const msg = String(err?.sqlMessage ?? err?.message ?? "");
|
|
11
|
-
if (/uq_line/i.test(msg))
|
|
12
|
-
return "uq_line";
|
|
13
|
-
if (/PRIMARY/i.test(msg))
|
|
14
|
-
return "primary";
|
|
15
|
-
return "other";
|
|
16
|
-
}
|
|
17
|
-
export class TiDBImageBake {
|
|
18
|
-
pool;
|
|
19
|
-
poolName;
|
|
20
|
-
constructor(pool, poolName = "standard") {
|
|
21
|
-
this.pool = pool;
|
|
22
|
-
this.poolName = poolName;
|
|
23
|
-
}
|
|
24
|
-
async createBake(input) {
|
|
25
|
-
const bakeId = uuidv7();
|
|
26
|
-
const now = new Date();
|
|
27
|
-
try {
|
|
28
|
-
await this.pool.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
29
|
-
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
30
|
-
"VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", [
|
|
31
|
-
bakeId,
|
|
32
|
-
input.profile,
|
|
33
|
-
input.bands == null ? null : JSON.stringify(input.bands),
|
|
34
|
-
input.baseRef,
|
|
35
|
-
input.push ? 1 : 0,
|
|
36
|
-
input.dryRun ? 1 : 0,
|
|
37
|
-
input.logs ? 1 : 0,
|
|
38
|
-
JSON.stringify(input.argv),
|
|
39
|
-
input.idemKey,
|
|
40
|
-
input.requestedBy,
|
|
41
|
-
now,
|
|
42
|
-
now,
|
|
43
|
-
]);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
if (isDupKey(err) && input.idemKey) {
|
|
47
|
-
const existing = await this.getBakeByIdem(input.idemKey);
|
|
48
|
-
if (existing)
|
|
49
|
-
return { created: false, bake: existing };
|
|
50
|
-
}
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
const bake = await this.getBake(bakeId);
|
|
54
|
-
if (!bake)
|
|
55
|
-
throw new Error("createBake: row vanished after insert");
|
|
56
|
-
return { created: true, bake };
|
|
57
|
-
}
|
|
58
|
-
async createBakeIfIdle(input) {
|
|
59
|
-
const bakeId = uuidv7();
|
|
60
|
-
const now = new Date();
|
|
61
|
-
const conn = await this.pool.getConnection();
|
|
62
|
-
let inserted = false;
|
|
63
|
-
try {
|
|
64
|
-
await conn.query("BEGIN PESSIMISTIC");
|
|
65
|
-
await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES (?)", [this.poolName]);
|
|
66
|
-
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
|
|
67
|
-
const [busyRows] = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
|
|
68
|
-
if (busyRows.length === 0) {
|
|
69
|
-
await conn.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
70
|
-
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
71
|
-
"VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", [
|
|
72
|
-
bakeId,
|
|
73
|
-
input.profile,
|
|
74
|
-
input.bands == null ? null : JSON.stringify(input.bands),
|
|
75
|
-
input.baseRef,
|
|
76
|
-
input.push ? 1 : 0,
|
|
77
|
-
input.dryRun ? 1 : 0,
|
|
78
|
-
input.logs ? 1 : 0,
|
|
79
|
-
JSON.stringify(input.argv),
|
|
80
|
-
input.idemKey,
|
|
81
|
-
input.requestedBy,
|
|
82
|
-
now,
|
|
83
|
-
now,
|
|
84
|
-
]);
|
|
85
|
-
inserted = true;
|
|
86
|
-
}
|
|
87
|
-
await conn.commit();
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
try {
|
|
91
|
-
await conn.rollback();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
conn.release();
|
|
99
|
-
}
|
|
100
|
-
if (!inserted)
|
|
101
|
-
return { created: false, bake: null };
|
|
102
|
-
const bake = await this.getBake(bakeId);
|
|
103
|
-
if (!bake)
|
|
104
|
-
throw new Error("createBakeIfIdle: row vanished after insert");
|
|
105
|
-
return { created: true, bake };
|
|
106
|
-
}
|
|
107
|
-
async getBake(bakeId) {
|
|
108
|
-
const [rows] = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE bake_id = ? LIMIT 1`, [bakeId]);
|
|
109
|
-
return rows[0] ? mapRow(rows[0]) : null;
|
|
110
|
-
}
|
|
111
|
-
async getBakeByIdem(idemKey) {
|
|
112
|
-
const [rows] = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = ? LIMIT 1`, [idemKey]);
|
|
113
|
-
return rows[0] ? mapRow(rows[0]) : null;
|
|
114
|
-
}
|
|
115
|
-
async findActiveByArgv(argv) {
|
|
116
|
-
const [rows] = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = CAST(? AS JSON) ` +
|
|
117
|
-
"ORDER BY created_at DESC LIMIT 1", [JSON.stringify(argv)]);
|
|
118
|
-
return rows[0] ? mapRow(rows[0]) : null;
|
|
119
|
-
}
|
|
120
|
-
async findActiveAny() {
|
|
121
|
-
const [rows] = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
|
|
122
|
-
"ORDER BY created_at ASC LIMIT 1", []);
|
|
123
|
-
return rows[0] ? mapRow(rows[0]) : null;
|
|
124
|
-
}
|
|
125
|
-
async findNextQueuedId() {
|
|
126
|
-
const [rows] = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1", []);
|
|
127
|
-
return rows[0] ? String(rows[0].bake_id) : null;
|
|
128
|
-
}
|
|
129
|
-
async claimBake(bakeId, runnerId, leaseMs) {
|
|
130
|
-
const conn = await this.pool.getConnection();
|
|
131
|
-
try {
|
|
132
|
-
await conn.beginTransaction();
|
|
133
|
-
const now = new Date();
|
|
134
|
-
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
135
|
-
const ingestSecret = randomBytes(24).toString("hex");
|
|
136
|
-
const [bakeRes] = await conn.query("UPDATE image_bake SET status='running', state='PENDING', runner_id=?, ingest_secret=?, " +
|
|
137
|
-
"lease_until=?, updated_at=? WHERE bake_id=? AND status='queued'", [runnerId, ingestSecret, leaseUntil, now, bakeId]);
|
|
138
|
-
if (bakeRes.affectedRows !== 1) {
|
|
139
|
-
await conn.rollback();
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const [dryRows] = await conn.query("SELECT dry_run FROM image_bake WHERE bake_id=?", [bakeId]);
|
|
143
|
-
if (Boolean(dryRows[0]?.dry_run)) {
|
|
144
|
-
await conn.commit();
|
|
145
|
-
return (await this.getBake(bakeId)) ?? null;
|
|
146
|
-
}
|
|
147
|
-
const [leaseRows] = await conn.query("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=?", [this.poolName]);
|
|
148
|
-
const cur = leaseRows[0];
|
|
149
|
-
if (!cur) {
|
|
150
|
-
try {
|
|
151
|
-
await conn.query("INSERT INTO image_bake_lease (pool, bake_id, acquired_at, expires_at) VALUES (?,?,?,?)", [this.poolName, bakeId, now, leaseUntil]);
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (isDupKey(err)) {
|
|
155
|
-
await conn.rollback();
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
throw err;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
if (new Date(cur.expires_at).getTime() >= now.getTime()) {
|
|
163
|
-
await conn.rollback();
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const holderId = String(cur.bake_id ?? "");
|
|
167
|
-
const [holderRows] = await conn.query("SELECT status FROM image_bake WHERE bake_id=?", [holderId]);
|
|
168
|
-
const holderStatus = String(holderRows[0]?.status ?? "");
|
|
169
|
-
if (holderStatus === "queued" || holderStatus === "running") {
|
|
170
|
-
await conn.rollback();
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const [stealRes] = await conn.query("UPDATE image_bake_lease SET bake_id=?, acquired_at=?, expires_at=? WHERE pool=? AND bake_id=? AND expires_at=?", [bakeId, now, leaseUntil, this.poolName, holderId, cur.expires_at]);
|
|
174
|
-
if (stealRes.affectedRows === 0) {
|
|
175
|
-
await conn.rollback();
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const [check] = await conn.query("SELECT bake_id FROM image_bake_lease WHERE pool=?", [this.poolName]);
|
|
180
|
-
if (String(check[0]?.bake_id ?? "") !== bakeId) {
|
|
181
|
-
await conn.rollback();
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
await conn.commit();
|
|
185
|
-
return (await this.getBake(bakeId)) ?? null;
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
try {
|
|
189
|
-
await conn.rollback();
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
}
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
conn.release();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async appendEvent(bakeId, kind, data, opts = {}) {
|
|
200
|
-
const lineOrd = opts.lineOrd ?? null;
|
|
201
|
-
if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
|
|
202
|
-
return null;
|
|
203
|
-
const payload = data == null ? null : JSON.stringify(data);
|
|
204
|
-
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
205
|
-
const seq = (await this.maxSeq(bakeId)) + 1;
|
|
206
|
-
try {
|
|
207
|
-
await this.pool.query("INSERT INTO image_bake_event (bake_id, seq, line_ord, kind, level, data, ts) VALUES (?,?,?,?,?,?,?)", [bakeId, seq, lineOrd, kind, opts.level ?? null, payload, new Date()]);
|
|
208
|
-
return seq;
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
if (!isDupKey(err))
|
|
212
|
-
throw err;
|
|
213
|
-
const which = dupKeyName(err);
|
|
214
|
-
if (which === "uq_line" && lineOrd !== null)
|
|
215
|
-
return null;
|
|
216
|
-
await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
throw new Error(`appendEvent: seq allocation lost the (bake_id,seq) race after retries (bakeId=${bakeId})`);
|
|
220
|
-
}
|
|
221
|
-
async lineOrdExists(bakeId, lineOrd) {
|
|
222
|
-
const [rows] = await this.pool.query("SELECT 1 FROM image_bake_event WHERE bake_id = ? AND line_ord = ? LIMIT 1", [bakeId, lineOrd]);
|
|
223
|
-
return rows.length > 0;
|
|
224
|
-
}
|
|
225
|
-
async maxSeq(bakeId) {
|
|
226
|
-
const [rows] = await this.pool.query("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = ?", [bakeId]);
|
|
227
|
-
const m = rows[0]?.m;
|
|
228
|
-
return m == null ? 0 : Number(m);
|
|
229
|
-
}
|
|
230
|
-
async retainedFrom(bakeId) {
|
|
231
|
-
const [rows] = await this.pool.query("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = ?", [bakeId]);
|
|
232
|
-
const m = rows[0]?.m;
|
|
233
|
-
return m == null ? 0 : Number(m);
|
|
234
|
-
}
|
|
235
|
-
async getEvents(bakeId, afterSeq) {
|
|
236
|
-
const [rows] = await this.pool.query("SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = ? AND seq > ? ORDER BY seq ASC", [bakeId, afterSeq]);
|
|
237
|
-
return rows.map((r) => ({
|
|
238
|
-
seq: Number(r.seq),
|
|
239
|
-
lineOrd: r.line_ord === null ? null : Number(r.line_ord),
|
|
240
|
-
kind: String(r.kind),
|
|
241
|
-
level: r.level ?? null,
|
|
242
|
-
data: parseJson(r.data, null),
|
|
243
|
-
ts: iso(r.ts),
|
|
244
|
-
}));
|
|
245
|
-
}
|
|
246
|
-
async setStatus(bakeId, status) {
|
|
247
|
-
await this.pool.query("UPDATE image_bake SET status = ?, updated_at = ? WHERE bake_id = ?", [status, new Date(), bakeId]);
|
|
248
|
-
}
|
|
249
|
-
async setState(bakeId, state) {
|
|
250
|
-
await this.pool.query("UPDATE image_bake SET state = ?, updated_at = ? WHERE bake_id = ?", [state, new Date(), bakeId]);
|
|
251
|
-
}
|
|
252
|
-
async setTerminal(bakeId, t) {
|
|
253
|
-
let affectedRows = 0;
|
|
254
|
-
await this.tx(async (conn) => {
|
|
255
|
-
const [res] = await conn.query("UPDATE image_bake SET status=?, state=?, digest=?, repo=?, ref=?, index_id=?, exit_code=?, error=?, " +
|
|
256
|
-
"error_code=?, manifest_sha=?, tag=?, ingest_secret=NULL, updated_at=? WHERE bake_id=? AND status IN ('queued','running')", [
|
|
257
|
-
t.status,
|
|
258
|
-
t.state,
|
|
259
|
-
t.digest ?? null,
|
|
260
|
-
t.repo ?? null,
|
|
261
|
-
t.ref ?? null,
|
|
262
|
-
t.indexId ?? null,
|
|
263
|
-
t.exitCode ?? null,
|
|
264
|
-
t.error ?? null,
|
|
265
|
-
t.errorCode ?? null,
|
|
266
|
-
t.manifestSha ?? null,
|
|
267
|
-
t.tag ?? null,
|
|
268
|
-
new Date(),
|
|
269
|
-
bakeId,
|
|
270
|
-
]);
|
|
271
|
-
affectedRows = res.affectedRows;
|
|
272
|
-
await conn.query("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", [this.poolName, bakeId]);
|
|
273
|
-
});
|
|
274
|
-
return affectedRows;
|
|
275
|
-
}
|
|
276
|
-
async setIndexId(bakeId, indexId) {
|
|
277
|
-
const [res] = await this.pool.query("UPDATE image_bake SET index_id = ?, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", [indexId, new Date(), bakeId]);
|
|
278
|
-
return res.affectedRows >= 1;
|
|
279
|
-
}
|
|
280
|
-
async heartbeatLease(bakeId, runnerId, leaseMs) {
|
|
281
|
-
const now = new Date();
|
|
282
|
-
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
283
|
-
const [res] = await this.pool.query("UPDATE image_bake SET lease_until = ?, updated_at = ? WHERE bake_id = ? AND runner_id = ? AND status = 'running'", [leaseUntil, now, bakeId, runnerId]);
|
|
284
|
-
if (res.affectedRows < 1)
|
|
285
|
-
return false;
|
|
286
|
-
const [dryRows] = await this.pool.query("SELECT dry_run FROM image_bake WHERE bake_id = ?", [bakeId]);
|
|
287
|
-
if (Boolean(dryRows[0]?.dry_run))
|
|
288
|
-
return true;
|
|
289
|
-
const [leaseRes] = await this.pool.query("UPDATE image_bake_lease SET expires_at = ? WHERE pool = ? AND bake_id = ?", [leaseUntil, this.poolName, bakeId]);
|
|
290
|
-
return leaseRes.affectedRows >= 1;
|
|
291
|
-
}
|
|
292
|
-
async requestCancel(bakeId) {
|
|
293
|
-
const [r] = await this.pool.query("UPDATE image_bake SET cancel_requested = 1, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", [new Date(), bakeId]);
|
|
294
|
-
return r.affectedRows > 0;
|
|
295
|
-
}
|
|
296
|
-
async isCancelRequested(bakeId) {
|
|
297
|
-
const [rows] = await this.pool.query("SELECT cancel_requested FROM image_bake WHERE bake_id = ?", [bakeId]);
|
|
298
|
-
return Boolean(rows[0]?.cancel_requested);
|
|
299
|
-
}
|
|
300
|
-
async reapStaleBakes(olderThanMs) {
|
|
301
|
-
const cutoff = new Date(Date.now() - olderThanMs);
|
|
302
|
-
const [victims] = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'running' AND " +
|
|
303
|
-
"(lease_until IS NULL OR lease_until < ?) AND updated_at < ?", [cutoff, cutoff]);
|
|
304
|
-
let reaped = 0;
|
|
305
|
-
for (const v of victims) {
|
|
306
|
-
const bakeId = String(v.bake_id);
|
|
307
|
-
const [res] = await this.pool.query("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
|
|
308
|
-
"ingest_secret=NULL, index_id=NULL, updated_at=? WHERE bake_id=? AND status='running'", [new Date(), bakeId]);
|
|
309
|
-
if (res.affectedRows === 1) {
|
|
310
|
-
reaped += 1;
|
|
311
|
-
try {
|
|
312
|
-
await this.appendEvent(bakeId, "done", {
|
|
313
|
-
status: "FAILED",
|
|
314
|
-
digest: null,
|
|
315
|
-
repo: null,
|
|
316
|
-
ref: null,
|
|
317
|
-
indexId: null,
|
|
318
|
-
exitCode: null,
|
|
319
|
-
error: "bake-runner lost (lease expired)",
|
|
320
|
-
errorCode: null,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
}
|
|
325
|
-
await this.pool.query("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", [this.poolName, bakeId]);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return reaped;
|
|
329
|
-
}
|
|
330
|
-
async tx(fn) {
|
|
331
|
-
const conn = await this.pool.getConnection();
|
|
332
|
-
try {
|
|
333
|
-
await conn.beginTransaction();
|
|
334
|
-
await fn(conn);
|
|
335
|
-
await conn.commit();
|
|
336
|
-
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
try {
|
|
339
|
-
await conn.rollback();
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
}
|
|
343
|
-
throw err;
|
|
344
|
-
}
|
|
345
|
-
finally {
|
|
346
|
-
conn.release();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
3
|
//# sourceMappingURL=tidb-image-bake.js.map
|
|
@@ -17,11 +17,12 @@ export interface WebSearchOpts {
|
|
|
17
17
|
blockedDomains?: string[];
|
|
18
18
|
}
|
|
19
19
|
export type WebSearchFn = (query: string, signal?: AbortSignal, opts?: WebSearchOpts) => Promise<WebSearchResult[]>;
|
|
20
|
-
export
|
|
21
|
-
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): {
|
|
20
|
+
export interface WebSearchBackend {
|
|
22
21
|
search: WebSearchFn;
|
|
23
22
|
maxResults: number;
|
|
24
|
-
}
|
|
23
|
+
}
|
|
24
|
+
export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void;
|
|
25
|
+
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend;
|
|
25
26
|
export declare function webSearchConfigFromEnv(env?: NodeJS.ProcessEnv): WebSearchBackendConfig | undefined;
|
|
26
27
|
export declare function webSearchConfigFromSettings(raw: unknown): WebSearchBackendConfig | undefined;
|
|
27
28
|
//# sourceMappingURL=web-search.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Pool } from "mysql2/promise";
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool } from "pg";
|
|
2
3
|
export interface CounterTable {
|
|
3
4
|
table: string;
|
|
4
5
|
keyCol: string;
|
|
@@ -13,7 +14,9 @@ export interface CounterDegradeInfo {
|
|
|
13
14
|
}
|
|
14
15
|
export type CounterDegradeHook = (info: CounterDegradeInfo) => void;
|
|
15
16
|
export declare const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30000;
|
|
16
|
-
|
|
17
|
+
type CounterDialect = "tidb" | "pg";
|
|
18
|
+
export declare class SqlWriteBehindCounter {
|
|
19
|
+
private readonly dialect;
|
|
17
20
|
private readonly pool;
|
|
18
21
|
private readonly t;
|
|
19
22
|
private readonly windowMs;
|
|
@@ -26,7 +29,8 @@ export declare class WriteBehindCounter {
|
|
|
26
29
|
private flushing;
|
|
27
30
|
private flushStartedAt;
|
|
28
31
|
private failStreak;
|
|
29
|
-
constructor(pool:
|
|
32
|
+
constructor(dialect: CounterDialect, pool: MySqlPool | PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, queryTimeoutMs?: number);
|
|
33
|
+
private exec;
|
|
30
34
|
private bump;
|
|
31
35
|
bucket(): number;
|
|
32
36
|
private rk;
|
|
@@ -40,4 +44,11 @@ export declare class WriteBehindCounter {
|
|
|
40
44
|
startRefresh(intervalMs?: number): this;
|
|
41
45
|
stop(): void;
|
|
42
46
|
}
|
|
47
|
+
export declare class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
48
|
+
constructor(pool: MySqlPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
49
|
+
}
|
|
50
|
+
export declare class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
51
|
+
constructor(pool: PgPool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook, queryTimeoutMs?: number);
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
43
54
|
//# sourceMappingURL=write-behind-counter.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SEP = "\u0000";
|
|
2
2
|
export const DEFAULT_COUNTER_QUERY_TIMEOUT_MS = 30_000;
|
|
3
|
-
export class
|
|
3
|
+
export class SqlWriteBehindCounter {
|
|
4
|
+
dialect;
|
|
4
5
|
pool;
|
|
5
6
|
t;
|
|
6
7
|
windowMs;
|
|
@@ -13,7 +14,8 @@ export class WriteBehindCounter {
|
|
|
13
14
|
flushing = false;
|
|
14
15
|
flushStartedAt = 0;
|
|
15
16
|
failStreak = new Map();
|
|
16
|
-
constructor(pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
17
|
+
constructor(dialect, pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
18
|
+
this.dialect = dialect;
|
|
17
19
|
this.pool = pool;
|
|
18
20
|
this.t = t;
|
|
19
21
|
this.windowMs = windowMs;
|
|
@@ -21,6 +23,17 @@ export class WriteBehindCounter {
|
|
|
21
23
|
this.onDegraded = onDegraded;
|
|
22
24
|
this.queryTimeoutMs = queryTimeoutMs;
|
|
23
25
|
}
|
|
26
|
+
async exec(sql, params) {
|
|
27
|
+
if (this.dialect === "tidb") {
|
|
28
|
+
const [r] = (await this.pool.query({ sql, timeout: this.queryTimeoutMs }, params));
|
|
29
|
+
if (Array.isArray(r))
|
|
30
|
+
return { rows: r, affected: 0 };
|
|
31
|
+
return { rows: [], affected: Number(r?.affectedRows ?? 0) };
|
|
32
|
+
}
|
|
33
|
+
const q = { text: sql, values: params, query_timeout: this.queryTimeoutMs };
|
|
34
|
+
const res = await this.pool.query(q);
|
|
35
|
+
return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
|
|
36
|
+
}
|
|
24
37
|
bump(kind, failed, error) {
|
|
25
38
|
if (!this.onDegraded)
|
|
26
39
|
return;
|
|
@@ -72,6 +85,11 @@ export class WriteBehindCounter {
|
|
|
72
85
|
}
|
|
73
86
|
async flushOnce() {
|
|
74
87
|
const { table, keyCol, valCol } = this.t;
|
|
88
|
+
const sql = this.dialect === "tidb"
|
|
89
|
+
? `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
|
|
90
|
+
`ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`
|
|
91
|
+
: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES ($1,$2,$3,$4) ` +
|
|
92
|
+
`ON CONFLICT (${keyCol}, window_bucket) DO UPDATE SET ${valCol} = ${table}.${valCol} + EXCLUDED.${valCol}, updated_at = EXCLUDED.updated_at`;
|
|
75
93
|
let attempted = false;
|
|
76
94
|
let failure;
|
|
77
95
|
let hadFailure = false;
|
|
@@ -83,11 +101,7 @@ export class WriteBehindCounter {
|
|
|
83
101
|
const windowBucket = Number(rk.slice(sep + 1));
|
|
84
102
|
attempted = true;
|
|
85
103
|
try {
|
|
86
|
-
await this.
|
|
87
|
-
sql: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES (?,?,?,?) ` +
|
|
88
|
-
`ON DUPLICATE KEY UPDATE ${valCol} = ${valCol} + VALUES(${valCol}), updated_at = VALUES(updated_at)`,
|
|
89
|
-
timeout: this.queryTimeoutMs,
|
|
90
|
-
}, [key, windowBucket, delta, new Date()]);
|
|
104
|
+
await this.exec(sql, [key, windowBucket, delta, new Date()]);
|
|
91
105
|
const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
|
|
92
106
|
if (remaining > 0)
|
|
93
107
|
this.pendingDelta.set(rk, remaining);
|
|
@@ -108,10 +122,11 @@ export class WriteBehindCounter {
|
|
|
108
122
|
const { table, keyCol, valCol } = this.t;
|
|
109
123
|
const b = this.bucket();
|
|
110
124
|
try {
|
|
111
|
-
const
|
|
125
|
+
const { rows } = await this.exec(this.dialect === "tidb"
|
|
126
|
+
? `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = ?`
|
|
127
|
+
: `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = $1`, [b]);
|
|
112
128
|
const next = new Map();
|
|
113
|
-
for (const
|
|
114
|
-
const row = r;
|
|
129
|
+
for (const row of rows) {
|
|
115
130
|
next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol]));
|
|
116
131
|
}
|
|
117
132
|
this.fleetTotal = next;
|
|
@@ -123,8 +138,10 @@ export class WriteBehindCounter {
|
|
|
123
138
|
}
|
|
124
139
|
async reap() {
|
|
125
140
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
141
|
+
const { affected } = await this.exec(this.dialect === "tidb"
|
|
142
|
+
? `DELETE FROM ${this.t.table} WHERE window_bucket < ?`
|
|
143
|
+
: `DELETE FROM ${this.t.table} WHERE window_bucket < $1`, [this.bucket()]);
|
|
144
|
+
return affected;
|
|
128
145
|
}
|
|
129
146
|
catch {
|
|
130
147
|
return 0;
|
|
@@ -141,4 +158,14 @@ export class WriteBehindCounter {
|
|
|
141
158
|
this.timer = undefined;
|
|
142
159
|
}
|
|
143
160
|
}
|
|
161
|
+
export class WriteBehindCounter extends SqlWriteBehindCounter {
|
|
162
|
+
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
163
|
+
super("tidb", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export class PgWriteBehindCounter extends SqlWriteBehindCounter {
|
|
167
|
+
constructor(pool, t, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
168
|
+
super("pg", pool, t, windowMs, now, onDegraded, queryTimeoutMs);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
144
171
|
//# sourceMappingURL=write-behind-counter.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
2
|
+
export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[], expect: {
|
|
3
|
+
iss: string;
|
|
4
|
+
aud: string;
|
|
5
|
+
nowSec?: number;
|
|
6
|
+
skewSec?: number;
|
|
7
|
+
maxTtlSec?: number;
|
|
8
|
+
}): {
|
|
9
|
+
sub: string;
|
|
10
|
+
bnd?: string;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function approvalBnd(env: {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
boundCallId?: string | null;
|
|
15
|
+
boundInputHash?: string | null;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function verifyDirectDoorProof(proof: {
|
|
18
|
+
jwt?: string;
|
|
19
|
+
mac?: string;
|
|
20
|
+
kid?: string;
|
|
21
|
+
}, envelope: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
boundCallId?: string | null;
|
|
24
|
+
boundInputHash?: string | null;
|
|
25
|
+
decision: string;
|
|
26
|
+
reason?: string | null;
|
|
27
|
+
}, config: {
|
|
28
|
+
principalJwtPubkeys: PrincipalJwtKey[];
|
|
29
|
+
principalJwtIss?: string;
|
|
30
|
+
principalJwtAud?: string;
|
|
31
|
+
principalJwtMaxTtlSec?: number;
|
|
32
|
+
approvalHmacKeys: ApprovalHmacKey[];
|
|
33
|
+
}, opts?: {
|
|
34
|
+
actionBinding?: boolean;
|
|
35
|
+
}): {
|
|
36
|
+
ok: true;
|
|
37
|
+
principal: string;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
status: number;
|
|
41
|
+
errorCode: string;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=principal-jwt.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
2
|
+
import { verifyApprovalHmac } from "./approval-hmac.js";
|
|
3
|
+
function b64urlToBuf(s) {
|
|
4
|
+
return Buffer.from(s, "base64url");
|
|
5
|
+
}
|
|
6
|
+
function verifyJwsSignature(alg, signingInput, sig, pub) {
|
|
7
|
+
try {
|
|
8
|
+
if (alg === "EdDSA")
|
|
9
|
+
return cryptoVerify(null, signingInput, pub, sig);
|
|
10
|
+
if (alg === "RS256")
|
|
11
|
+
return cryptoVerify("sha256", signingInput, pub, sig);
|
|
12
|
+
if (alg === "ES256")
|
|
13
|
+
return cryptoVerify("sha256", signingInput, { key: pub, dsaEncoding: "ieee-p1363" }, sig);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function verifyPrincipalJwt(token, keys, expect) {
|
|
21
|
+
if (keys.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const parts = token.split(".");
|
|
24
|
+
if (parts.length !== 3)
|
|
25
|
+
return null;
|
|
26
|
+
const [h, p, s] = parts;
|
|
27
|
+
let header;
|
|
28
|
+
let payload;
|
|
29
|
+
let sig;
|
|
30
|
+
try {
|
|
31
|
+
header = JSON.parse(b64urlToBuf(h).toString("utf8"));
|
|
32
|
+
payload = JSON.parse(b64urlToBuf(p).toString("utf8"));
|
|
33
|
+
sig = b64urlToBuf(s);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof header !== "object" || header === null || typeof payload !== "object" || payload === null)
|
|
39
|
+
return null;
|
|
40
|
+
if (typeof header.alg !== "string" || typeof header.kid !== "string" || sig.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const key = keys.find((k) => k.kid === header.kid);
|
|
43
|
+
if (!key || key.alg !== header.alg)
|
|
44
|
+
return null;
|
|
45
|
+
let pub;
|
|
46
|
+
try {
|
|
47
|
+
pub = createPublicKey(key.key);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const signingInput = Buffer.from(`${h}.${p}`, "utf8");
|
|
53
|
+
if (!verifyJwsSignature(key.alg, signingInput, sig, pub))
|
|
54
|
+
return null;
|
|
55
|
+
if (payload.iss !== expect.iss)
|
|
56
|
+
return null;
|
|
57
|
+
const aud = payload.aud;
|
|
58
|
+
const audOk = aud === expect.aud || (Array.isArray(aud) && aud.includes(expect.aud));
|
|
59
|
+
if (!audOk)
|
|
60
|
+
return null;
|
|
61
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const now = expect.nowSec ?? Math.floor(Date.now() / 1000);
|
|
64
|
+
const skew = expect.skewSec ?? 30;
|
|
65
|
+
if (typeof payload.exp !== "number" || payload.exp + skew < now)
|
|
66
|
+
return null;
|
|
67
|
+
if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || payload.nbf - skew > now))
|
|
68
|
+
return null;
|
|
69
|
+
if (expect.maxTtlSec && payload.exp - now > expect.maxTtlSec + skew)
|
|
70
|
+
return null;
|
|
71
|
+
const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
|
|
72
|
+
return { sub: payload.sub, bnd };
|
|
73
|
+
}
|
|
74
|
+
export function approvalBnd(env) {
|
|
75
|
+
return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
export function verifyDirectDoorProof(proof, envelope, config, opts) {
|
|
78
|
+
const actionBinding = opts?.actionBinding ?? true;
|
|
79
|
+
if (!proof.jwt)
|
|
80
|
+
return { ok: false, status: 401, errorCode: "principal_unverified", error: "direct door requires a signed principal token (X-Approval-Principal-Token)" };
|
|
81
|
+
const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec });
|
|
82
|
+
if (!verified)
|
|
83
|
+
return { ok: false, status: 401, errorCode: "principal_unverified", error: "principal token failed verification" };
|
|
84
|
+
if (actionBinding && !envelope.boundInputHash)
|
|
85
|
+
return { ok: false, status: 400, errorCode: "approval_mac_required", error: "direct door requires boundInputHash (the bound action) in the body" };
|
|
86
|
+
if (verified.bnd !== approvalBnd(envelope))
|
|
87
|
+
return { ok: false, status: 401, errorCode: "principal_unbound", error: "principal token is not bound to this decision (cnf.bnd mismatch)" };
|
|
88
|
+
if (!proof.mac)
|
|
89
|
+
return { ok: false, status: 401, errorCode: "approval_mac_required", error: "direct door requires an approval MAC (X-Approval-Mac)" };
|
|
90
|
+
const ok = verifyApprovalHmac({ sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
|
|
91
|
+
if (!ok)
|
|
92
|
+
return { ok: false, status: 401, errorCode: "approval_mac_invalid", error: "approval envelope MAC invalid" };
|
|
93
|
+
return { ok: true, principal: verified.sub };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=principal-jwt.js.map
|
package/dist/question.d.ts
CHANGED
|
@@ -17,13 +17,14 @@ export interface QuestionThrottle {
|
|
|
17
17
|
ttlMs: number;
|
|
18
18
|
}
|
|
19
19
|
export declare const DEFAULT_QUESTION_THROTTLE: QuestionThrottle;
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function parseQuestionResponse(body: unknown): {
|
|
21
21
|
ok: true;
|
|
22
22
|
value: QuestionAnswer;
|
|
23
23
|
} | {
|
|
24
24
|
ok: false;
|
|
25
25
|
error: string;
|
|
26
26
|
};
|
|
27
|
+
export declare const parseQuestionAnswer: typeof parseQuestionResponse;
|
|
27
28
|
export declare class QuestionCoordinator {
|
|
28
29
|
private readonly als;
|
|
29
30
|
private readonly pending;
|