@quolu/lattice 0.75.0 → 0.76.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/bin/lattice.mjs +9 -2
- package/package.json +1 -1
- package/src/cli-help.mjs +2 -1
- package/src/runtime-error-reporting.mjs +63 -31
package/bin/lattice.mjs
CHANGED
|
@@ -185,8 +185,9 @@ await scheduleRuntimeErrorReport();
|
|
|
185
185
|
* 送信を有効にしていない端末では、設定fileの有無を1回見るだけで終わる。hookは打鍵ごとに走るので見ない。
|
|
186
186
|
*/
|
|
187
187
|
async function scheduleRuntimeErrorReport() {
|
|
188
|
+
// `reporting verify`の直後は送らない。受け口は1分に1回まで受けるので、続けて送ると断られる。
|
|
188
189
|
if (help !== null || args[0] === '--version' || args[0] === 'hooks'
|
|
189
|
-
|| (args[0] === 'runtime-errors' && args[1] === 'report')) return;
|
|
190
|
+
|| (args[0] === 'runtime-errors' && (args[1] === 'report' || (args[1] === 'reporting' && args[2] === 'verify')))) return;
|
|
190
191
|
try {
|
|
191
192
|
const { runtimeErrorReportingEnabled } = await import('../src/runtime-errors.mjs');
|
|
192
193
|
if (!runtimeErrorReportingEnabled()) return;
|
|
@@ -222,7 +223,7 @@ function workingDirectory() {
|
|
|
222
223
|
async function runRuntimeErrorsCli(rest) {
|
|
223
224
|
const runtimeErrors = await import('../src/runtime-errors.mjs');
|
|
224
225
|
const usage = () => {
|
|
225
|
-
process.stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code: 'USAGE', message: 'usage: lattice runtime-errors <snapshot [--after-cursor N] [--limit N]|ack <cursor>|diagnostics|resolve <fingerprint>|reopen <fingerprint>|compact|report|reporting <status|enable|disable>> --json' })}\n`);
|
|
226
|
+
process.stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code: 'USAGE', message: 'usage: lattice runtime-errors <snapshot [--after-cursor N] [--limit N]|ack <cursor>|diagnostics|resolve <fingerprint>|reopen <fingerprint>|compact|report|reporting <status|enable|disable|verify>> --json' })}\n`);
|
|
226
227
|
return 2;
|
|
227
228
|
};
|
|
228
229
|
const options = { version: packageJson.version };
|
|
@@ -251,6 +252,12 @@ async function runRuntimeErrorsCli(rest) {
|
|
|
251
252
|
result = runtimeErrors.setRuntimeErrorStatus(words[1], words[0] === 'resolve' ? 'resolved' : 'open', options);
|
|
252
253
|
} else if (words[0] === 'compact' && words.length === 1) {
|
|
253
254
|
result = runtimeErrors.compactRuntimeErrors(options);
|
|
255
|
+
} else if (words[0] === 'reporting' && words.length === 2 && words[1] === 'verify') {
|
|
256
|
+
// 空の報告を1通送って経路を確かめる。受領まで確かめられた時だけexit 0。
|
|
257
|
+
const reporting = await import('../src/runtime-error-reporting.mjs');
|
|
258
|
+
result = await reporting.verifyRuntimeErrorReporting(options);
|
|
259
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
260
|
+
return result.outcome === 'verified' ? 0 : 1;
|
|
254
261
|
} else if (words[0] === 'reporting' && words.length === 2 && ['status', 'enable', 'disable'].includes(words[1])) {
|
|
255
262
|
const reporting = await import('../src/runtime-error-reporting.mjs');
|
|
256
263
|
result = words[1] === 'status' ? reporting.runtimeErrorReportingStatus(options)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
package/src/cli-help.mjs
CHANGED
|
@@ -198,6 +198,7 @@ Commands:
|
|
|
198
198
|
compact
|
|
199
199
|
report 未受領の記録をBugHubの受け口へ送る(送信を有効にした端末だけ)
|
|
200
200
|
reporting <status|enable|disable>
|
|
201
|
+
reporting verify 空の報告を1通送り、合鍵と受け口までの経路を確かめる(記録には触れない)
|
|
201
202
|
`,
|
|
202
203
|
bridge: `Usage: lattice bridge <command> [options] --json
|
|
203
204
|
|
|
@@ -315,7 +316,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
315
316
|
'runtime-errors reopen': 'runtime-errors reopen <fingerprint> --json',
|
|
316
317
|
'runtime-errors compact': 'runtime-errors compact --json',
|
|
317
318
|
'runtime-errors report': 'runtime-errors report --json',
|
|
318
|
-
'runtime-errors reporting': 'runtime-errors reporting <status|enable|disable> --json',
|
|
319
|
+
'runtime-errors reporting': 'runtime-errors reporting <status|enable|disable|verify> --json',
|
|
319
320
|
'bridge setup': 'bridge setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
320
321
|
'bridge reconfigure': 'bridge reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
321
322
|
'bridge status': 'bridge status --json',
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
* - 受領済みにするのは、200・`accepted: true`・`report_id`一致・応答の署名一致がそろった時だけ。
|
|
32
32
|
* そろわなければ「届いたか不明」として記録を未受領のまま残し、後から今の累計を送り直す。
|
|
33
33
|
* - 送るのは`runtime-errors snapshot`が出す項目だけ。message本文・path・引数・stackは記録に無い。
|
|
34
|
+
* - `reporting verify`は、記録を載せない空の報告を1通送って、合鍵・署名・受け口までの経路を確かめる。
|
|
35
|
+
* 受け口は受領を記録するだけで、issueも通知も動かない。storeの記録と受領の印には触れない。
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
38
|
const REPORT_SCHEMA_VERSION = '1.0';
|
|
@@ -275,6 +277,41 @@ function parseJson(text) {
|
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 報告を1通、署名して送り、受領を確かめる。`accepted`を返すのは、200・`accepted: true`・`report_id`一致・
|
|
282
|
+
* 応答の署名一致がそろった時だけ。戻り値に秘密・key_id・宛先は入れない。
|
|
283
|
+
*/
|
|
284
|
+
async function sendReport({ url, keyId, secret }, snapshot, options) {
|
|
285
|
+
const now = nowMs(options);
|
|
286
|
+
const ts = Math.floor(now / 1000);
|
|
287
|
+
// `observed_at`は、載せる記録のどの時刻よりも前にしない(BugHubは観測より後の記録を断る)。秒へ切り捨てると、
|
|
288
|
+
// 記録と同じ1秒の中で送った時に記録より前へずれる。`ts`とは同じ時刻から作り、差はBugHubの許す幅(10分)に収まる。
|
|
289
|
+
const observed = Math.max(now, ...snapshot.runtime_errors.map((record) => Date.parse(record.last_seen)),
|
|
290
|
+
...snapshot.resolutions.map((resolution) => Date.parse(resolution.resolved_at)));
|
|
291
|
+
const report = buildRuntimeErrorReport({ snapshot, version: options.version ?? 'unknown',
|
|
292
|
+
reportId: options.reportId ?? randomUUID(), observedAt: new Date(observed).toISOString() });
|
|
293
|
+
const bodyBytes = Buffer.from(JSON.stringify(report), 'utf8');
|
|
294
|
+
if (bodyBytes.length > REPORT_MAX_BYTES) return { outcome: 'too_large', reason: 'report_too_large', http_status: null };
|
|
295
|
+
|
|
296
|
+
const response = await post(url, {
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
'Content-Length': String(bodyBytes.length),
|
|
299
|
+
Authorization: `BugHub-HMAC-SHA256 key_id=${keyId}, ts=${ts}, sig=${signReport(secret, String(ts), bodyBytes)}`,
|
|
300
|
+
}, bodyBytes, options.timeoutMs ?? REQUEST_TIMEOUT_MS);
|
|
301
|
+
|
|
302
|
+
if (response.status === null) return { outcome: 'unconfirmed', reason: response.reason, http_status: null };
|
|
303
|
+
const body = parseJson(response.body);
|
|
304
|
+
if (response.status === 200) {
|
|
305
|
+
if (body === null || body.accepted !== true || body.report_id !== report.report_id
|
|
306
|
+
|| !receiptSignatureMatches(secret, report.report_id, body.received_at, body.sig)) {
|
|
307
|
+
return { outcome: 'unconfirmed', reason: 'receipt_not_verified', http_status: 200 };
|
|
308
|
+
}
|
|
309
|
+
return { outcome: 'accepted', reason: null, http_status: 200 };
|
|
310
|
+
}
|
|
311
|
+
const code = typeof body?.error === 'string' && REJECTION_CODE.test(body.error) ? body.error : 'unrecognized_response';
|
|
312
|
+
return { outcome: response.status >= 500 ? 'unconfirmed' : 'rejected', reason: code, http_status: response.status };
|
|
313
|
+
}
|
|
314
|
+
|
|
278
315
|
/**
|
|
279
316
|
* 未受領の記録を1回送る。`options.auto`の時だけ送る時機の制限を見る(手で打った`report`は見ない
|
|
280
317
|
* ——多すぎればBugHubが429で断る)。戻り値に秘密・key_id・宛先は入れない。
|
|
@@ -305,13 +342,6 @@ export async function reportRuntimeErrors(options = {}) {
|
|
|
305
342
|
if (reason !== null) return result('throttled', { reason });
|
|
306
343
|
}
|
|
307
344
|
|
|
308
|
-
// `ts`と`observed_at`は同じ時刻から作る(BugHubは10分より離れた組を断る)。
|
|
309
|
-
const ts = Math.floor(nowMs(options) / 1000);
|
|
310
|
-
const report = buildRuntimeErrorReport({ snapshot: pending, version: options.version ?? 'unknown',
|
|
311
|
-
reportId: options.reportId ?? randomUUID(), observedAt: new Date(ts * 1000).toISOString() });
|
|
312
|
-
const bodyBytes = Buffer.from(JSON.stringify(report), 'utf8');
|
|
313
|
-
if (bodyBytes.length > REPORT_MAX_BYTES) return result('rejected', { reason: 'report_too_large', sent });
|
|
314
|
-
|
|
315
345
|
const finish = (outcome, extra = {}) => {
|
|
316
346
|
try {
|
|
317
347
|
writeDelivery(options, { schema: DELIVERY_SCHEMA, last_attempt_at: new Date(nowMs(options)).toISOString(),
|
|
@@ -320,31 +350,33 @@ export async function reportRuntimeErrors(options = {}) {
|
|
|
320
350
|
return result(outcome, { sent, acknowledged_through: pending.cursor.acknowledged_through, ...extra });
|
|
321
351
|
};
|
|
322
352
|
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (response.status === 200) {
|
|
333
|
-
if (body === null || body.accepted !== true || body.report_id !== report.report_id
|
|
334
|
-
|| !receiptSignatureMatches(secret, report.report_id, body.received_at, body.sig)) {
|
|
335
|
-
return finish('unconfirmed', { reason: 'receipt_not_verified', http_status: 200 });
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const acknowledged = acknowledgeRuntimeErrors(pending.cursor.next, options);
|
|
339
|
-
return finish('delivered', { http_status: 200, acknowledged_through: acknowledged.cursor.acknowledged_through });
|
|
340
|
-
} catch {
|
|
341
|
-
// 届いているが受領済みを残せなかった。次の送信が同じ累計を送り直す(BugHubは二重に数えない)。
|
|
342
|
-
return finish('unconfirmed', { reason: 'ack_not_recorded', http_status: 200 });
|
|
343
|
-
}
|
|
353
|
+
const answer = await sendReport(credential.credential, pending, options);
|
|
354
|
+
if (answer.outcome === 'too_large') return result('rejected', { reason: 'report_too_large', sent });
|
|
355
|
+
if (answer.outcome !== 'accepted') return finish(answer.outcome, { reason: answer.reason, http_status: answer.http_status });
|
|
356
|
+
try {
|
|
357
|
+
const acknowledged = acknowledgeRuntimeErrors(pending.cursor.next, options);
|
|
358
|
+
return finish('delivered', { http_status: 200, acknowledged_through: acknowledged.cursor.acknowledged_through });
|
|
359
|
+
} catch {
|
|
360
|
+
// 届いているが受領済みを残せなかった。次の送信が同じ累計を送り直す(BugHubは二重に数えない)。
|
|
361
|
+
return finish('unconfirmed', { reason: 'ack_not_recorded', http_status: 200 });
|
|
344
362
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 記録を載せない空の報告を1通送り、合鍵・署名・受け口までの経路を確かめる(`reporting verify`)。
|
|
367
|
+
* 送る条件は`report`と同じで、送信を有効にしていない端末と合鍵の無い端末はnetworkへ触れない。
|
|
368
|
+
* storeの記録・受領の印・送る時機の記録には触れない。受け口は端末×製品ごとに1分に1回まで受けるので、
|
|
369
|
+
* 直前に送っていれば429で断られる。
|
|
370
|
+
*/
|
|
371
|
+
export async function verifyRuntimeErrorReporting(options = {}) {
|
|
372
|
+
if (!runtimeErrorCollectionSupported(options)) return result('unsupported');
|
|
373
|
+
if (!runtimeErrorReportingEnabled(options)) return result('disabled');
|
|
374
|
+
const credential = readProductCredential(options);
|
|
375
|
+
if (credential.status === 'missing') return result('credential_missing');
|
|
376
|
+
if (credential.status !== 'ok') return result('credential_unsafe', { reason: credential.reason });
|
|
377
|
+
const answer = await sendReport(credential.credential, { runtime_errors: [], resolutions: [] }, options);
|
|
378
|
+
if (answer.outcome === 'accepted') return result('verified', { http_status: 200 });
|
|
379
|
+
return result(answer.outcome, { reason: answer.reason, http_status: answer.http_status });
|
|
348
380
|
}
|
|
349
381
|
|
|
350
382
|
/**
|