@agentcomms/events-daemon 0.24.0 → 0.26.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/README.md +68 -5
- package/dist/{b3-contract-C1q5aekF.mjs → b3-contract-CG8Z2OtX.mjs} +320 -67
- package/dist/cli.mjs +3200 -121
- package/dist/index.d.mts +152 -47
- package/dist/index.mjs +1 -1
- package/dist/laya-worker.d.mts +1 -0
- package/dist/laya-worker.mjs +356 -0
- package/dist/{records-DsUxYBwb.mjs → records-D15LoVeb.mjs} +100 -3
- package/dist/records-qtNEBi7g.mjs +2 -0
- package/dist/{webhook-target-DyDTwWhV.mjs → webhook-target-EzLIFpIY.mjs} +551 -17
- package/dist/worker-boundary-probe-CFN6EsyK.mjs +850 -0
- package/laya-enablement-disclosures.json +17 -0
- package/laya-manifest.json +52 -0
- package/laya-runtime.json +4 -0
- package/package.json +12 -9
- package/dist/records-DjE5a4_H.mjs +0 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
2
|
+
import { $ as addSubscriber, $t as isControlHello, A as normaliseSourceOptions, At as updateJudge, B as canonicalJudgeKindEnablement, Bt as deliveriesList, C as normaliseActivationDocument, Ct as judgeKindsList, D as normaliseGmailSourceOptions, Dt as removeJudge, E as classifySourceOptionChange, Et as judgesList, F as assertCreatableLayaJudge, Ft as doctor, G as registerLayaEnablementInputs, Gt as catalogueShow, H as layaEnablementInputsFor, Ht as heldList, I as assertLayaJudgeKindAvailable, It as ACTIVATION_FAILURE, J as addTarget, Jt as probeControl, K as createEventsMcpServer, Kt as approvalView, L as assertLayaRuleJudgeOffered, Lt as isActivationFailureCode, M as APPROVED_TYPESAFE_MODELS, Mt as assertDoctorReportV1, N as JUDGE_BUDGET_ID, Nt as assertDoctorReportV2, O as normaliseResendSourceOptions, Ot as removeJudgeModel, P as JUDGE_KIND_DEFINITIONS, Pt as assertDoctorReportV3, Q as targetTest, Qt as encodeControlFrame, R as canonicalJudge, Rt as disableAll, S as disclosureBindingFor, St as enableJudgeKind, T as sseDeliveryTarget, Tt as judgeTest, U as layaJudgeKindDefinitionFor, Ut as retryDelivery, V as judgeKindDefinition, Vt as dropDelivery, W as layaJudgeTermsFor, Wt as catalogueList, X as targetsList, Xt as ControlFrameReader, Y as removeTarget$1, Yt as CONTROL_PROTOCOL_VERSIONS, Z as updateTarget, Zt as controlFailure, _ as isOwnerControlWireOperation, _t as pause, an as readInstance, at as loadSqlite, b as canonicalSubscriber, bt as budgetShow, cn as assertControlSupported, ct as createRule$1, d as B3_SECRET_TERMINAL_ORIGIN, dn as verifyControlSocket, dt as removeRule, en as isControlRequest, et as removeSubscriber, f as isB3ControlOperation, fn as verifyPrivateSocketDirectory, ft as ruleShow$1, g as OWNER_CONTROL_WIRES, gn as eventPaths, gt as ruleTestRetained, h as layaJudgePreviewFor, hn as ensureOwnerOnlyFile, ht as ruleTest, in as processIsLive, it as status, j as normaliseWhatsAppSourceOptions, jt as enableAll, k as normaliseSlackSourceOptions, kt as updateBudget, ln as assertSocketPathFits, lt as disableRule$1, m as renderB3ApprovalPreviewV1, mn as ensureEventSocketDirectory, mt as updateRule, nn as mayRecoverStaleInstance, nt as updateSubscriber, on as removeInstance, ot as sourceShow$1, p as isB3SecretOperation, pn as ensureEventPaths, pt as rulesList, q as VERSION, qt as EventControlClient, rn as newInstanceRecord, rt as stop, sn as writeInstance, st as sourcesList, tn as negotiateVersion, tt as subscribersList, un as controlEndpoint, ut as enableRule, v as activationDocumentDigest, vt as resume, w as canonicalSseSubscriber, wt as judgeModelStatus, x as canonicalTarget, xt as disableJudgeKind, y as canonicalFullRuleDocument, yt as addJudge, z as canonicalJudgeBudget, zt as decideHeld } from "./b3-contract-CG8Z2OtX.mjs";
|
|
3
|
+
import { a as createLayaWorkerBoundary, d as encodeWorkerFrame, h as isLayaTemplateId, l as WorkerFrameParser, u as createWorkerRequestId, v as installErrorSink } from "./worker-boundary-probe-CFN6EsyK.mjs";
|
|
4
|
+
import { _ as JUDGE_KIND_TYPESAFE, c as validateLocalEndpoint, d as assertApprovedAddressSet, f as parseIpLiteral, h as JUDGE_KIND_LAYA, i as webhookDeliveryTarget, l as AddressPolicyError, m as JUDGE_KINDS, n as canonicalWebhookTarget, p as EventDomainError, r as secretWebhookUrlDescriptor, t as canonicalSecretWebhookUrl, u as assertAddressAllowed, v as assertJudgeKindEnabled, y as isJudgeKind } from "./webhook-target-EzLIFpIY.mjs";
|
|
5
|
+
import { c as migrateEventSecrets, i as RecordStorageError, l as openEventSecretStore, n as LayaModelRepository, o as WriterQuiescer, s as withEventWriterLease, t as EventRecordCipher } from "./records-D15LoVeb.mjs";
|
|
5
6
|
import process$1 from "node:process";
|
|
6
7
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
|
-
import { chmod, lstat, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { appendFile, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, statfs, writeFile } from "node:fs/promises";
|
|
8
9
|
import net, { createServer } from "node:net";
|
|
9
10
|
import { CommsError, TaintCollector, agentMarker, canPrompt, canonicalJson, escapeForDisplay, hashChallenge, isCommsError, isDangerous, neutralise, newBoundary, openCore, renderFencedBody, resolvePaths, sanitizeHtmlToText, sanitizePlainText, sha256Hex, stripInvisible, wrapUntrusted } from "@agentcomms/core";
|
|
10
|
-
import fs, { constants } from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
11
|
+
import fs, { constants, createReadStream } from "node:fs";
|
|
12
|
+
import path, { basename, dirname, join, resolve, sep } from "node:path";
|
|
12
13
|
import { CATALOGUE, TEST_CLOUD_EVENT, TEST_CLOUD_EVENT_BYTES, applyRepresentation, buildCloudEvent, canonicalJson as canonicalJson$1, catalogueEntry, classifyMapped, cloudEventBytes, compileMapping, conditionPointers, evaluateCondition, evaluateMapping, eventId, eventIdPreimage, expandPattern, getPointer, gmailMessageLabelledV1, gmailMessageReceivedV1, gmailMessageSentV1, parsePointer, validateEvent, whatsappMessageKey } from "@agentcomms/events";
|
|
14
|
+
import childProcess from "node:child_process";
|
|
13
15
|
import { createInterface } from "node:readline/promises";
|
|
14
16
|
import { EventEmitter } from "node:events";
|
|
15
|
-
import childProcess from "node:child_process";
|
|
16
17
|
import { stripVTControlCharacters } from "node:util";
|
|
17
18
|
import { createGmailEventSource, normaliseGmailEventMetadata } from "@agentcomms/gmail";
|
|
18
19
|
import { createResendEventReaderForPaths } from "@agentcomms/resend";
|
|
@@ -20,6 +21,8 @@ import { openSlackEventSourceForPaths } from "@agentcomms/slack";
|
|
|
20
21
|
import { chatKindOf, openWhatsAppEventOperations, visibilityUnitKey } from "@agentcomms/whatsapp";
|
|
21
22
|
import tls from "node:tls";
|
|
22
23
|
import dns from "node:dns";
|
|
24
|
+
import { Readable } from "node:stream";
|
|
25
|
+
import { createGunzip } from "node:zlib";
|
|
23
26
|
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
|
|
24
27
|
/**
|
|
25
28
|
* CommanderError class
|
|
@@ -3246,13 +3249,24 @@ function documentRecord(value) {
|
|
|
3246
3249
|
return JSON.parse(canonicalJson(value));
|
|
3247
3250
|
}
|
|
3248
3251
|
/** SQLite repository for D2's immutable, initially inert version rows. */
|
|
3249
|
-
var ImmutableVersions = class {
|
|
3252
|
+
var ImmutableVersions = class ImmutableVersions {
|
|
3250
3253
|
#database;
|
|
3251
|
-
|
|
3254
|
+
#layaInputs;
|
|
3255
|
+
#judgeContract = "b3-control-v1";
|
|
3256
|
+
constructor(database, options = layaEnablementInputsFor(database)) {
|
|
3252
3257
|
this.#database = database;
|
|
3258
|
+
this.#layaInputs = options;
|
|
3259
|
+
}
|
|
3260
|
+
/** The repository over this database's own Laya inputs, creating judges and rules as `contract` defines them. */
|
|
3261
|
+
static forJudgeContract(database, contract) {
|
|
3262
|
+
const versions = new ImmutableVersions(database);
|
|
3263
|
+
versions.#judgeContract = contract;
|
|
3264
|
+
return versions;
|
|
3253
3265
|
}
|
|
3254
3266
|
createRule(input) {
|
|
3255
3267
|
const document = canonicalFullRuleDocument(input);
|
|
3268
|
+
if (this.#judgeContract === "b3-control-v3") assertCreatableLayaJudge(document.agentic?.judge, this.#layaInputs);
|
|
3269
|
+
else this.#assertLayaRuleOffered(document);
|
|
3256
3270
|
this.assertEmbeddedVersions(document);
|
|
3257
3271
|
return this.insert("rule_versions", "rule", document.ruleId, document.version, document);
|
|
3258
3272
|
}
|
|
@@ -3272,9 +3286,24 @@ var ImmutableVersions = class {
|
|
|
3272
3286
|
const document = canonicalSubscriber(input);
|
|
3273
3287
|
return this.insert("subscriber_versions", "subscriber", document.subscriberId, document.version, document);
|
|
3274
3288
|
}
|
|
3275
|
-
/**
|
|
3289
|
+
/**
|
|
3290
|
+
* Validates a dormant judge version; this deliberately does not require an enabled kind. Under B3 v1 and v2 a Laya
|
|
3291
|
+
* judge, or a rule naming one, is refused with the kind's own closed reason wherever this release or platform does
|
|
3292
|
+
* not offer Laya — with the placeholder lock, everywhere. Under B3 v3 it is kept as configuration, naming this
|
|
3293
|
+
* release's reviewed manifest and a template the worker runs (JudgeDocumentContract).
|
|
3294
|
+
*/
|
|
3276
3295
|
prepareJudge(input) {
|
|
3277
|
-
|
|
3296
|
+
const document = canonicalJudge(input);
|
|
3297
|
+
if (this.#judgeContract === "b3-control-v3") {
|
|
3298
|
+
assertCreatableLayaJudge(document, this.#layaInputs);
|
|
3299
|
+
return document;
|
|
3300
|
+
}
|
|
3301
|
+
if (document.kind === JUDGE_KINDS[1]) assertLayaJudgeKindAvailable(this.#layaInputs);
|
|
3302
|
+
return document;
|
|
3303
|
+
}
|
|
3304
|
+
/** B3 v1 and v2: a rule naming a Laya judge exists only where this release and platform offer Laya (E2 Task 8). */
|
|
3305
|
+
#assertLayaRuleOffered(document) {
|
|
3306
|
+
if (document.agentic?.judge.kind === JUDGE_KINDS[1]) assertLayaJudgeKindAvailable(this.#layaInputs);
|
|
3278
3307
|
}
|
|
3279
3308
|
createJudge(input) {
|
|
3280
3309
|
const document = this.prepareJudge(input);
|
|
@@ -3309,16 +3338,15 @@ var ImmutableVersions = class {
|
|
|
3309
3338
|
return this.insert("judge_budget_versions", "judge-budget", document.budgetId, document.version, document);
|
|
3310
3339
|
}
|
|
3311
3340
|
prepareJudgeKindDocument(input) {
|
|
3312
|
-
return canonicalJudgeKindEnablement(input);
|
|
3341
|
+
return canonicalJudgeKindEnablement(input, this.#layaInputs);
|
|
3313
3342
|
}
|
|
3314
3343
|
/** The sole digest-verifying loader for an immutable kind enablement document. */
|
|
3315
3344
|
loadJudgeKind(kind, version) {
|
|
3316
|
-
if (isJudgeKind(kind) && judgeKindDefinition(kind).availability === "unavailable") throw new EventDomainError("E2_REQUIRED", "laya is unavailable until E2");
|
|
3317
3345
|
const row = this.#database.prepare("SELECT document, digest FROM judge_kind_versions WHERE kind = ? AND version = ? AND revoked_at IS NULL").get(kind, version);
|
|
3318
3346
|
if (row === void 0) throw new EventDomainError("VERSION_DOCUMENT_INVALID", "the requested judge kind version does not exist");
|
|
3319
3347
|
let enablement;
|
|
3320
3348
|
try {
|
|
3321
|
-
enablement = canonicalJudgeKindEnablement(JSON.parse(row.document));
|
|
3349
|
+
enablement = canonicalJudgeKindEnablement(JSON.parse(row.document), this.#layaInputs);
|
|
3322
3350
|
} catch {
|
|
3323
3351
|
throw new EventDomainError("VERSION_DOCUMENT_INVALID", "the stored judge kind document is invalid");
|
|
3324
3352
|
}
|
|
@@ -3327,6 +3355,10 @@ var ImmutableVersions = class {
|
|
|
3327
3355
|
}
|
|
3328
3356
|
/** Loads one exact immutable kind enablement document, never inferring a default enabled state. */
|
|
3329
3357
|
prepareJudgeKind(kind, version) {
|
|
3358
|
+
if (isJudgeKind(kind) && judgeKindDefinition(kind).availability === "unavailable") {
|
|
3359
|
+
if (kind !== JUDGE_KINDS[1] || judgeKindDefinition(kind).implementation === void 0) throw new EventDomainError("E2_REQUIRED", "the judge kind requires its next phase implementation");
|
|
3360
|
+
assertLayaJudgeKindAvailable(this.#layaInputs);
|
|
3361
|
+
}
|
|
3330
3362
|
return {
|
|
3331
3363
|
documentVersion: 1,
|
|
3332
3364
|
kind: "judge-kind",
|
|
@@ -3350,7 +3382,10 @@ var ImmutableVersions = class {
|
|
|
3350
3382
|
if (row === void 0) throw new EventDomainError("VERSION_DOCUMENT_INVALID", "the requested rule version does not exist");
|
|
3351
3383
|
const rule = canonicalFullRuleDocument(JSON.parse(row.document));
|
|
3352
3384
|
this.assertEmbeddedVersions(rule);
|
|
3353
|
-
if (rule.agentic !== void 0)
|
|
3385
|
+
if (rule.agentic !== void 0) {
|
|
3386
|
+
if (rule.agentic.judge.kind === JUDGE_KINDS[1]) assertLayaRuleJudgeOffered(this.#layaInputs);
|
|
3387
|
+
assertJudgeKindEnabled(rule.agentic.judge.kind, this.activeVersion("judge-kind", rule.agentic.judge.kind) !== null);
|
|
3388
|
+
}
|
|
3354
3389
|
return normaliseActivationDocument({
|
|
3355
3390
|
documentVersion: 1,
|
|
3356
3391
|
kind: "rule",
|
|
@@ -7240,6 +7275,7 @@ function ruleAuthority(database, scope, judge) {
|
|
|
7240
7275
|
function providerLimit(document, provider) {
|
|
7241
7276
|
const found = document.providers.find((entry) => entry.provider === provider);
|
|
7242
7277
|
if (found === void 0) refusal$2("PROVIDER_BUDGET_UNCONFIGURED", "the judge provider has no approved budget ceiling");
|
|
7278
|
+
if (provider === "laya" && found.concurrency > 1) refusal$2("PROVIDER_BUDGET_UNCONFIGURED", "the Laya provider budget must remain one-at-a-time");
|
|
7243
7279
|
return found;
|
|
7244
7280
|
}
|
|
7245
7281
|
function usage(database, where, args, now) {
|
|
@@ -7536,7 +7572,7 @@ var JudgeReservationIoGate = class {
|
|
|
7536
7572
|
return this.#firstGateError;
|
|
7537
7573
|
}
|
|
7538
7574
|
finalGate = (phase, assertBeforeTransition) => {
|
|
7539
|
-
const firstPhysicalPhase = this.#judgeKind === "typesafe" ? "dns" : "tcp";
|
|
7575
|
+
const firstPhysicalPhase = this.#judgeKind === "typesafe" ? "dns" : this.#judgeKind === "laya" ? "write" : "tcp";
|
|
7540
7576
|
if (!this.#entered) {
|
|
7541
7577
|
if (phase !== firstPhysicalPhase) {
|
|
7542
7578
|
assertBeforeTransition?.();
|
|
@@ -8660,10 +8696,20 @@ function recoverRevokedJudgeVersions(input) {
|
|
|
8660
8696
|
}
|
|
8661
8697
|
//#endregion
|
|
8662
8698
|
//#region src/runtime/judge-purge.ts
|
|
8699
|
+
const purgeListeners = /* @__PURE__ */ new WeakMap();
|
|
8700
|
+
/** One owner composes one listener per database; the returned function removes exactly that registration. */
|
|
8701
|
+
function registerJudgeWorkPurgeListener(database, listener) {
|
|
8702
|
+
if (purgeListeners.has(database)) throw new Error("a judge work purge listener is already registered");
|
|
8703
|
+
purgeListeners.set(database, listener);
|
|
8704
|
+
return () => {
|
|
8705
|
+
if (purgeListeners.get(database) === listener) purgeListeners.delete(database);
|
|
8706
|
+
};
|
|
8707
|
+
}
|
|
8663
8708
|
/**
|
|
8664
8709
|
* The sole judge participant for a caller's already-open immediate transaction. It deliberately performs no
|
|
8665
8710
|
* reservation transition itself: Task 5's idempotent recovery primitive releases pre-I/O work and terminalises
|
|
8666
|
-
* post-I/O work exactly once.
|
|
8711
|
+
* post-I/O work exactly once. After each transition it tells the database's work listener, so every E purge caller
|
|
8712
|
+
* — they all reach judge work through this class — also stops the in-memory work that transition orphaned.
|
|
8667
8713
|
*/
|
|
8668
8714
|
var JudgePurgeParticipant = class {
|
|
8669
8715
|
#database;
|
|
@@ -8672,6 +8718,10 @@ var JudgePurgeParticipant = class {
|
|
|
8672
8718
|
this.#database = input.database;
|
|
8673
8719
|
this.#now = input.now ?? Date.now;
|
|
8674
8720
|
}
|
|
8721
|
+
#purged(result) {
|
|
8722
|
+
purgeListeners.get(this.#database)?.onWorkPurged();
|
|
8723
|
+
return result;
|
|
8724
|
+
}
|
|
8675
8725
|
all() {
|
|
8676
8726
|
const result = recoverJudgeWork({
|
|
8677
8727
|
database: this.#database,
|
|
@@ -8680,7 +8730,7 @@ var JudgePurgeParticipant = class {
|
|
|
8680
8730
|
});
|
|
8681
8731
|
purgeHeldDecisionsInTransaction(this.#database, { scope: "all" });
|
|
8682
8732
|
this.#database.prepare("UPDATE decisions SET encrypted_record = NULL WHERE encrypted_record IS NOT NULL").run();
|
|
8683
|
-
return result;
|
|
8733
|
+
return this.#purged(result);
|
|
8684
8734
|
}
|
|
8685
8735
|
kind(judgeKind) {
|
|
8686
8736
|
const result = recoverJudgeWork({
|
|
@@ -8695,16 +8745,16 @@ var JudgePurgeParticipant = class {
|
|
|
8695
8745
|
scope: "judge-kind",
|
|
8696
8746
|
judgeKind
|
|
8697
8747
|
});
|
|
8698
|
-
return result;
|
|
8748
|
+
return this.#purged(result);
|
|
8699
8749
|
}
|
|
8700
8750
|
/** Judge removal is version-exact and delegates each durable identity to the single recovery transition. */
|
|
8701
8751
|
judge(input) {
|
|
8702
|
-
return recoverRevokedJudgeVersions({
|
|
8752
|
+
return this.#purged(recoverRevokedJudgeVersions({
|
|
8703
8753
|
database: this.#database,
|
|
8704
8754
|
now: this.#now(),
|
|
8705
8755
|
judgeId: input.judgeId,
|
|
8706
8756
|
versions: input.versions
|
|
8707
|
-
});
|
|
8757
|
+
}));
|
|
8708
8758
|
}
|
|
8709
8759
|
account(input) {
|
|
8710
8760
|
const result = recoverJudgeWork({
|
|
@@ -8720,7 +8770,7 @@ var JudgePurgeParticipant = class {
|
|
|
8720
8770
|
scope: "source-account",
|
|
8721
8771
|
...input
|
|
8722
8772
|
});
|
|
8723
|
-
return result;
|
|
8773
|
+
return this.#purged(result);
|
|
8724
8774
|
}
|
|
8725
8775
|
event(input) {
|
|
8726
8776
|
if (input.ruleId === void 0 !== (input.ruleVersion === void 0)) throw new Error("a rule-bound judge event purge needs both rule id and version");
|
|
@@ -8755,7 +8805,7 @@ var JudgePurgeParticipant = class {
|
|
|
8755
8805
|
ruleId: input.ruleId,
|
|
8756
8806
|
ruleVersion: input.ruleVersion
|
|
8757
8807
|
});
|
|
8758
|
-
return result;
|
|
8808
|
+
return this.#purged(result);
|
|
8759
8809
|
}
|
|
8760
8810
|
/**
|
|
8761
8811
|
* Purge judge work for one retained projection without inferring its source from the replacement document. The
|
|
@@ -8777,10 +8827,10 @@ var JudgePurgeParticipant = class {
|
|
|
8777
8827
|
released += result.released;
|
|
8778
8828
|
settled += result.settled;
|
|
8779
8829
|
}
|
|
8780
|
-
return {
|
|
8830
|
+
return this.#purged({
|
|
8781
8831
|
released,
|
|
8782
8832
|
settled
|
|
8783
|
-
};
|
|
8833
|
+
});
|
|
8784
8834
|
}
|
|
8785
8835
|
/** Enumerate the raw source/account identities first; each recovery call remains exact and account-scoped. */
|
|
8786
8836
|
rule(input) {
|
|
@@ -8842,10 +8892,10 @@ var JudgePurgeParticipant = class {
|
|
|
8842
8892
|
ruleId: input.ruleId,
|
|
8843
8893
|
ruleVersion: row.rule_version
|
|
8844
8894
|
});
|
|
8845
|
-
return {
|
|
8895
|
+
return this.#purged({
|
|
8846
8896
|
released,
|
|
8847
8897
|
settled
|
|
8848
|
-
};
|
|
8898
|
+
});
|
|
8849
8899
|
}
|
|
8850
8900
|
};
|
|
8851
8901
|
//#endregion
|
|
@@ -11680,6 +11730,18 @@ const EVENT_MIGRATIONS = [
|
|
|
11680
11730
|
END`,
|
|
11681
11731
|
"UPDATE meta SET value = '22' WHERE key = 'schema_version'"
|
|
11682
11732
|
]
|
|
11733
|
+
},
|
|
11734
|
+
{
|
|
11735
|
+
version: 23,
|
|
11736
|
+
name: "event-laya-model-state-v1",
|
|
11737
|
+
statements: [`CREATE TABLE laya_models (
|
|
11738
|
+
manifest_id TEXT PRIMARY KEY CHECK (manifest_id <> ''),
|
|
11739
|
+
state TEXT NOT NULL CHECK (state IN ('installing', 'downloading', 'ready', 'failed', 'removed')),
|
|
11740
|
+
bytes_verified INTEGER NOT NULL CHECK (bytes_verified >= 0),
|
|
11741
|
+
verified_at INTEGER,
|
|
11742
|
+
failure_code TEXT,
|
|
11743
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
|
|
11744
|
+
) STRICT`, "UPDATE meta SET value = '23' WHERE key = 'schema_version'"]
|
|
11683
11745
|
}
|
|
11684
11746
|
];
|
|
11685
11747
|
function initialiseLedger(database) {
|
|
@@ -11846,6 +11908,8 @@ var ActivationRuntime = class {
|
|
|
11846
11908
|
#failpoint;
|
|
11847
11909
|
#deadlineFailpoint;
|
|
11848
11910
|
#assertObjectSecretReady;
|
|
11911
|
+
#layaInputs;
|
|
11912
|
+
#onJudgeKindEnabled;
|
|
11849
11913
|
/** One owner process writes this state directory, so this in-memory queue is enough to serialise one approval. */
|
|
11850
11914
|
#approvalOperations = /* @__PURE__ */ new Map();
|
|
11851
11915
|
constructor(options) {
|
|
@@ -11873,10 +11937,15 @@ var ActivationRuntime = class {
|
|
|
11873
11937
|
this.#failpoint = options.failpoint;
|
|
11874
11938
|
this.#deadlineFailpoint = options.deadlineFailpoint;
|
|
11875
11939
|
this.#assertObjectSecretReady = options.assertObjectSecretReady;
|
|
11940
|
+
this.#layaInputs = {
|
|
11941
|
+
...options.layaAssetSource === void 0 ? {} : { layaAssetSource: options.layaAssetSource },
|
|
11942
|
+
...options.layaPlatformProbe === void 0 ? {} : { layaPlatformProbe: options.layaPlatformProbe }
|
|
11943
|
+
};
|
|
11944
|
+
this.#onJudgeKindEnabled = options.onJudgeKindEnabled;
|
|
11876
11945
|
}
|
|
11877
11946
|
async prepareRule(input) {
|
|
11878
11947
|
await this.assertNoCompletingMutation(input.ruleId);
|
|
11879
|
-
const versions =
|
|
11948
|
+
const versions = this.#versions();
|
|
11880
11949
|
const document = versions.prepareRule(input.ruleId, input.version);
|
|
11881
11950
|
await this.#assertDocumentSecretsReady(document.rule);
|
|
11882
11951
|
this.#sourceRegistry.require(document.rule.source.channel);
|
|
@@ -11963,7 +12032,7 @@ var ActivationRuntime = class {
|
|
|
11963
12032
|
ruleVersion: row.version
|
|
11964
12033
|
}))
|
|
11965
12034
|
};
|
|
11966
|
-
const versions =
|
|
12035
|
+
const versions = this.#versions();
|
|
11967
12036
|
const liveBySource = /* @__PURE__ */ new Map();
|
|
11968
12037
|
const points = [];
|
|
11969
12038
|
for (const row of rows) {
|
|
@@ -11987,7 +12056,7 @@ var ActivationRuntime = class {
|
|
|
11987
12056
|
/** Prepares the singleton budget solely from its immutable stored document. */
|
|
11988
12057
|
async prepareJudgeBudget(input) {
|
|
11989
12058
|
await this.assertNoCompletingMutation();
|
|
11990
|
-
const versions =
|
|
12059
|
+
const versions = this.#versions();
|
|
11991
12060
|
const document = versions.prepareJudgeBudget(input.version);
|
|
11992
12061
|
const active = versions.activeVersion("judge-budget", JUDGE_BUDGET_ID);
|
|
11993
12062
|
if (active?.version === input.version) throw new CommsError("CONFIG", "the requested judge budget is already active");
|
|
@@ -12003,7 +12072,7 @@ var ActivationRuntime = class {
|
|
|
12003
12072
|
/** Prepares a separately approved available judge-kind pointer; it performs no provider work. */
|
|
12004
12073
|
async prepareJudgeKind(input) {
|
|
12005
12074
|
const disableGeneration = this.#judgeKindDisableGeneration(input.kind);
|
|
12006
|
-
const versions =
|
|
12075
|
+
const versions = this.#versions();
|
|
12007
12076
|
const document = versions.prepareJudgeKind(input.kind, input.version);
|
|
12008
12077
|
await this.assertNoCompletingMutation();
|
|
12009
12078
|
const active = versions.activeVersion("judge-kind", document.enablement.kind);
|
|
@@ -12267,7 +12336,7 @@ var ActivationRuntime = class {
|
|
|
12267
12336
|
}
|
|
12268
12337
|
async #prepare(document, points, effect, replacementOfVersion) {
|
|
12269
12338
|
const intentId = this.#newIntentId();
|
|
12270
|
-
const binding = disclosureBindingFor(intentId, document);
|
|
12339
|
+
const binding = disclosureBindingFor(intentId, document, this.#layaInputs);
|
|
12271
12340
|
const now = this.#now();
|
|
12272
12341
|
const plannedAccounts = [...new Map(points.map((point) => [`${point.source}\u0000${point.accountId}`, point])).values()];
|
|
12273
12342
|
this.#store.immediate(() => {
|
|
@@ -12465,6 +12534,12 @@ var ActivationRuntime = class {
|
|
|
12465
12534
|
if (document.kind === "rule") await this.#assertDocumentSecretsReady(document.rule);
|
|
12466
12535
|
this.#finalise(current, document, preparedPoints, usedAt);
|
|
12467
12536
|
this.#failpoint?.("after-move");
|
|
12537
|
+
if (document.kind === "judge-kind") try {
|
|
12538
|
+
this.#onJudgeKindEnabled?.({
|
|
12539
|
+
kind: document.enablement.kind,
|
|
12540
|
+
version: document.enablement.version
|
|
12541
|
+
});
|
|
12542
|
+
} catch {}
|
|
12468
12543
|
return {
|
|
12469
12544
|
intentId: current.id,
|
|
12470
12545
|
status: "completed",
|
|
@@ -12506,7 +12581,7 @@ var ActivationRuntime = class {
|
|
|
12506
12581
|
}
|
|
12507
12582
|
}
|
|
12508
12583
|
if (document.kind === "rule") {
|
|
12509
|
-
const active =
|
|
12584
|
+
const active = this.#versions().activeVersion("rule", document.rule.ruleId);
|
|
12510
12585
|
if (latest.replacement_of_version === null && active !== null) throw new CommsError("TRANSIENT", "the active rule pointer changed during activation", { details: { reason: "ACTIVATION_COMPLETING" } });
|
|
12511
12586
|
if (latest.replacement_of_version !== null && (active === null || `${document.rule.ruleId}@${active.version}` !== latest.replacement_of_version)) {
|
|
12512
12587
|
this.#cancel(intent.id, ACTIVATION_FAILURE.STALE_POINTER);
|
|
@@ -12594,11 +12669,11 @@ var ActivationRuntime = class {
|
|
|
12594
12669
|
}
|
|
12595
12670
|
#liveBinding(intent) {
|
|
12596
12671
|
const document = this.#document(intent);
|
|
12597
|
-
const binding = disclosureBindingFor(intent.id, document);
|
|
12598
|
-
if (binding.digest !== intent.digest || activationDocumentDigest(document) !== intent.digest) throw new CommsError("APPROVAL_VOID", "the activation document changed after preparation");
|
|
12672
|
+
const binding = disclosureBindingFor(intent.id, document, this.#layaInputs);
|
|
12673
|
+
if (binding.digest !== intent.digest || activationDocumentDigest(document, this.#layaInputs) !== intent.digest) throw new CommsError("APPROVAL_VOID", "the activation document changed after preparation");
|
|
12599
12674
|
this.#assertBoundRuleAuthority(document);
|
|
12600
12675
|
if (document.kind === "rule") {
|
|
12601
|
-
const active =
|
|
12676
|
+
const active = this.#versions().activeVersion("rule", document.rule.ruleId);
|
|
12602
12677
|
if (active !== null && (intent.replacement_of_version === null || `${document.rule.ruleId}@${active.version}` !== intent.replacement_of_version)) throw new CommsError("APPROVAL_VOID", "the rule gained an active pointer after preparation");
|
|
12603
12678
|
} else if (document.kind === "enable-all") {
|
|
12604
12679
|
const current = this.#switch();
|
|
@@ -12624,19 +12699,22 @@ var ActivationRuntime = class {
|
|
|
12624
12699
|
const expectedVersion = document.kind === "judge-budget" ? document.budget.version : document.enablement.version;
|
|
12625
12700
|
if (pointer?.kind !== expectedKind || pointer.objectId !== expectedObjectId) throw new CommsError("APPROVAL_VOID", "the judge activation pointer expectation is invalid");
|
|
12626
12701
|
if (document.kind === "judge-kind" && pointer.disableGeneration !== this.#judgeKindDisableGeneration(expectedObjectId)) assertJudgeKindEnabled(document.enablement.kind, false);
|
|
12627
|
-
const versions =
|
|
12702
|
+
const versions = this.#versions();
|
|
12628
12703
|
if ((versions.activeVersion(pointer.kind, pointer.objectId)?.version ?? null) !== pointer.version) throw new CommsError("APPROVAL_VOID", "the judge activation pointer changed after preparation");
|
|
12629
12704
|
const live = document.kind === "judge-budget" ? versions.prepareJudgeBudget(expectedVersion) : versions.prepareJudgeKind(expectedObjectId, expectedVersion);
|
|
12630
|
-
if (canonicalJson(live) !== canonicalJson(document) || activationDocumentDigest(live) !== activationDocumentDigest(document)) throw new CommsError("APPROVAL_VOID", "the immutable judge activation document changed after preparation");
|
|
12705
|
+
if (canonicalJson(live) !== canonicalJson(document) || activationDocumentDigest(live, this.#layaInputs) !== activationDocumentDigest(document, this.#layaInputs)) throw new CommsError("APPROVAL_VOID", "the immutable judge activation document changed after preparation");
|
|
12631
12706
|
}
|
|
12632
12707
|
/** A disable advances this durable fence even when the kind has no active pointer yet. */
|
|
12708
|
+
#versions() {
|
|
12709
|
+
return new ImmutableVersions(this.#store.database, this.#layaInputs);
|
|
12710
|
+
}
|
|
12633
12711
|
#judgeKindDisableGeneration(kind) {
|
|
12634
12712
|
return this.#store.database.prepare("SELECT generation FROM judge_kind_disable_generations WHERE kind = ?").get(kind)?.generation ?? 0;
|
|
12635
12713
|
}
|
|
12636
12714
|
/** Re-read every exact bound rule, including its embedded judge revocation and kind pointer, at each authority gate. */
|
|
12637
12715
|
#assertBoundRuleAuthority(document) {
|
|
12638
12716
|
if (document.kind !== "rule" && document.kind !== "enable-all") return;
|
|
12639
|
-
const versions =
|
|
12717
|
+
const versions = this.#versions();
|
|
12640
12718
|
if (document.kind === "rule") {
|
|
12641
12719
|
const live = versions.prepareRule(document.rule.ruleId, document.rule.version);
|
|
12642
12720
|
if (canonicalJson(live) !== canonicalJson(document)) throw new CommsError("APPROVAL_VOID", "the rule changed after preparation");
|
|
@@ -12647,7 +12725,7 @@ var ActivationRuntime = class {
|
|
|
12647
12725
|
/** Preparation may await local stores; the exact pointer plan captured before those awaits must still be current. */
|
|
12648
12726
|
#assertPreparationPlanLive(document, effect, replacementOfVersion) {
|
|
12649
12727
|
if (document.kind === "rule") {
|
|
12650
|
-
const active =
|
|
12728
|
+
const active = this.#versions().activeVersion("rule", document.rule.ruleId);
|
|
12651
12729
|
if ((active === null ? null : `${document.rule.ruleId}@${active.version}`) !== (replacementOfVersion ?? null) || this.#switch().generation !== effect.switchGeneration) throw new CommsError("APPROVAL_VOID", "the rule activation pointer plan changed during preparation");
|
|
12652
12730
|
return;
|
|
12653
12731
|
}
|
|
@@ -13391,7 +13469,7 @@ function resolutionHealth(database, source, accountId, category) {
|
|
|
13391
13469
|
}
|
|
13392
13470
|
};
|
|
13393
13471
|
}
|
|
13394
|
-
function record(value) {
|
|
13472
|
+
function record$1(value) {
|
|
13395
13473
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
13396
13474
|
}
|
|
13397
13475
|
function retry(value, first, next, attempts) {
|
|
@@ -13405,13 +13483,13 @@ function retry(value, first, next, attempts) {
|
|
|
13405
13483
|
}
|
|
13406
13484
|
/** Extracts only retry timing/count fields and immediately discards the decrypted source continuation. */
|
|
13407
13485
|
function retriesFromState(source, cursorScope, state, category) {
|
|
13408
|
-
const decrypted = record(state);
|
|
13486
|
+
const decrypted = record$1(state);
|
|
13409
13487
|
if (decrypted === null) return [];
|
|
13410
13488
|
if (category === "metadata") {
|
|
13411
13489
|
if (source !== "gmail" || cursorScope !== "mailbox") return [];
|
|
13412
|
-
const states = record(decrypted.messageStates);
|
|
13490
|
+
const states = record$1(decrypted.messageStates);
|
|
13413
13491
|
return states === null ? [] : Object.values(states).flatMap((message) => {
|
|
13414
|
-
const values = record(message);
|
|
13492
|
+
const values = record$1(message);
|
|
13415
13493
|
const pending = values === null ? null : retry(values, "firstFailedAt", "nextRetryAt", "attempts");
|
|
13416
13494
|
return pending === null ? [] : [pending];
|
|
13417
13495
|
});
|
|
@@ -13421,7 +13499,7 @@ function retriesFromState(source, cursorScope, state, category) {
|
|
|
13421
13499
|
return pending === null ? [] : [pending];
|
|
13422
13500
|
}
|
|
13423
13501
|
if (source === "resend" && cursorScope === "received") {
|
|
13424
|
-
const values = record(decrypted.retry);
|
|
13502
|
+
const values = record$1(decrypted.retry);
|
|
13425
13503
|
const pending = values === null ? null : retry(values, "firstFailedAt", "nextRetryAt", "attempts");
|
|
13426
13504
|
return pending === null ? [] : [pending];
|
|
13427
13505
|
}
|
|
@@ -13700,6 +13778,23 @@ async function projectE9Doctor(input) {
|
|
|
13700
13778
|
}
|
|
13701
13779
|
});
|
|
13702
13780
|
}
|
|
13781
|
+
/**
|
|
13782
|
+
* E2's successor beside the frozen doctor-v1 and doctor-v2 projections: the same v2 report, with no remaining judge
|
|
13783
|
+
* boundary, Laya's real kind state, and its content-free artifact entry from the one artifact manager.
|
|
13784
|
+
*/
|
|
13785
|
+
async function projectE2Doctor(input) {
|
|
13786
|
+
const report = await projectE9Doctor(input);
|
|
13787
|
+
return assertDoctorReportV3({
|
|
13788
|
+
...report,
|
|
13789
|
+
schemaVersion: "doctor-v3",
|
|
13790
|
+
judgeBoundary: [],
|
|
13791
|
+
judge: {
|
|
13792
|
+
...report.judge,
|
|
13793
|
+
kinds: report.judge.kinds.map((entry) => entry.kind === "laya" ? input.laya.kindEntry() : entry),
|
|
13794
|
+
laya: await input.laya.doctor()
|
|
13795
|
+
}
|
|
13796
|
+
});
|
|
13797
|
+
}
|
|
13703
13798
|
function whatsappHealth(database, live) {
|
|
13704
13799
|
const applied = new Map(database.prepare("SELECT account_id, changed_at FROM whatsapp_visibility").all().map((row) => [row.account_id, row.changed_at]));
|
|
13705
13800
|
const accountIds = new Set([...live].filter((identity) => identity.startsWith("whatsapp\0")).map((identity) => identity.slice(9)));
|
|
@@ -13757,7 +13852,8 @@ function unavailableE2() {
|
|
|
13757
13852
|
//#region src/runtime/judge-kinds.ts
|
|
13758
13853
|
function available(kind) {
|
|
13759
13854
|
if (!JUDGE_KINDS.includes(kind)) throw new CommsError("BAD_DATA", "the judge kind is not recognised");
|
|
13760
|
-
|
|
13855
|
+
const definition = JUDGE_KIND_DEFINITIONS.find((entry) => entry.kind === kind);
|
|
13856
|
+
if (definition?.availability !== "available" && definition?.implementation === void 0) throw new EventDomainError("E2_REQUIRED", "the judge kind is unavailable");
|
|
13761
13857
|
return kind;
|
|
13762
13858
|
}
|
|
13763
13859
|
/** Task 4's dormant kind authority. Public operation registration remains Task 9's job. */
|
|
@@ -13765,10 +13861,21 @@ var JudgeKinds = class {
|
|
|
13765
13861
|
#store;
|
|
13766
13862
|
#now;
|
|
13767
13863
|
#activations;
|
|
13864
|
+
#artifacts;
|
|
13768
13865
|
constructor(input) {
|
|
13769
13866
|
this.#store = input.store;
|
|
13770
13867
|
this.#now = input.now ?? Date.now;
|
|
13771
13868
|
this.#activations = input.activations;
|
|
13869
|
+
this.#artifacts = input.artifacts;
|
|
13870
|
+
}
|
|
13871
|
+
/**
|
|
13872
|
+
* The operation-facing disable. A kind with installed artifacts disables through their lifecycle, which wraps
|
|
13873
|
+
* exactly the transaction below; every other kind is that transaction alone.
|
|
13874
|
+
*/
|
|
13875
|
+
async disableKind(kind) {
|
|
13876
|
+
const enabledKind = available(kind);
|
|
13877
|
+
if (this.#artifacts !== void 0 && this.#artifacts.kind === enabledKind) return this.#artifacts.disableKind(() => this.disable(enabledKind));
|
|
13878
|
+
return this.disable(enabledKind);
|
|
13772
13879
|
}
|
|
13773
13880
|
prepareEnable(input) {
|
|
13774
13881
|
available(input.kind);
|
|
@@ -13812,6 +13919,12 @@ var B3OperatorControl = class {
|
|
|
13812
13919
|
#activations;
|
|
13813
13920
|
#judgeTest;
|
|
13814
13921
|
#holdResolver;
|
|
13922
|
+
/**
|
|
13923
|
+
* E2's artifact manager. When the owner composes it, every Laya operation answers with the real model state and a
|
|
13924
|
+
* Laya judge is created and listed like any other (B3 control v3, which keeps v2's answers); without it this control
|
|
13925
|
+
* keeps B3 v1's frozen typed `E2_REQUIRED` answers and v1's judge documents, which have no Laya variant.
|
|
13926
|
+
*/
|
|
13927
|
+
#laya;
|
|
13815
13928
|
constructor(input) {
|
|
13816
13929
|
this.#store = input.store;
|
|
13817
13930
|
this.#ports = input.ports;
|
|
@@ -13819,9 +13932,10 @@ var B3OperatorControl = class {
|
|
|
13819
13932
|
this.#activations = input.activations;
|
|
13820
13933
|
this.#judgeTest = input.judgeTest;
|
|
13821
13934
|
this.#holdResolver = input.holdResolver;
|
|
13935
|
+
this.#laya = input.laya;
|
|
13822
13936
|
}
|
|
13823
13937
|
async handle(request) {
|
|
13824
|
-
const unavailable = request.operation === "judgeModelStatus" || request.operation === "removeJudgeModel" || (request.operation === "enableJudgeKind" || request.operation === "disableJudgeKind") && request.args.kind === "laya" ? judgeUnavailable(request.operation, request.args) : void 0;
|
|
13938
|
+
const unavailable = this.#laya === void 0 && (request.operation === "judgeModelStatus" || request.operation === "removeJudgeModel" || (request.operation === "enableJudgeKind" || request.operation === "disableJudgeKind") && request.args.kind === "laya") ? judgeUnavailable(request.operation, request.args) : void 0;
|
|
13825
13939
|
if (unavailable !== void 0) return unavailable;
|
|
13826
13940
|
switch (request.operation) {
|
|
13827
13941
|
case "subscribersList": return this.subscribersList();
|
|
@@ -13833,7 +13947,7 @@ var B3OperatorControl = class {
|
|
|
13833
13947
|
case "dropDelivery": return this.#ports.quiescer.withShared(async () => this.dropDelivery(requiredText$1(request.args.deliveryId, "delivery id")));
|
|
13834
13948
|
case "judgesList": return this.judgesList();
|
|
13835
13949
|
case "addJudge":
|
|
13836
|
-
case "updateJudge": return this.#ports.quiescer.withShared(async () => this.#store.immediate(() =>
|
|
13950
|
+
case "updateJudge": return this.#ports.quiescer.withShared(async () => this.#store.immediate(() => this.#judgeVersions().createJudge(request.args.document)));
|
|
13837
13951
|
case "removeJudge": return this.removeJudge(requiredText$1(request.args.judgeId, "judge id"));
|
|
13838
13952
|
case "judgeTest":
|
|
13839
13953
|
if (this.#judgeTest === void 0) throw new CommsError("CONFIG", "judge test is not composed for this operator control");
|
|
@@ -13846,6 +13960,8 @@ var B3OperatorControl = class {
|
|
|
13846
13960
|
case "judgeKindsList": return this.judgeKindsList();
|
|
13847
13961
|
case "enableJudgeKind": return this.enableJudgeKind(requiredText$1(request.args.kind, "judge kind"));
|
|
13848
13962
|
case "disableJudgeKind": return this.disableJudgeKind(requiredText$1(request.args.kind, "judge kind"));
|
|
13963
|
+
case "judgeModelStatus": return this.requireLaya().status();
|
|
13964
|
+
case "removeJudgeModel": return this.requireLaya().remove();
|
|
13849
13965
|
case "heldList": return this.heldList();
|
|
13850
13966
|
case "decideHeld":
|
|
13851
13967
|
if (this.#holdResolver === void 0) throw new CommsError("CONFIG", "held resolution has not been composed for this operator control");
|
|
@@ -13856,6 +13972,14 @@ var B3OperatorControl = class {
|
|
|
13856
13972
|
default: throw new CommsError("USAGE", `the local B3 operator control does not implement ${request.operation}`);
|
|
13857
13973
|
}
|
|
13858
13974
|
}
|
|
13975
|
+
/** The judge documents this control answers: B3 v3's with E2's artifact manager, B3 v1's without it. */
|
|
13976
|
+
#judgeVersions() {
|
|
13977
|
+
return this.#laya === void 0 ? new ImmutableVersions(this.#store.database) : ImmutableVersions.forJudgeContract(this.#store.database, "b3-control-v3");
|
|
13978
|
+
}
|
|
13979
|
+
requireLaya() {
|
|
13980
|
+
if (this.#laya === void 0) throw new CommsError("CONFIG", "the Laya artifact manager is not composed for this operator control");
|
|
13981
|
+
return this.#laya;
|
|
13982
|
+
}
|
|
13859
13983
|
requireActivations() {
|
|
13860
13984
|
if (this.#activations === void 0) throw new CommsError("CONFIG", "judge activation is not composed for this operator control");
|
|
13861
13985
|
return this.#activations;
|
|
@@ -13879,6 +14003,7 @@ var B3OperatorControl = class {
|
|
|
13879
14003
|
}
|
|
13880
14004
|
judgeKindsList() {
|
|
13881
14005
|
return JUDGE_KINDS.map((kind) => {
|
|
14006
|
+
if (kind === "laya" && this.#laya !== void 0) return this.#laya.kindEntry();
|
|
13882
14007
|
if (JUDGE_KIND_DEFINITIONS.find((entry) => entry.kind === kind)?.availability === "unavailable") return {
|
|
13883
14008
|
kind: JUDGE_KIND_LAYA,
|
|
13884
14009
|
state: "unavailable",
|
|
@@ -13899,6 +14024,18 @@ var B3OperatorControl = class {
|
|
|
13899
14024
|
}
|
|
13900
14025
|
async enableJudgeKind(kind) {
|
|
13901
14026
|
const activations = this.requireActivations();
|
|
14027
|
+
if (kind === "laya" && this.#laya !== void 0) {
|
|
14028
|
+
const prepared = this.#laya.enablementVersion();
|
|
14029
|
+
if (!("version" in prepared)) return prepared;
|
|
14030
|
+
return new JudgeKinds({
|
|
14031
|
+
store: this.#store,
|
|
14032
|
+
now: this.#now,
|
|
14033
|
+
activations
|
|
14034
|
+
}).prepareEnable({
|
|
14035
|
+
kind: JUDGE_KIND_LAYA,
|
|
14036
|
+
version: prepared.version
|
|
14037
|
+
});
|
|
14038
|
+
}
|
|
13902
14039
|
const available = availableJudgeKind(kind);
|
|
13903
14040
|
const version = this.#store.immediate(() => {
|
|
13904
14041
|
const latest = this.#store.database.prepare("SELECT max(version) AS version FROM judge_kind_versions WHERE kind = ? AND revoked_at IS NULL").get(available);
|
|
@@ -13929,8 +14066,9 @@ var B3OperatorControl = class {
|
|
|
13929
14066
|
return new JudgeKinds({
|
|
13930
14067
|
store: this.#store,
|
|
13931
14068
|
now: this.#now,
|
|
13932
|
-
activations
|
|
13933
|
-
|
|
14069
|
+
activations,
|
|
14070
|
+
artifacts: this.#laya
|
|
14071
|
+
}).disableKind(kind);
|
|
13934
14072
|
}
|
|
13935
14073
|
async removeJudge(judgeId) {
|
|
13936
14074
|
return this.#store.immediate(() => {
|
|
@@ -13984,14 +14122,17 @@ var B3OperatorControl = class {
|
|
|
13984
14122
|
return this.#store.database.prepare("SELECT judge_id, version, document, revoked_at FROM judge_versions ORDER BY judge_id, version").all().flatMap((row) => {
|
|
13985
14123
|
try {
|
|
13986
14124
|
const judge = canonicalJudge(JSON.parse(row.document));
|
|
13987
|
-
|
|
14125
|
+
const entry = {
|
|
13988
14126
|
judgeId: row.judge_id,
|
|
13989
14127
|
version: row.version,
|
|
13990
14128
|
kind: judge.kind,
|
|
13991
14129
|
provider: judge.provider,
|
|
13992
14130
|
model: judge.model,
|
|
13993
14131
|
revokedAt: row.revoked_at
|
|
13994
|
-
}
|
|
14132
|
+
};
|
|
14133
|
+
if (this.#laya !== void 0) return [entry];
|
|
14134
|
+
if (judge.kind === "laya") return [];
|
|
14135
|
+
return [entry];
|
|
13995
14136
|
} catch {
|
|
13996
14137
|
return [];
|
|
13997
14138
|
}
|
|
@@ -15454,19 +15595,27 @@ const systemAddressResolver = { async lookup(hostname) {
|
|
|
15454
15595
|
})).map((answer) => answer.address);
|
|
15455
15596
|
} };
|
|
15456
15597
|
/** A literal host is its singleton answer; a hostname is resolved afresh by the injected resolver. */
|
|
15457
|
-
async function resolveHost(hostname, resolver = systemAddressResolver) {
|
|
15598
|
+
async function resolveHost(hostname, resolver = systemAddressResolver, options = {}) {
|
|
15458
15599
|
try {
|
|
15459
15600
|
return [parseIpLiteral(hostname).address];
|
|
15460
15601
|
} catch (error) {
|
|
15461
15602
|
if (!(error instanceof AddressPolicyError)) throw error;
|
|
15462
15603
|
}
|
|
15463
|
-
const answers = await resolver.lookup(hostname);
|
|
15604
|
+
const answers = await resolver.lookup(hostname, options);
|
|
15464
15605
|
if (answers.length === 0) throw new AddressPolicyError(`resolver returned no addresses for ${hostname}`);
|
|
15465
15606
|
return answers;
|
|
15466
15607
|
}
|
|
15467
15608
|
//#endregion
|
|
15468
15609
|
//#region src/network/pinned-connection.ts
|
|
15469
15610
|
const TYPESAFE_GLOBAL_ONLY_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
15611
|
+
var PinnedResponseError = class extends Error {
|
|
15612
|
+
code;
|
|
15613
|
+
constructor(code) {
|
|
15614
|
+
super(`pinned network response refused: ${code}`);
|
|
15615
|
+
this.name = "PinnedResponseError";
|
|
15616
|
+
this.code = code;
|
|
15617
|
+
}
|
|
15618
|
+
};
|
|
15470
15619
|
function hostWithoutBrackets(hostname) {
|
|
15471
15620
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
15472
15621
|
}
|
|
@@ -15497,6 +15646,9 @@ function assertPinnedUrl(url) {
|
|
|
15497
15646
|
if (url.username !== "" || url.password !== "") throw new AddressPolicyError("network URL userinfo is refused");
|
|
15498
15647
|
if (url.hash !== "") throw new AddressPolicyError("network URL fragments are refused");
|
|
15499
15648
|
}
|
|
15649
|
+
function abortReason(signal) {
|
|
15650
|
+
return signal.reason instanceof Error ? signal.reason : new PinnedResponseError("NETWORK_ABORTED");
|
|
15651
|
+
}
|
|
15500
15652
|
/** Resolves all answers, validates them all, and selects a literal only after the complete set passes. */
|
|
15501
15653
|
async function preparePinnedConnection(options) {
|
|
15502
15654
|
const url = typeof options.url === "string" ? new URL(options.url) : new URL(options.url.href);
|
|
@@ -15513,7 +15665,7 @@ async function preparePinnedConnection(options) {
|
|
|
15513
15665
|
const literal = isLiteral(hostname);
|
|
15514
15666
|
if (url.protocol === "http:" && (!literal || hostname !== "127.0.0.1" && hostname !== "::1")) throw new AddressPolicyError("HTTP requires literal 127.0.0.1 or ::1");
|
|
15515
15667
|
if (url.protocol === "http:" && !globalOnly && !isSoleApprovedLiteral(options.approvedAddressSet, hostname)) throw new AddressPolicyError("HTTP requires its one exact approved literal loopback address");
|
|
15516
|
-
const addresses = (await resolveHost(hostname, options.resolver ?? systemAddressResolver)).map((answer) => assertAddressAllowed(answer, options)).map((answer) => answer.address).sort((left, right) => left.localeCompare(right));
|
|
15668
|
+
const addresses = (await resolveHost(hostname, options.resolver ?? systemAddressResolver, { signal: options.signal })).map((answer) => assertAddressAllowed(answer, options)).map((answer) => answer.address).sort((left, right) => left.localeCompare(right));
|
|
15517
15669
|
if (addresses.length === 0) throw new AddressPolicyError("resolver returned no vetted addresses");
|
|
15518
15670
|
const [address] = addresses;
|
|
15519
15671
|
if (address === void 0) throw new AddressPolicyError("resolver returned no selected address");
|
|
@@ -15545,36 +15697,160 @@ function requestBytesForPinnedConnection(connection, method, body = Buffer.alloc
|
|
|
15545
15697
|
const supplied = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
15546
15698
|
return Buffer.concat([Buffer.from(`${method} ${pathname} HTTP/1.1\r\nHost: ${connection.hostHeader}\r\nContent-Length: ${body.byteLength}\r\nConnection: close\r\n${supplied.join("\r\n")}\r\n\r\n`), Buffer.from(body)]);
|
|
15547
15699
|
}
|
|
15700
|
+
/**
|
|
15701
|
+
* Generates a no-body raw GET for a fixed-purpose caller. The caller's header
|
|
15702
|
+
* policy is deliberately outside B2; B2 only guarantees that these exact raw
|
|
15703
|
+
* bytes go over a previously pinned connection.
|
|
15704
|
+
*/
|
|
15705
|
+
function getRequestBytesForPinnedConnection(connection, headers) {
|
|
15706
|
+
const pathname = `${connection.url.pathname}${connection.url.search}` || "/";
|
|
15707
|
+
const supplied = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
15708
|
+
return Buffer.from(`GET ${pathname} HTTP/1.1\r\nHost: ${connection.hostHeader}\r\nConnection: close\r\n${supplied.join("\r\n")}\r\n\r\n`, "latin1");
|
|
15709
|
+
}
|
|
15710
|
+
/** Streams one bounded raw response without retaining its bytes in memory. */
|
|
15711
|
+
function streamPinnedResponseTo(socket, request, options) {
|
|
15712
|
+
const { maxBytes, onChunk, signal } = options;
|
|
15713
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) throw new PinnedResponseError("NETWORK_RESPONSE_TOO_LARGE");
|
|
15714
|
+
for (const deadline of [options.idleTimeoutMs, options.totalTimeoutMs]) if (deadline !== void 0 && (!Number.isSafeInteger(deadline) || deadline < 1)) throw new PinnedResponseError("NETWORK_TIMEOUT_INVALID");
|
|
15715
|
+
return new Promise((resolve, reject) => {
|
|
15716
|
+
let total = 0;
|
|
15717
|
+
let terminal;
|
|
15718
|
+
let ended = false;
|
|
15719
|
+
let pending = Promise.resolve();
|
|
15720
|
+
let idleTimer;
|
|
15721
|
+
let totalTimer;
|
|
15722
|
+
const onError = (error) => fail(error);
|
|
15723
|
+
const onAbort = () => fail(signal === void 0 ? new PinnedResponseError("NETWORK_ABORTED") : abortReason(signal));
|
|
15724
|
+
const clearTimers = () => {
|
|
15725
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
15726
|
+
if (totalTimer !== void 0) clearTimeout(totalTimer);
|
|
15727
|
+
};
|
|
15728
|
+
const resetIdleDeadline = () => {
|
|
15729
|
+
if (options.idleTimeoutMs === void 0) return;
|
|
15730
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
15731
|
+
idleTimer = setTimeout(() => fail(new PinnedResponseError("NETWORK_IDLE_TIMEOUT")), options.idleTimeoutMs);
|
|
15732
|
+
};
|
|
15733
|
+
const cleanup = () => {
|
|
15734
|
+
clearTimers();
|
|
15735
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15736
|
+
socket.off("error", onError);
|
|
15737
|
+
};
|
|
15738
|
+
const finish = () => {
|
|
15739
|
+
if (terminal !== void 0) return;
|
|
15740
|
+
terminal = null;
|
|
15741
|
+
cleanup();
|
|
15742
|
+
pending.then(() => resolve());
|
|
15743
|
+
};
|
|
15744
|
+
const fail = (error) => {
|
|
15745
|
+
if (terminal !== void 0) return;
|
|
15746
|
+
terminal = error;
|
|
15747
|
+
cleanup();
|
|
15748
|
+
socket.destroy();
|
|
15749
|
+
pending.then(() => reject(error));
|
|
15750
|
+
};
|
|
15751
|
+
socket.once("error", onError);
|
|
15752
|
+
if (signal?.aborted === true) {
|
|
15753
|
+
fail(new PinnedResponseError("NETWORK_ABORTED"));
|
|
15754
|
+
return;
|
|
15755
|
+
}
|
|
15756
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15757
|
+
resetIdleDeadline();
|
|
15758
|
+
if (options.totalTimeoutMs !== void 0) totalTimer = setTimeout(() => fail(new PinnedResponseError("NETWORK_TOTAL_TIMEOUT")), options.totalTimeoutMs);
|
|
15759
|
+
socket.on("data", (chunk) => {
|
|
15760
|
+
if (terminal !== void 0) return;
|
|
15761
|
+
resetIdleDeadline();
|
|
15762
|
+
total += chunk.byteLength;
|
|
15763
|
+
if (total > maxBytes) {
|
|
15764
|
+
fail(new PinnedResponseError("NETWORK_RESPONSE_TOO_LARGE"));
|
|
15765
|
+
return;
|
|
15766
|
+
}
|
|
15767
|
+
socket.pause();
|
|
15768
|
+
pending = pending.then(() => onChunk(Buffer.from(chunk))).then(() => {
|
|
15769
|
+
if (terminal === void 0) socket.resume();
|
|
15770
|
+
if (ended) finish();
|
|
15771
|
+
}, (error) => fail(error instanceof Error ? error : /* @__PURE__ */ new Error("raw pinned response sink failed")));
|
|
15772
|
+
});
|
|
15773
|
+
socket.once("end", () => {
|
|
15774
|
+
ended = true;
|
|
15775
|
+
pending.then(() => finish());
|
|
15776
|
+
});
|
|
15777
|
+
socket.once("close", () => {
|
|
15778
|
+
if (terminal === void 0) fail(new PinnedResponseError("NETWORK_RESPONSE_INCOMPLETE"));
|
|
15779
|
+
});
|
|
15780
|
+
try {
|
|
15781
|
+
socket.write(request);
|
|
15782
|
+
} catch (error) {
|
|
15783
|
+
fail(error instanceof Error ? error : /* @__PURE__ */ new Error("raw pinned request write failed"));
|
|
15784
|
+
}
|
|
15785
|
+
});
|
|
15786
|
+
}
|
|
15548
15787
|
/** Opens the selected literal TCP peer. Callers place their final authority fence immediately before this call. */
|
|
15549
15788
|
function connectPinnedTcp(options) {
|
|
15550
15789
|
return new Promise((resolve, reject) => {
|
|
15790
|
+
if (options.signal?.aborted === true) {
|
|
15791
|
+
reject(abortReason(options.signal));
|
|
15792
|
+
return;
|
|
15793
|
+
}
|
|
15551
15794
|
const socket = net.connect({
|
|
15552
15795
|
host: options.host,
|
|
15553
15796
|
port: options.port
|
|
15554
15797
|
});
|
|
15555
|
-
socket.once("error", reject);
|
|
15556
15798
|
installErrorSink(socket);
|
|
15557
|
-
|
|
15558
|
-
|
|
15799
|
+
const cleanup = () => {
|
|
15800
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
15801
|
+
socket.off("error", onError);
|
|
15802
|
+
socket.off("connect", onConnect);
|
|
15803
|
+
};
|
|
15804
|
+
const fail = (error) => {
|
|
15805
|
+
cleanup();
|
|
15806
|
+
socket.destroy();
|
|
15807
|
+
reject(error);
|
|
15808
|
+
};
|
|
15809
|
+
const onError = (error) => fail(error);
|
|
15810
|
+
const onAbort = () => fail(options.signal === void 0 ? new PinnedResponseError("NETWORK_ABORTED") : abortReason(options.signal));
|
|
15811
|
+
const onConnect = () => {
|
|
15812
|
+
cleanup();
|
|
15559
15813
|
resolve(socket);
|
|
15560
|
-
}
|
|
15814
|
+
};
|
|
15815
|
+
socket.once("error", onError);
|
|
15816
|
+
socket.once("connect", onConnect);
|
|
15817
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15561
15818
|
});
|
|
15562
15819
|
}
|
|
15563
15820
|
/** Starts TLS only over the already-pinned TCP socket. Callers fence immediately before this ClientHello. */
|
|
15564
15821
|
function connectPinnedTls(options) {
|
|
15565
15822
|
return new Promise((resolve, reject) => {
|
|
15823
|
+
if (options.signal?.aborted === true) {
|
|
15824
|
+
options.socket.destroy();
|
|
15825
|
+
reject(abortReason(options.signal));
|
|
15826
|
+
return;
|
|
15827
|
+
}
|
|
15566
15828
|
const socket = tls.connect({
|
|
15567
15829
|
socket: options.socket,
|
|
15568
15830
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
15569
15831
|
...options.servername === void 0 ? {} : { servername: options.servername },
|
|
15570
15832
|
checkServerIdentity: (_name, certificate) => tls.checkServerIdentity(options.certificateHost, certificate)
|
|
15571
15833
|
});
|
|
15572
|
-
socket.once("error", reject);
|
|
15573
15834
|
installErrorSink(socket);
|
|
15574
|
-
|
|
15575
|
-
|
|
15835
|
+
const cleanup = () => {
|
|
15836
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
15837
|
+
socket.off("error", onError);
|
|
15838
|
+
socket.off("secureConnect", onSecureConnect);
|
|
15839
|
+
};
|
|
15840
|
+
const fail = (error) => {
|
|
15841
|
+
cleanup();
|
|
15842
|
+
socket.destroy();
|
|
15843
|
+
reject(error);
|
|
15844
|
+
};
|
|
15845
|
+
const onError = (error) => fail(error);
|
|
15846
|
+
const onAbort = () => fail(options.signal === void 0 ? new PinnedResponseError("NETWORK_ABORTED") : abortReason(options.signal));
|
|
15847
|
+
const onSecureConnect = () => {
|
|
15848
|
+
cleanup();
|
|
15576
15849
|
resolve(socket);
|
|
15577
|
-
}
|
|
15850
|
+
};
|
|
15851
|
+
socket.once("error", onError);
|
|
15852
|
+
socket.once("secureConnect", onSecureConnect);
|
|
15853
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15578
15854
|
});
|
|
15579
15855
|
}
|
|
15580
15856
|
//#endregion
|
|
@@ -15982,6 +16258,462 @@ var JudgeTransport = class {
|
|
|
15982
16258
|
}
|
|
15983
16259
|
}
|
|
15984
16260
|
};
|
|
16261
|
+
/**
|
|
16262
|
+
* Each worker's owner-only working and proof directory is a fresh `mkdtemp` beneath `/tmp`, which is sticky and owned
|
|
16263
|
+
* by root as B1-G's socket-directory check accepts. Task 7's probe binds `<directory>/laya-boundary-XXXXXX/
|
|
16264
|
+
* boundary.sock`, 35 bytes beyond it, so this 27-byte directory keeps the socket at 62 bytes, inside macOS's 103 for
|
|
16265
|
+
* any home: core's data directory fits only while the home is at most 22 bytes.
|
|
16266
|
+
*/
|
|
16267
|
+
const LAYA_WORKER_DIRECTORY_PARENT = "/tmp";
|
|
16268
|
+
const LAYA_WORKER_DIRECTORY_PREFIX = "agentcomms-laya-";
|
|
16269
|
+
const BINDING_FIELDS = [
|
|
16270
|
+
"nonce",
|
|
16271
|
+
"pid",
|
|
16272
|
+
"profile",
|
|
16273
|
+
"entry",
|
|
16274
|
+
"pipeGeneration"
|
|
16275
|
+
];
|
|
16276
|
+
function sameSelection(current, started) {
|
|
16277
|
+
return current !== null && current.epoch === started.epoch && current.version === started.version && current.digest === started.digest && current.manifestId === started.manifestId;
|
|
16278
|
+
}
|
|
16279
|
+
function exactFrame(frame, keys) {
|
|
16280
|
+
const found = Object.keys(frame).sort();
|
|
16281
|
+
const expected = [...keys].sort();
|
|
16282
|
+
return found.length === expected.length && found.every((key, index) => key === expected[index]);
|
|
16283
|
+
}
|
|
16284
|
+
function bound(frame, binding) {
|
|
16285
|
+
return BINDING_FIELDS.every((field) => frame[field] === binding[field]);
|
|
16286
|
+
}
|
|
16287
|
+
/** The worker's reply, in its closed shapes only: a finite value in [0, 1], never clamped, and its token count. */
|
|
16288
|
+
function replyFrame(frame, binding, requestId) {
|
|
16289
|
+
if (!bound(frame, binding) || frame.requestId !== requestId) return null;
|
|
16290
|
+
if (frame.type === "answer") {
|
|
16291
|
+
if (!exactFrame(frame, [
|
|
16292
|
+
"type",
|
|
16293
|
+
...BINDING_FIELDS,
|
|
16294
|
+
"requestId",
|
|
16295
|
+
"value",
|
|
16296
|
+
"inputTokens"
|
|
16297
|
+
])) return null;
|
|
16298
|
+
const { value, inputTokens } = frame;
|
|
16299
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) return null;
|
|
16300
|
+
if (!Number.isSafeInteger(inputTokens) || inputTokens < 1) return null;
|
|
16301
|
+
if (inputTokens > 512) return null;
|
|
16302
|
+
return {
|
|
16303
|
+
kind: "answer",
|
|
16304
|
+
value,
|
|
16305
|
+
inputTokens
|
|
16306
|
+
};
|
|
16307
|
+
}
|
|
16308
|
+
if (frame.type !== "refused" || !exactFrame(frame, [
|
|
16309
|
+
"type",
|
|
16310
|
+
...BINDING_FIELDS,
|
|
16311
|
+
"requestId",
|
|
16312
|
+
"code"
|
|
16313
|
+
])) return null;
|
|
16314
|
+
if (frame.code === "token-limit") return {
|
|
16315
|
+
kind: "refused",
|
|
16316
|
+
code: "laya-token-limit",
|
|
16317
|
+
preIo: false
|
|
16318
|
+
};
|
|
16319
|
+
if (frame.code === "malformed") return {
|
|
16320
|
+
kind: "refused",
|
|
16321
|
+
code: "laya-malformed",
|
|
16322
|
+
preIo: false
|
|
16323
|
+
};
|
|
16324
|
+
if (frame.code === "template-blocked") return {
|
|
16325
|
+
kind: "refused",
|
|
16326
|
+
code: "laya-template-blocked",
|
|
16327
|
+
preIo: false
|
|
16328
|
+
};
|
|
16329
|
+
return null;
|
|
16330
|
+
}
|
|
16331
|
+
function loadFrame(frame, binding) {
|
|
16332
|
+
if (!bound(frame, binding)) return null;
|
|
16333
|
+
if (frame.type === "loaded" && exactFrame(frame, ["type", ...BINDING_FIELDS])) return "loaded";
|
|
16334
|
+
if (frame.type === "load-refused" && exactFrame(frame, [
|
|
16335
|
+
"type",
|
|
16336
|
+
...BINDING_FIELDS,
|
|
16337
|
+
"code"
|
|
16338
|
+
])) return "refused";
|
|
16339
|
+
return null;
|
|
16340
|
+
}
|
|
16341
|
+
function refusedBefore(code) {
|
|
16342
|
+
return { refused: {
|
|
16343
|
+
kind: "refused",
|
|
16344
|
+
code,
|
|
16345
|
+
preIo: true
|
|
16346
|
+
} };
|
|
16347
|
+
}
|
|
16348
|
+
/**
|
|
16349
|
+
* The one isolated Laya worker (E2-7, D11): started lazily, one request at a time, and killed on a 30-second
|
|
16350
|
+
* deadline, a crash, any frame outside the protocol, a purge of its in-flight reservation, a kind disable or update
|
|
16351
|
+
* tightening, the owner's stop, and after ten idle minutes. It holds no database, account, event, target or secret:
|
|
16352
|
+
* E's coordinator reserves, gates and settles, and this supervisor moves only one bounded request to the child and
|
|
16353
|
+
* its bounded answer back. A later call after any kill starts a fresh child, proved again from the beginning.
|
|
16354
|
+
*/
|
|
16355
|
+
var LayaWorkerSupervisor = class {
|
|
16356
|
+
#artifacts;
|
|
16357
|
+
#startWorker;
|
|
16358
|
+
#requestDeadlineMs;
|
|
16359
|
+
#idleExitMs;
|
|
16360
|
+
#loadDeadlineMs;
|
|
16361
|
+
#detach;
|
|
16362
|
+
#queue = Promise.resolve();
|
|
16363
|
+
#worker;
|
|
16364
|
+
/** Bumped by every stop: a start or request that began before it never adopts or keeps a worker. */
|
|
16365
|
+
#generation = 0;
|
|
16366
|
+
#stopping = new AbortController();
|
|
16367
|
+
constructor(options) {
|
|
16368
|
+
this.#artifacts = options.artifacts;
|
|
16369
|
+
this.#startWorker = options.startWorker ?? ((start) => createLayaWorkerBoundary(start));
|
|
16370
|
+
this.#requestDeadlineMs = options.requestDeadlineMs ?? 3e4;
|
|
16371
|
+
this.#idleExitMs = options.idleExitMs ?? 6e5;
|
|
16372
|
+
this.#loadDeadlineMs = options.loadDeadlineMs ?? 12e4;
|
|
16373
|
+
this.#detach = options.artifacts.attachWorker(this);
|
|
16374
|
+
}
|
|
16375
|
+
availability() {
|
|
16376
|
+
return this.#artifacts.availability();
|
|
16377
|
+
}
|
|
16378
|
+
/**
|
|
16379
|
+
* Before E reserves anything: this release must offer Laya here, the enabled entry must be the judge's own manifest
|
|
16380
|
+
* and list the judge's template, and the request must fit one frame. The body is what E reserves against.
|
|
16381
|
+
*/
|
|
16382
|
+
prepare(input) {
|
|
16383
|
+
const availability = this.#artifacts.availability();
|
|
16384
|
+
if (!availability.available) return {
|
|
16385
|
+
ok: false,
|
|
16386
|
+
code: availability.code
|
|
16387
|
+
};
|
|
16388
|
+
const selection = this.#artifacts.workerSelection();
|
|
16389
|
+
if (selection === null) return {
|
|
16390
|
+
ok: false,
|
|
16391
|
+
code: "laya-not-ready"
|
|
16392
|
+
};
|
|
16393
|
+
const templateId = input.judge.promptTemplate;
|
|
16394
|
+
if (input.judge.model !== selection.manifestId || !isLayaTemplateId(templateId) || !selection.promptTemplates.includes(templateId) || input.state.length === 0 || input.instructions.length === 0) return {
|
|
16395
|
+
ok: false,
|
|
16396
|
+
code: "laya-adapter-blocked"
|
|
16397
|
+
};
|
|
16398
|
+
const body = Buffer.from(JSON.stringify({
|
|
16399
|
+
templateId,
|
|
16400
|
+
instructions: input.instructions,
|
|
16401
|
+
state: input.state
|
|
16402
|
+
}));
|
|
16403
|
+
if (body.length > 61440) return {
|
|
16404
|
+
ok: false,
|
|
16405
|
+
code: "laya-request-too-large"
|
|
16406
|
+
};
|
|
16407
|
+
return {
|
|
16408
|
+
ok: true,
|
|
16409
|
+
request: {
|
|
16410
|
+
templateId,
|
|
16411
|
+
instructions: input.instructions,
|
|
16412
|
+
state: input.state,
|
|
16413
|
+
body
|
|
16414
|
+
}
|
|
16415
|
+
};
|
|
16416
|
+
}
|
|
16417
|
+
/**
|
|
16418
|
+
* One request at a time: the next one's frame is not written until this one has settled. Work is tracked by its
|
|
16419
|
+
* E reservation from the moment it is handed over, so a purge reaches it while it waits, too.
|
|
16420
|
+
*/
|
|
16421
|
+
execute(request, hooks) {
|
|
16422
|
+
const tracked = this.#artifacts.trackWork(hooks.reservationId);
|
|
16423
|
+
const run = this.#queue.then(() => this.#execute(request, hooks, tracked.signal));
|
|
16424
|
+
this.#queue = run.then(() => void 0, () => void 0);
|
|
16425
|
+
return run.finally(() => tracked.release());
|
|
16426
|
+
}
|
|
16427
|
+
/** Kills the worker — a kind disable, an update tightening, the owner's stop — and fails whatever waited on it. */
|
|
16428
|
+
async stop() {
|
|
16429
|
+
this.#generation += 1;
|
|
16430
|
+
const stopping = this.#stopping;
|
|
16431
|
+
this.#stopping = new AbortController();
|
|
16432
|
+
stopping.abort();
|
|
16433
|
+
const worker = this.#worker;
|
|
16434
|
+
this.#worker = void 0;
|
|
16435
|
+
if (worker !== void 0) await this.#kill(worker, "laya-stopped");
|
|
16436
|
+
}
|
|
16437
|
+
/** The owner's final stop: also detaches from the manager. */
|
|
16438
|
+
async close() {
|
|
16439
|
+
await this.stop();
|
|
16440
|
+
this.#detach();
|
|
16441
|
+
}
|
|
16442
|
+
async #execute(request, hooks, signal) {
|
|
16443
|
+
const generation = this.#generation;
|
|
16444
|
+
const stopping = this.#stopping.signal;
|
|
16445
|
+
if (signal.aborted) return {
|
|
16446
|
+
kind: "refused",
|
|
16447
|
+
code: "laya-purged",
|
|
16448
|
+
preIo: true
|
|
16449
|
+
};
|
|
16450
|
+
const started = await this.#ensureWorker(generation, signal, stopping);
|
|
16451
|
+
if ("refused" in started) return started.refused;
|
|
16452
|
+
const worker = started.worker;
|
|
16453
|
+
if (signal.aborted) return {
|
|
16454
|
+
kind: "refused",
|
|
16455
|
+
code: "laya-purged",
|
|
16456
|
+
preIo: true
|
|
16457
|
+
};
|
|
16458
|
+
if (generation !== this.#generation || this.#worker !== worker) return {
|
|
16459
|
+
kind: "refused",
|
|
16460
|
+
code: "laya-stopped",
|
|
16461
|
+
preIo: true
|
|
16462
|
+
};
|
|
16463
|
+
if (!sameSelection(this.#artifacts.workerSelection(), worker.selection)) return {
|
|
16464
|
+
kind: "refused",
|
|
16465
|
+
code: "laya-authority-changed",
|
|
16466
|
+
preIo: true
|
|
16467
|
+
};
|
|
16468
|
+
const requestId = createWorkerRequestId();
|
|
16469
|
+
let bytes;
|
|
16470
|
+
try {
|
|
16471
|
+
bytes = encodeWorkerFrame({
|
|
16472
|
+
type: "request",
|
|
16473
|
+
...worker.binding,
|
|
16474
|
+
requestId,
|
|
16475
|
+
templateId: request.templateId,
|
|
16476
|
+
instructions: request.instructions,
|
|
16477
|
+
state: request.state
|
|
16478
|
+
});
|
|
16479
|
+
} catch {
|
|
16480
|
+
return {
|
|
16481
|
+
kind: "refused",
|
|
16482
|
+
code: "laya-request-too-large",
|
|
16483
|
+
preIo: true
|
|
16484
|
+
};
|
|
16485
|
+
}
|
|
16486
|
+
this.#clearIdle(worker);
|
|
16487
|
+
const reply = this.#expect(worker, this.#requestDeadlineMs, signal, stopping, (frame) => replyFrame(frame, worker.binding, requestId));
|
|
16488
|
+
try {
|
|
16489
|
+
hooks.finalGate();
|
|
16490
|
+
} catch {
|
|
16491
|
+
reply.cancel();
|
|
16492
|
+
this.#armIdle(worker);
|
|
16493
|
+
return {
|
|
16494
|
+
kind: "refused",
|
|
16495
|
+
code: "laya-authority-changed",
|
|
16496
|
+
preIo: true
|
|
16497
|
+
};
|
|
16498
|
+
}
|
|
16499
|
+
worker.child.stdin.write(bytes);
|
|
16500
|
+
const result = await reply.settled;
|
|
16501
|
+
if ("gone" in result) {
|
|
16502
|
+
await this.#kill(worker, result.gone);
|
|
16503
|
+
return {
|
|
16504
|
+
kind: "refused",
|
|
16505
|
+
code: result.gone,
|
|
16506
|
+
preIo: false
|
|
16507
|
+
};
|
|
16508
|
+
}
|
|
16509
|
+
this.#armIdle(worker);
|
|
16510
|
+
return result.value;
|
|
16511
|
+
}
|
|
16512
|
+
async #ensureWorker(generation, signal, stopping) {
|
|
16513
|
+
const running = this.#worker;
|
|
16514
|
+
if (running !== void 0) return { worker: running };
|
|
16515
|
+
const availability = this.#artifacts.availability();
|
|
16516
|
+
if (!availability.available) return refusedBefore(availability.code);
|
|
16517
|
+
const selection = this.#artifacts.workerSelection();
|
|
16518
|
+
if (selection === null) return refusedBefore("laya-not-ready");
|
|
16519
|
+
const verified = await this.#artifacts.verifyForWorkerStart();
|
|
16520
|
+
if (!verified.ok) return refusedBefore(verified.failureCode ?? "laya-not-ready");
|
|
16521
|
+
if (generation !== this.#generation || stopping.aborted) return refusedBefore("laya-stopped");
|
|
16522
|
+
if (signal.aborted) return refusedBefore("laya-purged");
|
|
16523
|
+
if (!sameSelection(this.#artifacts.workerSelection(), selection)) return refusedBefore("laya-authority-changed");
|
|
16524
|
+
let directory;
|
|
16525
|
+
try {
|
|
16526
|
+
directory = await mkdtemp(join(LAYA_WORKER_DIRECTORY_PARENT, LAYA_WORKER_DIRECTORY_PREFIX));
|
|
16527
|
+
} catch {
|
|
16528
|
+
return refusedBefore("boundary-failed");
|
|
16529
|
+
}
|
|
16530
|
+
const proof = new AbortController();
|
|
16531
|
+
const abortProof = () => proof.abort();
|
|
16532
|
+
signal.addEventListener("abort", abortProof, { once: true });
|
|
16533
|
+
stopping.addEventListener("abort", abortProof, { once: true });
|
|
16534
|
+
if (signal.aborted || stopping.aborted) proof.abort();
|
|
16535
|
+
let boundary;
|
|
16536
|
+
try {
|
|
16537
|
+
boundary = await this.#startWorker({
|
|
16538
|
+
dataDirectory: directory,
|
|
16539
|
+
signal: proof.signal
|
|
16540
|
+
});
|
|
16541
|
+
} catch {
|
|
16542
|
+
boundary = {
|
|
16543
|
+
ok: false,
|
|
16544
|
+
code: "boundary-failed",
|
|
16545
|
+
loadSent: false
|
|
16546
|
+
};
|
|
16547
|
+
} finally {
|
|
16548
|
+
signal.removeEventListener("abort", abortProof);
|
|
16549
|
+
stopping.removeEventListener("abort", abortProof);
|
|
16550
|
+
}
|
|
16551
|
+
if (!boundary.ok) {
|
|
16552
|
+
await rm(directory, {
|
|
16553
|
+
recursive: true,
|
|
16554
|
+
force: true
|
|
16555
|
+
}).catch(() => void 0);
|
|
16556
|
+
if (proof.signal.aborted) return refusedBefore(signal.aborted ? "laya-purged" : "laya-stopped");
|
|
16557
|
+
if (boundary.code === "boundary-failed") this.#artifacts.recordWorkerFailure(selection, "boundary-failed");
|
|
16558
|
+
return refusedBefore(boundary.code);
|
|
16559
|
+
}
|
|
16560
|
+
const proven = boundary;
|
|
16561
|
+
const worker = {
|
|
16562
|
+
child: proven.child,
|
|
16563
|
+
binding: proven.binding,
|
|
16564
|
+
selection,
|
|
16565
|
+
directory,
|
|
16566
|
+
close: () => proven.close(),
|
|
16567
|
+
pending: void 0,
|
|
16568
|
+
idle: void 0,
|
|
16569
|
+
closing: void 0
|
|
16570
|
+
};
|
|
16571
|
+
this.#read(worker);
|
|
16572
|
+
if (generation !== this.#generation || stopping.aborted || signal.aborted) {
|
|
16573
|
+
await this.#kill(worker, "laya-stopped");
|
|
16574
|
+
return refusedBefore(signal.aborted ? "laya-purged" : "laya-stopped");
|
|
16575
|
+
}
|
|
16576
|
+
if (!sameSelection(this.#artifacts.workerSelection(), selection)) {
|
|
16577
|
+
await this.#kill(worker, "laya-authority-changed");
|
|
16578
|
+
return refusedBefore("laya-authority-changed");
|
|
16579
|
+
}
|
|
16580
|
+
const loaded = this.#expect(worker, this.#loadDeadlineMs, signal, stopping, (frame) => loadFrame(frame, worker.binding));
|
|
16581
|
+
try {
|
|
16582
|
+
await proven.sendLoad({
|
|
16583
|
+
runtimeRoot: verified.runtimeRoot,
|
|
16584
|
+
modelDir: verified.modelRoot
|
|
16585
|
+
});
|
|
16586
|
+
} catch {
|
|
16587
|
+
loaded.cancel();
|
|
16588
|
+
await this.#kill(worker, "laya-crashed");
|
|
16589
|
+
return refusedBefore("laya-crashed");
|
|
16590
|
+
}
|
|
16591
|
+
const outcome = await loaded.settled;
|
|
16592
|
+
if ("gone" in outcome || outcome.value !== "loaded") {
|
|
16593
|
+
const gone = "gone" in outcome ? outcome.gone : "runtime-failed";
|
|
16594
|
+
await this.#kill(worker, gone);
|
|
16595
|
+
if (gone === "laya-purged" || gone === "laya-stopped") return refusedBefore(gone);
|
|
16596
|
+
this.#artifacts.recordWorkerFailure(selection, "runtime-failed");
|
|
16597
|
+
return refusedBefore("runtime-failed");
|
|
16598
|
+
}
|
|
16599
|
+
if (generation !== this.#generation || stopping.aborted) {
|
|
16600
|
+
await this.#kill(worker, "laya-stopped");
|
|
16601
|
+
return refusedBefore("laya-stopped");
|
|
16602
|
+
}
|
|
16603
|
+
this.#worker = worker;
|
|
16604
|
+
return { worker };
|
|
16605
|
+
}
|
|
16606
|
+
/** Attaches the child's one stdout reader. A proven child's stdout is paused, and stays paused until resumed. */
|
|
16607
|
+
#read(worker) {
|
|
16608
|
+
const parser = new WorkerFrameParser();
|
|
16609
|
+
worker.child.stdout.on("data", (chunk) => {
|
|
16610
|
+
let frames;
|
|
16611
|
+
try {
|
|
16612
|
+
frames = parser.push(chunk);
|
|
16613
|
+
} catch {
|
|
16614
|
+
this.#kill(worker, "laya-malformed");
|
|
16615
|
+
return;
|
|
16616
|
+
}
|
|
16617
|
+
for (const frame of frames) {
|
|
16618
|
+
const pending = worker.pending;
|
|
16619
|
+
if (pending === void 0) {
|
|
16620
|
+
this.#kill(worker, "laya-malformed");
|
|
16621
|
+
return;
|
|
16622
|
+
}
|
|
16623
|
+
pending.frame(frame);
|
|
16624
|
+
}
|
|
16625
|
+
});
|
|
16626
|
+
worker.child.once("exit", () => void this.#kill(worker, "laya-crashed"));
|
|
16627
|
+
worker.child.stdout.resume();
|
|
16628
|
+
if (worker.child.exitCode !== null || worker.child.signalCode !== null) this.#kill(worker, "laya-crashed");
|
|
16629
|
+
}
|
|
16630
|
+
/**
|
|
16631
|
+
* Waits for exactly one expected frame within a deadline. Any other frame, the child's end, the deadline, a purge of
|
|
16632
|
+
* the waiting request or a stop settles it as gone; `cancel` withdraws it when nothing was written.
|
|
16633
|
+
*/
|
|
16634
|
+
#expect(worker, deadlineMs, signal, stopping, accept) {
|
|
16635
|
+
let resolve = () => void 0;
|
|
16636
|
+
const settled = new Promise((done) => {
|
|
16637
|
+
resolve = done;
|
|
16638
|
+
});
|
|
16639
|
+
let done = false;
|
|
16640
|
+
const onPurge = () => finish({ gone: "laya-purged" });
|
|
16641
|
+
const onStop = () => finish({ gone: "laya-stopped" });
|
|
16642
|
+
const timer = setTimeout(() => finish({ gone: "laya-timeout" }), deadlineMs);
|
|
16643
|
+
const pending = {
|
|
16644
|
+
frame: (frame) => {
|
|
16645
|
+
const value = accept(frame);
|
|
16646
|
+
finish(value === null ? { gone: "laya-malformed" } : { value });
|
|
16647
|
+
},
|
|
16648
|
+
gone: (code) => finish({ gone: code })
|
|
16649
|
+
};
|
|
16650
|
+
function withdraw() {
|
|
16651
|
+
if (done) return false;
|
|
16652
|
+
done = true;
|
|
16653
|
+
clearTimeout(timer);
|
|
16654
|
+
signal.removeEventListener("abort", onPurge);
|
|
16655
|
+
stopping.removeEventListener("abort", onStop);
|
|
16656
|
+
if (worker.pending === pending) worker.pending = void 0;
|
|
16657
|
+
return true;
|
|
16658
|
+
}
|
|
16659
|
+
function finish(result) {
|
|
16660
|
+
if (withdraw()) resolve(result);
|
|
16661
|
+
}
|
|
16662
|
+
worker.pending = pending;
|
|
16663
|
+
signal.addEventListener("abort", onPurge, { once: true });
|
|
16664
|
+
stopping.addEventListener("abort", onStop, { once: true });
|
|
16665
|
+
if (signal.aborted) onPurge();
|
|
16666
|
+
if (stopping.aborted) onStop();
|
|
16667
|
+
if (worker.closing !== void 0) finish({ gone: "laya-crashed" });
|
|
16668
|
+
return {
|
|
16669
|
+
settled,
|
|
16670
|
+
cancel: () => void withdraw()
|
|
16671
|
+
};
|
|
16672
|
+
}
|
|
16673
|
+
#clearIdle(worker) {
|
|
16674
|
+
if (worker.idle !== void 0) clearTimeout(worker.idle);
|
|
16675
|
+
worker.idle = void 0;
|
|
16676
|
+
}
|
|
16677
|
+
#armIdle(worker) {
|
|
16678
|
+
this.#clearIdle(worker);
|
|
16679
|
+
worker.idle = setTimeout(() => {
|
|
16680
|
+
if (worker.pending === void 0) this.#kill(worker, "laya-stopped");
|
|
16681
|
+
}, this.#idleExitMs);
|
|
16682
|
+
}
|
|
16683
|
+
/**
|
|
16684
|
+
* SIGKILL, wait, and remove the worker's owner-only directory. Whatever was waiting learns why first, and the child's
|
|
16685
|
+
* reader is detached, so no frame is read from a killed worker.
|
|
16686
|
+
*/
|
|
16687
|
+
#kill(worker, reason) {
|
|
16688
|
+
worker.closing ??= (async () => {
|
|
16689
|
+
if (this.#worker === worker) this.#worker = void 0;
|
|
16690
|
+
this.#clearIdle(worker);
|
|
16691
|
+
worker.child.stdout.removeAllListeners("data");
|
|
16692
|
+
const pending = worker.pending;
|
|
16693
|
+
worker.pending = void 0;
|
|
16694
|
+
pending?.gone(reason);
|
|
16695
|
+
await worker.close().catch(() => void 0);
|
|
16696
|
+
await rm(worker.directory, {
|
|
16697
|
+
recursive: true,
|
|
16698
|
+
force: true
|
|
16699
|
+
}).catch(() => void 0);
|
|
16700
|
+
})();
|
|
16701
|
+
return worker.closing;
|
|
16702
|
+
}
|
|
16703
|
+
};
|
|
16704
|
+
/** One owner composes one Laya executor per database, as it does E's purge listener; a test composes its own. */
|
|
16705
|
+
const executors = /* @__PURE__ */ new WeakMap();
|
|
16706
|
+
function registerLayaJudgeExecutor(database, executor) {
|
|
16707
|
+
if (executors.has(database)) throw new Error("a Laya judge executor is already registered");
|
|
16708
|
+
executors.set(database, executor);
|
|
16709
|
+
return () => {
|
|
16710
|
+
if (executors.get(database) === executor) executors.delete(database);
|
|
16711
|
+
};
|
|
16712
|
+
}
|
|
16713
|
+
/** The executor E's coordinator and judge test reach a Laya judge through; none means Laya cannot run here. */
|
|
16714
|
+
function layaJudgeExecutorFor(database) {
|
|
16715
|
+
return executors.get(database);
|
|
16716
|
+
}
|
|
15985
16717
|
//#endregion
|
|
15986
16718
|
//#region src/runtime/judge-test.ts
|
|
15987
16719
|
const TEST_ACCOUNT_ID = "synthetic-judge-test";
|
|
@@ -16020,6 +16752,29 @@ function activeRuleAuthority(database, judgeId, version) {
|
|
|
16020
16752
|
}
|
|
16021
16753
|
throw new JudgeBudgetError("JUDGE_AUTHORITY_CHANGED", "no active rule grants this exact judge test");
|
|
16022
16754
|
}
|
|
16755
|
+
/**
|
|
16756
|
+
* A live Laya judge version where this release or platform does not offer Laya: Laya's own closed reason (the
|
|
16757
|
+
* placeholder lock's `E2_EXTERNAL_REFERENCE_BLOCKED`, or the platform's or the sandbox's), read from the database's
|
|
16758
|
+
* own Laya inputs as a rule naming one is (E2 Task 10a). Undefined for any other judge, and wherever Laya is offered.
|
|
16759
|
+
*/
|
|
16760
|
+
function unofferedLayaJudge(database, input) {
|
|
16761
|
+
const row = database.prepare("SELECT document FROM judge_versions WHERE judge_id = ? AND version = ? AND revoked_at IS NULL").get(input.judgeId, input.version);
|
|
16762
|
+
if (row === void 0) return void 0;
|
|
16763
|
+
let kind;
|
|
16764
|
+
try {
|
|
16765
|
+
kind = JSON.parse(row.document).kind;
|
|
16766
|
+
} catch {
|
|
16767
|
+
return;
|
|
16768
|
+
}
|
|
16769
|
+
if (kind !== "laya") return void 0;
|
|
16770
|
+
try {
|
|
16771
|
+
assertLayaJudgeKindAvailable(layaEnablementInputsFor(database));
|
|
16772
|
+
return;
|
|
16773
|
+
} catch (error) {
|
|
16774
|
+
if (error instanceof EventDomainError) return error.code;
|
|
16775
|
+
throw error;
|
|
16776
|
+
}
|
|
16777
|
+
}
|
|
16023
16778
|
function switchGeneration(database) {
|
|
16024
16779
|
return database.prepare("SELECT switch_generation FROM event_settings WHERE singleton = 1").get()?.switch_generation ?? 0;
|
|
16025
16780
|
}
|
|
@@ -16052,6 +16807,13 @@ var JudgeTestControl = class {
|
|
|
16052
16807
|
}
|
|
16053
16808
|
async test(input) {
|
|
16054
16809
|
if (typeof input.judgeId !== "string" || input.judgeId.length === 0 || !Number.isSafeInteger(input.version) || input.version < 1) throw new CommsError("USAGE", "judge test needs one exact judge id and positive version");
|
|
16810
|
+
const unoffered = unofferedLayaJudge(this.#store.database, input);
|
|
16811
|
+
if (unoffered !== void 0) return {
|
|
16812
|
+
kind: "refused",
|
|
16813
|
+
judgeId: input.judgeId,
|
|
16814
|
+
version: input.version,
|
|
16815
|
+
code: unoffered
|
|
16816
|
+
};
|
|
16055
16817
|
let authority;
|
|
16056
16818
|
try {
|
|
16057
16819
|
authority = activeRuleAuthority(this.#store.database, input.judgeId, input.version);
|
|
@@ -16078,6 +16840,7 @@ var JudgeTestControl = class {
|
|
|
16078
16840
|
handles: []
|
|
16079
16841
|
}
|
|
16080
16842
|
});
|
|
16843
|
+
if (authority.judge.kind === "laya") return this.#testLaya(input, authority, authority.judge);
|
|
16081
16844
|
let hostedApiKey;
|
|
16082
16845
|
if (authority.judge.kind === "typesafe") {
|
|
16083
16846
|
const key = await this.#hostedApiKeyFor({
|
|
@@ -16111,43 +16874,9 @@ var JudgeTestControl = class {
|
|
|
16111
16874
|
code: prepared.code
|
|
16112
16875
|
};
|
|
16113
16876
|
}
|
|
16114
|
-
const reservationId = this.#newId();
|
|
16115
16877
|
let reservation;
|
|
16116
16878
|
try {
|
|
16117
|
-
reservation =
|
|
16118
|
-
store: this.#store,
|
|
16119
|
-
id: reservationId,
|
|
16120
|
-
workId: `judge-test:${reservationId}`,
|
|
16121
|
-
testWorkId: `judge-test:${reservationId}`,
|
|
16122
|
-
judge: authority.judge,
|
|
16123
|
-
finalEnvelope: prepared.request,
|
|
16124
|
-
switchGeneration: switchGeneration(this.#store.database),
|
|
16125
|
-
now: this.#now(),
|
|
16126
|
-
authority: {
|
|
16127
|
-
ruleId: authority.ruleId,
|
|
16128
|
-
ruleVersion: authority.ruleVersion,
|
|
16129
|
-
..."activationId" in authority ? {
|
|
16130
|
-
activationId: authority.activationId,
|
|
16131
|
-
approval: authority.challenge
|
|
16132
|
-
} : {}
|
|
16133
|
-
},
|
|
16134
|
-
assertAuthority: (database) => {
|
|
16135
|
-
if ("activationId" in authority) {
|
|
16136
|
-
const current = assertPendingRuleJudgeTestAuthority({
|
|
16137
|
-
database,
|
|
16138
|
-
activationId: authority.activationId,
|
|
16139
|
-
ruleId: authority.ruleId,
|
|
16140
|
-
ruleVersion: authority.ruleVersion,
|
|
16141
|
-
judgeId: input.judgeId,
|
|
16142
|
-
judgeVersion: input.version
|
|
16143
|
-
});
|
|
16144
|
-
if (current.question !== authority.question || current.maxInputTokens !== authority.maxInputTokens || current.maxOutputTokens !== authority.maxOutputTokens) throw new JudgeBudgetError("JUDGE_AUTHORITY_CHANGED", "the pending rule authority changed before judge test admission");
|
|
16145
|
-
return;
|
|
16146
|
-
}
|
|
16147
|
-
const current = activeRuleAuthority(database, input.judgeId, input.version);
|
|
16148
|
-
if (current.ruleId !== authority.ruleId || current.ruleVersion !== authority.ruleVersion) throw new JudgeBudgetError("JUDGE_AUTHORITY_CHANGED", "the active rule authority changed before judge test admission");
|
|
16149
|
-
}
|
|
16150
|
-
});
|
|
16879
|
+
reservation = this.#reserve(input, authority, prepared.request);
|
|
16151
16880
|
} catch (error) {
|
|
16152
16881
|
return refusal(input.judgeId, input.version, error);
|
|
16153
16882
|
}
|
|
@@ -16198,13 +16927,2183 @@ var JudgeTestControl = class {
|
|
|
16198
16927
|
reservationId: reservation.id
|
|
16199
16928
|
};
|
|
16200
16929
|
}
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
|
|
16206
|
-
|
|
16207
|
-
|
|
16930
|
+
/** The one synthetic-test admission: the active singleton's global ceilings and this judge version's test charge. */
|
|
16931
|
+
#reserve(input, authority, finalEnvelope) {
|
|
16932
|
+
const reservationId = this.#newId();
|
|
16933
|
+
return reserveJudgeTest({
|
|
16934
|
+
store: this.#store,
|
|
16935
|
+
id: reservationId,
|
|
16936
|
+
workId: `judge-test:${reservationId}`,
|
|
16937
|
+
testWorkId: `judge-test:${reservationId}`,
|
|
16938
|
+
judge: authority.judge,
|
|
16939
|
+
finalEnvelope,
|
|
16940
|
+
switchGeneration: switchGeneration(this.#store.database),
|
|
16941
|
+
now: this.#now(),
|
|
16942
|
+
authority: {
|
|
16943
|
+
ruleId: authority.ruleId,
|
|
16944
|
+
ruleVersion: authority.ruleVersion,
|
|
16945
|
+
..."activationId" in authority ? {
|
|
16946
|
+
activationId: authority.activationId,
|
|
16947
|
+
approval: authority.challenge
|
|
16948
|
+
} : {}
|
|
16949
|
+
},
|
|
16950
|
+
assertAuthority: (database) => {
|
|
16951
|
+
if ("activationId" in authority) {
|
|
16952
|
+
const current = assertPendingRuleJudgeTestAuthority({
|
|
16953
|
+
database,
|
|
16954
|
+
activationId: authority.activationId,
|
|
16955
|
+
ruleId: authority.ruleId,
|
|
16956
|
+
ruleVersion: authority.ruleVersion,
|
|
16957
|
+
judgeId: input.judgeId,
|
|
16958
|
+
judgeVersion: input.version
|
|
16959
|
+
});
|
|
16960
|
+
if (current.question !== authority.question || current.maxInputTokens !== authority.maxInputTokens || current.maxOutputTokens !== authority.maxOutputTokens) throw new JudgeBudgetError("JUDGE_AUTHORITY_CHANGED", "the pending rule authority changed before judge test admission");
|
|
16961
|
+
return;
|
|
16962
|
+
}
|
|
16963
|
+
const current = activeRuleAuthority(database, input.judgeId, input.version);
|
|
16964
|
+
if (current.ruleId !== authority.ruleId || current.ruleVersion !== authority.ruleVersion) throw new JudgeBudgetError("JUDGE_AUTHORITY_CHANGED", "the active rule authority changed before judge test admission");
|
|
16965
|
+
}
|
|
16966
|
+
});
|
|
16967
|
+
}
|
|
16968
|
+
async #testLaya(input, authority, judge) {
|
|
16969
|
+
const executor = layaJudgeExecutorFor(this.#store.database);
|
|
16970
|
+
if (executor === void 0) return {
|
|
16971
|
+
kind: "refused",
|
|
16972
|
+
judgeId: input.judgeId,
|
|
16973
|
+
version: input.version,
|
|
16974
|
+
code: "ADAPTER_REFERENCE_BLOCKED"
|
|
16975
|
+
};
|
|
16976
|
+
const syntheticState = canonicalJson(TEST_CLOUD_EVENT);
|
|
16977
|
+
const laya = executor.prepare({
|
|
16978
|
+
judge,
|
|
16979
|
+
state: syntheticState,
|
|
16980
|
+
instructions: authority.question
|
|
16981
|
+
});
|
|
16982
|
+
if (!laya.ok) return {
|
|
16983
|
+
kind: "refused",
|
|
16984
|
+
judgeId: input.judgeId,
|
|
16985
|
+
version: input.version,
|
|
16986
|
+
code: laya.code
|
|
16987
|
+
};
|
|
16988
|
+
let reservation;
|
|
16989
|
+
try {
|
|
16990
|
+
reservation = this.#reserve(input, authority, laya.request.body);
|
|
16991
|
+
} catch (error) {
|
|
16992
|
+
return refusal(input.judgeId, input.version, error);
|
|
16993
|
+
}
|
|
16994
|
+
const ioGate = new JudgeReservationIoGate({
|
|
16995
|
+
store: this.#store,
|
|
16996
|
+
reservationId: reservation.id,
|
|
16997
|
+
judgeKind: judge.kind,
|
|
16998
|
+
now: this.#now
|
|
16999
|
+
});
|
|
17000
|
+
const result = await executor.execute(laya.request, {
|
|
17001
|
+
reservationId: reservation.id,
|
|
17002
|
+
finalGate: () => ioGate.finalGate("write")
|
|
17003
|
+
});
|
|
17004
|
+
if (result.kind === "answer") {
|
|
17005
|
+
settleJudgeWork({
|
|
17006
|
+
store: this.#store,
|
|
17007
|
+
reservationId: reservation.id,
|
|
17008
|
+
result: "completed",
|
|
17009
|
+
actualInputTokens: result.inputTokens,
|
|
17010
|
+
now: this.#now()
|
|
17011
|
+
});
|
|
17012
|
+
return {
|
|
17013
|
+
kind: "completed",
|
|
17014
|
+
judgeId: input.judgeId,
|
|
17015
|
+
version: input.version,
|
|
17016
|
+
value: result.value,
|
|
17017
|
+
valueLabel: "probability",
|
|
17018
|
+
reservationId: reservation.id
|
|
17019
|
+
};
|
|
17020
|
+
}
|
|
17021
|
+
finalizeJudgeTransportRefusal({
|
|
17022
|
+
store: this.#store,
|
|
17023
|
+
reservationId: reservation.id,
|
|
17024
|
+
result: result.code === "laya-timeout" ? "timeout" : "transport-failure",
|
|
17025
|
+
now: this.#now()
|
|
17026
|
+
});
|
|
17027
|
+
if (ioGate.firstGateError !== void 0) return refusal(input.judgeId, input.version, ioGate.firstGateError);
|
|
17028
|
+
if (result.preIo) return {
|
|
17029
|
+
kind: "refused",
|
|
17030
|
+
judgeId: input.judgeId,
|
|
17031
|
+
version: input.version,
|
|
17032
|
+
code: result.code
|
|
17033
|
+
};
|
|
17034
|
+
return {
|
|
17035
|
+
kind: "deferred",
|
|
17036
|
+
judgeId: input.judgeId,
|
|
17037
|
+
version: input.version,
|
|
17038
|
+
code: result.code,
|
|
17039
|
+
reservationId: reservation.id
|
|
17040
|
+
};
|
|
17041
|
+
}
|
|
17042
|
+
};
|
|
17043
|
+
//#endregion
|
|
17044
|
+
//#region src/runtime/laya/artifact-paths.ts
|
|
17045
|
+
var LayaArtifactPathError = class extends Error {
|
|
17046
|
+
code = "ARTIFACT_PATH";
|
|
17047
|
+
constructor() {
|
|
17048
|
+
super("Laya artifact path is outside the reviewed artifact tree");
|
|
17049
|
+
this.name = "LayaArtifactPathError";
|
|
17050
|
+
}
|
|
17051
|
+
};
|
|
17052
|
+
function safeSegment(value) {
|
|
17053
|
+
return /^[a-z0-9][a-z0-9._-]*$/i.test(value) && value !== "." && value !== "..";
|
|
17054
|
+
}
|
|
17055
|
+
function safeRelativePath(value) {
|
|
17056
|
+
return value.length > 0 && !value.includes("\\") && !value.includes("\0") && !/[\uD800-\uDFFF]/u.test(value) && value.normalize("NFC") === value && value.split("/").every(safeSegment);
|
|
17057
|
+
}
|
|
17058
|
+
function below(root, target) {
|
|
17059
|
+
const resolvedRoot = resolve(root);
|
|
17060
|
+
const resolvedTarget = resolve(target);
|
|
17061
|
+
if (!resolvedTarget.startsWith(`${resolvedRoot}${sep}`)) throw new LayaArtifactPathError();
|
|
17062
|
+
return resolvedTarget;
|
|
17063
|
+
}
|
|
17064
|
+
/** Derives owner-controlled locations only from the reviewed manifest identity and file list. */
|
|
17065
|
+
function layaModelArtifactPaths(dataDirectory, manifestId) {
|
|
17066
|
+
if (!safeSegment(manifestId)) throw new LayaArtifactPathError();
|
|
17067
|
+
const models = below(dataDirectory, join(dataDirectory, "models"));
|
|
17068
|
+
const partial = below(models, join(models, `${manifestId}.partial`));
|
|
17069
|
+
const complete = below(models, join(models, manifestId));
|
|
17070
|
+
return {
|
|
17071
|
+
models,
|
|
17072
|
+
partial,
|
|
17073
|
+
complete,
|
|
17074
|
+
file(file, state) {
|
|
17075
|
+
if (!safeRelativePath(file.path)) throw new LayaArtifactPathError();
|
|
17076
|
+
return below(state === "partial" ? partial : complete, join(state === "partial" ? partial : complete, file.path));
|
|
17077
|
+
}
|
|
17078
|
+
};
|
|
17079
|
+
}
|
|
17080
|
+
//#endregion
|
|
17081
|
+
//#region src/runtime/laya/fetcher.ts
|
|
17082
|
+
const MAX_REDIRECTS = 5;
|
|
17083
|
+
const MAX_RESPONSE_HEADER_BYTES = 16384;
|
|
17084
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 3e4;
|
|
17085
|
+
const DEFAULT_TOTAL_TIMEOUT_MS = 18e5;
|
|
17086
|
+
const USER_AGENT = "agentcomms-events/0.15.2";
|
|
17087
|
+
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([
|
|
17088
|
+
301,
|
|
17089
|
+
302,
|
|
17090
|
+
303,
|
|
17091
|
+
307,
|
|
17092
|
+
308
|
|
17093
|
+
]);
|
|
17094
|
+
var LayaFetchError = class extends Error {
|
|
17095
|
+
code;
|
|
17096
|
+
constructor(code) {
|
|
17097
|
+
super(`Laya artifact download refused: ${code}`);
|
|
17098
|
+
this.name = "LayaFetchError";
|
|
17099
|
+
this.code = code;
|
|
17100
|
+
}
|
|
17101
|
+
};
|
|
17102
|
+
function exactLowercaseHex(value, length) {
|
|
17103
|
+
return new RegExp(`^[a-f0-9]{${length}}$`).test(value);
|
|
17104
|
+
}
|
|
17105
|
+
function responseHeaderLimit(value) {
|
|
17106
|
+
const limit = value ?? MAX_RESPONSE_HEADER_BYTES;
|
|
17107
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17108
|
+
return limit;
|
|
17109
|
+
}
|
|
17110
|
+
function fetchDeadline(value, fallback) {
|
|
17111
|
+
const deadline = value ?? fallback;
|
|
17112
|
+
if (!Number.isSafeInteger(deadline) || deadline < 1) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17113
|
+
return deadline;
|
|
17114
|
+
}
|
|
17115
|
+
function parseResponse(bytes, headerLimit) {
|
|
17116
|
+
const boundary = bytes.indexOf("\r\n\r\n", 0, "latin1");
|
|
17117
|
+
if (boundary < 0 || boundary + 4 > headerLimit) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17118
|
+
const lines = bytes.subarray(0, boundary).toString("latin1").split("\r\n");
|
|
17119
|
+
const status = /^HTTP\/1\.[01] ([0-9]{3}) [^\r\n]*$/u.exec(lines[0] ?? "");
|
|
17120
|
+
if (status?.[1] === void 0) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17121
|
+
const headers = /* @__PURE__ */ new Map();
|
|
17122
|
+
for (const line of lines.slice(1)) {
|
|
17123
|
+
const separator = line.indexOf(":");
|
|
17124
|
+
if (separator < 1) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17125
|
+
const name = line.slice(0, separator).toLowerCase();
|
|
17126
|
+
const value = line.slice(separator + 1).trim();
|
|
17127
|
+
if (headers.has(name) || /[\r\n]/u.test(value)) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17128
|
+
headers.set(name, value);
|
|
17129
|
+
}
|
|
17130
|
+
return {
|
|
17131
|
+
status: Number(status[1]),
|
|
17132
|
+
headers,
|
|
17133
|
+
body: bytes.subarray(boundary + 4)
|
|
17134
|
+
};
|
|
17135
|
+
}
|
|
17136
|
+
function contentLength(response) {
|
|
17137
|
+
const value = response.headers.get("content-length");
|
|
17138
|
+
if (value === void 0) return null;
|
|
17139
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(value)) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17140
|
+
const length = Number(value);
|
|
17141
|
+
if (!Number.isSafeInteger(length)) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17142
|
+
return length;
|
|
17143
|
+
}
|
|
17144
|
+
function exactSriSha512(value) {
|
|
17145
|
+
const matched = /^sha512-([A-Za-z0-9+/]{86}==)$/.exec(value);
|
|
17146
|
+
if (matched?.[1] === void 0) throw new LayaFetchError("RUNTIME_INTEGRITY");
|
|
17147
|
+
const decoded = Buffer.from(matched[1], "base64");
|
|
17148
|
+
if (decoded.length !== 64 || decoded.toString("base64") !== matched[1]) throw new LayaFetchError("RUNTIME_INTEGRITY");
|
|
17149
|
+
return decoded;
|
|
17150
|
+
}
|
|
17151
|
+
function isExactRegistryTarballUrl(source) {
|
|
17152
|
+
if (!/^(?:@[-a-z0-9]+\/)?[-a-z0-9]+$/iu.test(source.name)) return false;
|
|
17153
|
+
if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/u.test(source.version)) return false;
|
|
17154
|
+
const basename = source.name.slice(source.name.lastIndexOf("/") + 1);
|
|
17155
|
+
return source.url === `https://registry.npmjs.org/${source.name}/-/${basename}-${source.version}.tgz`;
|
|
17156
|
+
}
|
|
17157
|
+
function throwIfAborted(signal) {
|
|
17158
|
+
if (!signal.aborted) return;
|
|
17159
|
+
if (signal.reason instanceof LayaFetchError) throw signal.reason;
|
|
17160
|
+
throw new LayaFetchError("ABORTED");
|
|
17161
|
+
}
|
|
17162
|
+
/** One request-wide lifetime: total time starts before the first DNS query and never resets on redirects. */
|
|
17163
|
+
var FetchDeadline = class {
|
|
17164
|
+
#controller = new AbortController();
|
|
17165
|
+
#expiresAt;
|
|
17166
|
+
#onCallerAbort;
|
|
17167
|
+
#timer;
|
|
17168
|
+
signal = this.#controller.signal;
|
|
17169
|
+
constructor(callerSignal, totalTimeoutMs) {
|
|
17170
|
+
this.#expiresAt = Date.now() + totalTimeoutMs;
|
|
17171
|
+
this.#onCallerAbort = () => this.#abort(new LayaFetchError("ABORTED"));
|
|
17172
|
+
if (callerSignal.aborted) this.#onCallerAbort();
|
|
17173
|
+
else callerSignal.addEventListener("abort", this.#onCallerAbort, { once: true });
|
|
17174
|
+
this.#timer = setTimeout(() => this.#abort(new LayaFetchError("TOTAL_TIMEOUT")), totalTimeoutMs);
|
|
17175
|
+
this.#callerSignal = callerSignal;
|
|
17176
|
+
}
|
|
17177
|
+
#callerSignal;
|
|
17178
|
+
remainingMs() {
|
|
17179
|
+
throwIfAborted(this.signal);
|
|
17180
|
+
return Math.max(1, this.#expiresAt - Date.now());
|
|
17181
|
+
}
|
|
17182
|
+
dispose() {
|
|
17183
|
+
clearTimeout(this.#timer);
|
|
17184
|
+
this.#callerSignal.removeEventListener("abort", this.#onCallerAbort);
|
|
17185
|
+
}
|
|
17186
|
+
#abort(reason) {
|
|
17187
|
+
if (!this.signal.aborted) this.#controller.abort(reason);
|
|
17188
|
+
}
|
|
17189
|
+
};
|
|
17190
|
+
function mapPinnedResponseError(error) {
|
|
17191
|
+
if (error instanceof PinnedResponseError) {
|
|
17192
|
+
if (error.code === "NETWORK_ABORTED") throw new LayaFetchError("ABORTED");
|
|
17193
|
+
if (error.code === "NETWORK_IDLE_TIMEOUT") throw new LayaFetchError("IDLE_TIMEOUT");
|
|
17194
|
+
if (error.code === "NETWORK_TOTAL_TIMEOUT") throw new LayaFetchError("TOTAL_TIMEOUT");
|
|
17195
|
+
}
|
|
17196
|
+
throw error;
|
|
17197
|
+
}
|
|
17198
|
+
function safeManifestPath(value) {
|
|
17199
|
+
return value.length > 0 && !value.includes("\\") && !value.includes("\0") && !/[\uD800-\uDFFF]/u.test(value) && value.normalize("NFC") === value && value.split("/").every((part) => /^[a-z0-9][a-z0-9._-]*$/iu.test(part) && part !== "." && part !== "..");
|
|
17200
|
+
}
|
|
17201
|
+
function modelUrl(manifest, file) {
|
|
17202
|
+
if (manifest.source.host !== "huggingface.co") throw new LayaFetchError("HTTP_RESPONSE");
|
|
17203
|
+
if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/iu.test(manifest.source.repo)) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17204
|
+
if (!exactLowercaseHex(manifest.source.revision, 40) || !safeManifestPath(file.path)) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17205
|
+
return new URL(`https://${manifest.source.host}/${manifest.source.repo}/resolve/${manifest.source.revision}/${file.path}`);
|
|
17206
|
+
}
|
|
17207
|
+
/** Rejects every optional request header before any pinned DNS/TCP/TLS activity begins. */
|
|
17208
|
+
function assertLayaRequestHeaders(headers) {
|
|
17209
|
+
const allowed = /* @__PURE__ */ new Set(["range", "user-agent"]);
|
|
17210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17211
|
+
for (const [rawName, value] of Object.entries(headers)) {
|
|
17212
|
+
const name = rawName.toLowerCase();
|
|
17213
|
+
if (!allowed.has(name)) throw new LayaFetchError("REQUEST_HEADER");
|
|
17214
|
+
if (seen.has(name)) throw new LayaFetchError("REQUEST_HEADER");
|
|
17215
|
+
seen.add(name);
|
|
17216
|
+
if (name === "user-agent" && value !== USER_AGENT) throw new LayaFetchError("REQUEST_HEADER");
|
|
17217
|
+
if (name === "range" && !/^bytes=[1-9]\d*-$/.test(value)) throw new LayaFetchError("REQUEST_HEADER");
|
|
17218
|
+
}
|
|
17219
|
+
}
|
|
17220
|
+
function expectedContentRange(value, start, size, received) {
|
|
17221
|
+
if (value === void 0) throw new LayaFetchError("CONTENT_RANGE");
|
|
17222
|
+
const matched = /^bytes (\d+)-(\d+)\/(\d+)$/u.exec(value);
|
|
17223
|
+
if (matched === null) throw new LayaFetchError("CONTENT_RANGE");
|
|
17224
|
+
const [, returnedStart, returnedEnd, total] = matched;
|
|
17225
|
+
if (Number(returnedStart) !== start || Number(total) !== size || Number(returnedEnd) !== start + received - 1 || received < 1 || start + received > size) throw new LayaFetchError("CONTENT_RANGE");
|
|
17226
|
+
}
|
|
17227
|
+
/** Fixed-purpose raw artifact reader: it owns no proxy, agent, HTTP client, or loopback exception. */
|
|
17228
|
+
var LayaArtifactFetcher = class {
|
|
17229
|
+
manifest;
|
|
17230
|
+
#runtimePackages;
|
|
17231
|
+
#resolver;
|
|
17232
|
+
#tcpConnect;
|
|
17233
|
+
#tlsConnect;
|
|
17234
|
+
#headerLimit;
|
|
17235
|
+
#idleTimeoutMs;
|
|
17236
|
+
#totalTimeoutMs;
|
|
17237
|
+
constructor(options) {
|
|
17238
|
+
this.manifest = options.manifest;
|
|
17239
|
+
this.#runtimePackages = options.runtimePackages;
|
|
17240
|
+
this.#resolver = options.resolver;
|
|
17241
|
+
this.#tcpConnect = options.tcpConnect ?? connectPinnedTcp;
|
|
17242
|
+
this.#tlsConnect = options.tlsConnect ?? connectPinnedTls;
|
|
17243
|
+
this.#headerLimit = responseHeaderLimit(options.maxResponseHeaderBytes);
|
|
17244
|
+
this.#idleTimeoutMs = fetchDeadline(options.idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS);
|
|
17245
|
+
this.#totalTimeoutMs = fetchDeadline(options.totalTimeoutMs, DEFAULT_TOTAL_TIMEOUT_MS);
|
|
17246
|
+
}
|
|
17247
|
+
async fetchRuntimeArchive(input, options) {
|
|
17248
|
+
const runtimePackage = this.#runtimePackages.find((candidate) => candidate.name === input.name && candidate.version === input.version);
|
|
17249
|
+
if (runtimePackage === void 0) throw new LayaFetchError("RUNTIME_ORIGIN");
|
|
17250
|
+
if (!isExactRegistryTarballUrl(runtimePackage)) throw new LayaFetchError("RUNTIME_ORIGIN");
|
|
17251
|
+
const expected = exactSriSha512(runtimePackage.integrity);
|
|
17252
|
+
const chunks = [];
|
|
17253
|
+
const result = await this.#get(new URL(runtimePackage.url), void 0, 536870912, async (chunk) => {
|
|
17254
|
+
chunks.push(Buffer.from(chunk));
|
|
17255
|
+
}, options.signal, false);
|
|
17256
|
+
const bytes = Buffer.concat(chunks);
|
|
17257
|
+
if (result.status !== 200 || contentLength(result) !== null && contentLength(result) !== bytes.length) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17258
|
+
const actual = createHash("sha512").update(bytes).digest();
|
|
17259
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new LayaFetchError("RUNTIME_INTEGRITY");
|
|
17260
|
+
return bytes;
|
|
17261
|
+
}
|
|
17262
|
+
async fetchModelFile(file, resumeFrom, options) {
|
|
17263
|
+
const chunks = [];
|
|
17264
|
+
const result = await this.fetchModelFileTo(file, resumeFrom, async (chunk) => {
|
|
17265
|
+
chunks.push(Buffer.from(chunk));
|
|
17266
|
+
}, options);
|
|
17267
|
+
return {
|
|
17268
|
+
bytes: Buffer.concat(chunks),
|
|
17269
|
+
url: result.url,
|
|
17270
|
+
status: result.status,
|
|
17271
|
+
headers: result.headers
|
|
17272
|
+
};
|
|
17273
|
+
}
|
|
17274
|
+
async fetchModelFileTo(file, resumeFrom, onChunk, options) {
|
|
17275
|
+
if (!Number.isSafeInteger(file.size) || file.size < 1 || !exactLowercaseHex(file.sha256, 64)) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17276
|
+
if (resumeFrom !== void 0 && (!Number.isSafeInteger(resumeFrom) || resumeFrom < 1 || resumeFrom >= file.size)) throw new LayaFetchError("CONTENT_RANGE");
|
|
17277
|
+
const result = await this.#get(modelUrl(this.manifest, file), resumeFrom, file.size, onChunk, options.signal, true);
|
|
17278
|
+
const declaredLength = contentLength(result);
|
|
17279
|
+
if (declaredLength !== null && declaredLength !== result.bytesReceived) throw new LayaFetchError("RESPONSE_BODY");
|
|
17280
|
+
if (resumeFrom === void 0) {
|
|
17281
|
+
if (result.status !== 200 || result.bytesReceived !== file.size) throw new LayaFetchError("HTTP_RESPONSE");
|
|
17282
|
+
} else {
|
|
17283
|
+
if (result.status !== 206) throw new LayaFetchError("CONTENT_RANGE");
|
|
17284
|
+
expectedContentRange(result.headers.get("content-range"), resumeFrom, file.size, result.bytesReceived);
|
|
17285
|
+
}
|
|
17286
|
+
return result;
|
|
17287
|
+
}
|
|
17288
|
+
async #globalApprovedAddresses(url, signal) {
|
|
17289
|
+
const hostname = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
|
|
17290
|
+
let answers;
|
|
17291
|
+
try {
|
|
17292
|
+
answers = await resolveHost(hostname, this.#resolver, { signal });
|
|
17293
|
+
const vetted = answers.map((answer) => assertAddressAllowed(answer, { addressMode: "global-only" }));
|
|
17294
|
+
return [...new Set(vetted.map((answer) => answer.address))].sort((left, right) => left.localeCompare(right));
|
|
17295
|
+
} catch (error) {
|
|
17296
|
+
if (error instanceof AddressPolicyError) throw new LayaFetchError("ADDRESS");
|
|
17297
|
+
throw error;
|
|
17298
|
+
}
|
|
17299
|
+
}
|
|
17300
|
+
async #get(initial, resumeFrom, maximumBytes, onBody, signal, allowRedirects) {
|
|
17301
|
+
let url = new URL(initial.href);
|
|
17302
|
+
let redirects = 0;
|
|
17303
|
+
const deadline = new FetchDeadline(signal, this.#totalTimeoutMs);
|
|
17304
|
+
try {
|
|
17305
|
+
while (true) {
|
|
17306
|
+
throwIfAborted(deadline.signal);
|
|
17307
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "") throw new LayaFetchError("REDIRECT");
|
|
17308
|
+
const approvedAddressSet = await this.#waitForPhase(() => this.#globalApprovedAddresses(url, deadline.signal), deadline);
|
|
17309
|
+
let connection;
|
|
17310
|
+
try {
|
|
17311
|
+
connection = await this.#waitForPhase(() => preparePinnedConnection({
|
|
17312
|
+
url,
|
|
17313
|
+
approvedAddressSet,
|
|
17314
|
+
approvedAddressSetDigest: addressSetDigest(approvedAddressSet),
|
|
17315
|
+
...this.#resolver === void 0 ? {} : { resolver: this.#resolver },
|
|
17316
|
+
signal: deadline.signal
|
|
17317
|
+
}), deadline);
|
|
17318
|
+
} catch (error) {
|
|
17319
|
+
if (error instanceof AddressPolicyError) throw new LayaFetchError("ADDRESS");
|
|
17320
|
+
throw error;
|
|
17321
|
+
}
|
|
17322
|
+
const tcp = await this.#waitForPhase(() => this.#tcpConnect({
|
|
17323
|
+
host: connection.address,
|
|
17324
|
+
port: connection.port,
|
|
17325
|
+
signal: deadline.signal
|
|
17326
|
+
}), deadline, (late) => late.destroy());
|
|
17327
|
+
let socket;
|
|
17328
|
+
try {
|
|
17329
|
+
socket = await this.#waitForPhase(() => this.#tlsConnect({
|
|
17330
|
+
socket: tcp,
|
|
17331
|
+
...connection.tls,
|
|
17332
|
+
signal: deadline.signal
|
|
17333
|
+
}), deadline, (late) => late.destroy());
|
|
17334
|
+
} catch (error) {
|
|
17335
|
+
tcp.destroy();
|
|
17336
|
+
throw error;
|
|
17337
|
+
}
|
|
17338
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
17339
|
+
if (resumeFrom !== void 0) headers.Range = `bytes=${resumeFrom}-`;
|
|
17340
|
+
assertLayaRequestHeaders(headers);
|
|
17341
|
+
let header = Buffer.alloc(0);
|
|
17342
|
+
let response;
|
|
17343
|
+
let bytesReceived = 0;
|
|
17344
|
+
const hash = createHash("sha256");
|
|
17345
|
+
try {
|
|
17346
|
+
await streamPinnedResponseTo(socket, getRequestBytesForPinnedConnection(connection, headers), {
|
|
17347
|
+
maxBytes: maximumBytes + MAX_RESPONSE_HEADER_BYTES,
|
|
17348
|
+
signal: deadline.signal,
|
|
17349
|
+
idleTimeoutMs: this.#idleTimeoutMs,
|
|
17350
|
+
totalTimeoutMs: deadline.remainingMs(),
|
|
17351
|
+
onChunk: async (chunk) => {
|
|
17352
|
+
let body = chunk;
|
|
17353
|
+
if (response === void 0) {
|
|
17354
|
+
header = Buffer.concat([header, chunk]);
|
|
17355
|
+
const boundary = header.indexOf("\r\n\r\n", 0, "latin1");
|
|
17356
|
+
if (boundary < 0) {
|
|
17357
|
+
if (header.length > this.#headerLimit) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17358
|
+
return;
|
|
17359
|
+
}
|
|
17360
|
+
response = parseResponse(header.subarray(0, boundary + 4), this.#headerLimit);
|
|
17361
|
+
body = header.subarray(boundary + 4);
|
|
17362
|
+
header = Buffer.alloc(0);
|
|
17363
|
+
}
|
|
17364
|
+
if (response === void 0 || REDIRECT_STATUSES.has(response.status) || body.length === 0) return;
|
|
17365
|
+
bytesReceived += body.length;
|
|
17366
|
+
if (bytesReceived > maximumBytes) throw new LayaFetchError("RESPONSE_BODY");
|
|
17367
|
+
hash.update(body);
|
|
17368
|
+
await onBody(body, deadline.signal);
|
|
17369
|
+
}
|
|
17370
|
+
});
|
|
17371
|
+
} catch (error) {
|
|
17372
|
+
mapPinnedResponseError(error);
|
|
17373
|
+
}
|
|
17374
|
+
if (response === void 0) throw new LayaFetchError("RESPONSE_HEADERS");
|
|
17375
|
+
if (!REDIRECT_STATUSES.has(response.status)) return {
|
|
17376
|
+
...response,
|
|
17377
|
+
bytesReceived,
|
|
17378
|
+
sha256: hash.digest("hex"),
|
|
17379
|
+
url: url.href
|
|
17380
|
+
};
|
|
17381
|
+
if (!allowRedirects) throw new LayaFetchError("REDIRECT");
|
|
17382
|
+
if (redirects >= MAX_REDIRECTS) throw new LayaFetchError("REDIRECT_LIMIT");
|
|
17383
|
+
const location = response.headers.get("location");
|
|
17384
|
+
if (location === void 0 || location.length === 0) throw new LayaFetchError("REDIRECT");
|
|
17385
|
+
try {
|
|
17386
|
+
url = new URL(location, url);
|
|
17387
|
+
} catch {
|
|
17388
|
+
throw new LayaFetchError("REDIRECT");
|
|
17389
|
+
}
|
|
17390
|
+
redirects += 1;
|
|
17391
|
+
}
|
|
17392
|
+
} finally {
|
|
17393
|
+
deadline.dispose();
|
|
17394
|
+
}
|
|
17395
|
+
}
|
|
17396
|
+
#waitForPhase(operation, deadline, destroyLate) {
|
|
17397
|
+
return new Promise((resolve, reject) => {
|
|
17398
|
+
let settled = false;
|
|
17399
|
+
const cleanup = () => deadline.signal.removeEventListener("abort", onAbort);
|
|
17400
|
+
const fail = (error) => {
|
|
17401
|
+
if (settled) return;
|
|
17402
|
+
settled = true;
|
|
17403
|
+
cleanup();
|
|
17404
|
+
reject(error);
|
|
17405
|
+
};
|
|
17406
|
+
const onAbort = () => {
|
|
17407
|
+
try {
|
|
17408
|
+
throwIfAborted(deadline.signal);
|
|
17409
|
+
} catch (error) {
|
|
17410
|
+
fail(error instanceof Error ? error : new LayaFetchError("ABORTED"));
|
|
17411
|
+
}
|
|
17412
|
+
};
|
|
17413
|
+
deadline.signal.addEventListener("abort", onAbort, { once: true });
|
|
17414
|
+
if (deadline.signal.aborted) {
|
|
17415
|
+
onAbort();
|
|
17416
|
+
return;
|
|
17417
|
+
}
|
|
17418
|
+
Promise.resolve().then(() => {
|
|
17419
|
+
throwIfAborted(deadline.signal);
|
|
17420
|
+
return operation();
|
|
17421
|
+
}).then((value) => {
|
|
17422
|
+
if (settled || deadline.signal.aborted) {
|
|
17423
|
+
destroyLate?.(value);
|
|
17424
|
+
onAbort();
|
|
17425
|
+
return;
|
|
17426
|
+
}
|
|
17427
|
+
settled = true;
|
|
17428
|
+
cleanup();
|
|
17429
|
+
resolve(value);
|
|
17430
|
+
}, (error) => fail(error instanceof Error ? error : /* @__PURE__ */ new Error("pinned download phase failed")));
|
|
17431
|
+
});
|
|
17432
|
+
}
|
|
17433
|
+
};
|
|
17434
|
+
//#endregion
|
|
17435
|
+
//#region src/runtime/laya/model-downloader.ts
|
|
17436
|
+
const OWNER_ONLY_DIRECTORY_MODE$3 = 448;
|
|
17437
|
+
const OWNER_ONLY_FILE_MODE$1 = 384;
|
|
17438
|
+
const MAX_FILE_ATTEMPTS = 5;
|
|
17439
|
+
var LayaModelDownloadError = class extends Error {
|
|
17440
|
+
code;
|
|
17441
|
+
constructor(code) {
|
|
17442
|
+
super(`Laya model download refused: ${code}`);
|
|
17443
|
+
this.name = "LayaModelDownloadError";
|
|
17444
|
+
this.code = code;
|
|
17445
|
+
}
|
|
17446
|
+
};
|
|
17447
|
+
async function ownerOnlyDirectory$2(path) {
|
|
17448
|
+
await mkdir(path, {
|
|
17449
|
+
recursive: true,
|
|
17450
|
+
mode: OWNER_ONLY_DIRECTORY_MODE$3
|
|
17451
|
+
});
|
|
17452
|
+
const info = await lstat(path);
|
|
17453
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new LayaModelDownloadError("MODEL_PATH");
|
|
17454
|
+
await chmod(path, OWNER_ONLY_DIRECTORY_MODE$3);
|
|
17455
|
+
}
|
|
17456
|
+
async function missing(path) {
|
|
17457
|
+
try {
|
|
17458
|
+
await lstat(path);
|
|
17459
|
+
return false;
|
|
17460
|
+
} catch (error) {
|
|
17461
|
+
if (error.code === "ENOENT") return true;
|
|
17462
|
+
throw error;
|
|
17463
|
+
}
|
|
17464
|
+
}
|
|
17465
|
+
function abortIfNeeded(signal) {
|
|
17466
|
+
if (!signal.aborted) return;
|
|
17467
|
+
if (signal.reason instanceof LayaFetchError) throw downloadErrorForFetch(signal.reason);
|
|
17468
|
+
throw new LayaModelDownloadError("CANCELLED");
|
|
17469
|
+
}
|
|
17470
|
+
function awaitFenceWithSignal(wait, signal) {
|
|
17471
|
+
return new Promise((resolve, reject) => {
|
|
17472
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
17473
|
+
const onAbort = () => {
|
|
17474
|
+
cleanup();
|
|
17475
|
+
try {
|
|
17476
|
+
abortIfNeeded(signal);
|
|
17477
|
+
} catch (error) {
|
|
17478
|
+
reject(error);
|
|
17479
|
+
}
|
|
17480
|
+
};
|
|
17481
|
+
if (signal.aborted) {
|
|
17482
|
+
wait.catch(() => void 0);
|
|
17483
|
+
onAbort();
|
|
17484
|
+
return;
|
|
17485
|
+
}
|
|
17486
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
17487
|
+
wait.then(() => {
|
|
17488
|
+
cleanup();
|
|
17489
|
+
resolve();
|
|
17490
|
+
}, (error) => {
|
|
17491
|
+
cleanup();
|
|
17492
|
+
reject(error);
|
|
17493
|
+
});
|
|
17494
|
+
});
|
|
17495
|
+
}
|
|
17496
|
+
function downloadErrorForFetch(error) {
|
|
17497
|
+
if (error.code === "ABORTED") return new LayaModelDownloadError("CANCELLED");
|
|
17498
|
+
if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") return new LayaModelDownloadError("DEADLINE");
|
|
17499
|
+
return new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17500
|
+
}
|
|
17501
|
+
/**
|
|
17502
|
+
* A file's size and SHA-256, streamed one read at a time as the tree verifier does: the model's data file is 1.69 GB,
|
|
17503
|
+
* which a whole-file read would hold in memory at once (and Node refuses to read a file of 2 GiB or more whole).
|
|
17504
|
+
*/
|
|
17505
|
+
async function hashFile(path) {
|
|
17506
|
+
const hash = createHash("sha256");
|
|
17507
|
+
let size = 0;
|
|
17508
|
+
for await (const chunk of createReadStream(path)) {
|
|
17509
|
+
size += chunk.length;
|
|
17510
|
+
hash.update(chunk);
|
|
17511
|
+
}
|
|
17512
|
+
return {
|
|
17513
|
+
size,
|
|
17514
|
+
sha256: hash.digest("hex")
|
|
17515
|
+
};
|
|
17516
|
+
}
|
|
17517
|
+
async function partialSize(path) {
|
|
17518
|
+
try {
|
|
17519
|
+
const info = await lstat(path);
|
|
17520
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new LayaModelDownloadError("MODEL_PATH");
|
|
17521
|
+
return info.size;
|
|
17522
|
+
} catch (error) {
|
|
17523
|
+
if (error.code === "ENOENT") return 0;
|
|
17524
|
+
throw error;
|
|
17525
|
+
}
|
|
17526
|
+
}
|
|
17527
|
+
async function verifyPartialFile(path, file, size) {
|
|
17528
|
+
const actual = await hashFile(path);
|
|
17529
|
+
if (actual.size !== size || size > file.size) throw new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17530
|
+
if (size === file.size && actual.sha256 !== file.sha256) throw new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17531
|
+
}
|
|
17532
|
+
async function availableBytes$1(path) {
|
|
17533
|
+
const stats = await statfs(path);
|
|
17534
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
17535
|
+
}
|
|
17536
|
+
/** Downloads only the reviewed complete manifest tree, keeping every incomplete byte under one partial root. */
|
|
17537
|
+
var LayaModelDownloader = class {
|
|
17538
|
+
#dataDirectory;
|
|
17539
|
+
#manifest;
|
|
17540
|
+
#fetcher;
|
|
17541
|
+
#fence;
|
|
17542
|
+
constructor(options) {
|
|
17543
|
+
if (options.fence === void 0) throw new LayaModelDownloadError("LIFECYCLE_FENCE");
|
|
17544
|
+
this.#dataDirectory = options.dataDirectory;
|
|
17545
|
+
this.#manifest = options.manifest;
|
|
17546
|
+
this.#fetcher = options.fetcher;
|
|
17547
|
+
this.#fence = options.fence;
|
|
17548
|
+
}
|
|
17549
|
+
async download(options) {
|
|
17550
|
+
const paths = layaModelArtifactPaths(this.#dataDirectory, this.#manifest.manifestId);
|
|
17551
|
+
try {
|
|
17552
|
+
abortIfNeeded(options.signal);
|
|
17553
|
+
await this.#assertCurrent("start", options.signal);
|
|
17554
|
+
await ownerOnlyDirectory$2(paths.models);
|
|
17555
|
+
if (!await missing(paths.complete)) throw new LayaModelDownloadError("MODEL_EXISTS");
|
|
17556
|
+
await this.#assertCurrent("retry", options.signal);
|
|
17557
|
+
await ownerOnlyDirectory$2(paths.partial);
|
|
17558
|
+
const total = this.#manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
17559
|
+
const free = await (options.freeBytes ?? (() => availableBytes$1(paths.models)))();
|
|
17560
|
+
if (!Number.isSafeInteger(free) || free < Math.ceil(total * 1.1)) throw new LayaModelDownloadError("DISK_SPACE");
|
|
17561
|
+
let verified = 0;
|
|
17562
|
+
for (const file of this.#manifest.files) {
|
|
17563
|
+
abortIfNeeded(options.signal);
|
|
17564
|
+
await this.#assertCurrent("retry", options.signal);
|
|
17565
|
+
verified += await this.#downloadFile(paths.file(file, "partial"), file, options.signal);
|
|
17566
|
+
}
|
|
17567
|
+
abortIfNeeded(options.signal);
|
|
17568
|
+
if (!await missing(paths.complete)) throw new LayaModelDownloadError("MODEL_EXISTS");
|
|
17569
|
+
return this.#fence.withArtifactLock(async () => {
|
|
17570
|
+
abortIfNeeded(options.signal);
|
|
17571
|
+
if (!await missing(paths.complete)) throw new LayaModelDownloadError("MODEL_EXISTS");
|
|
17572
|
+
await this.#assertCurrent("promotion", options.signal);
|
|
17573
|
+
await rename(paths.partial, paths.complete);
|
|
17574
|
+
return {
|
|
17575
|
+
root: paths.complete,
|
|
17576
|
+
bytesVerified: verified
|
|
17577
|
+
};
|
|
17578
|
+
});
|
|
17579
|
+
} catch (error) {
|
|
17580
|
+
await rm(paths.partial, {
|
|
17581
|
+
recursive: true,
|
|
17582
|
+
force: true
|
|
17583
|
+
});
|
|
17584
|
+
if (error instanceof LayaModelDownloadError) throw error;
|
|
17585
|
+
if (error instanceof LayaFetchError) throw downloadErrorForFetch(error);
|
|
17586
|
+
throw error;
|
|
17587
|
+
}
|
|
17588
|
+
}
|
|
17589
|
+
async cleanupPartial() {
|
|
17590
|
+
const paths = layaModelArtifactPaths(this.#dataDirectory, this.#manifest.manifestId);
|
|
17591
|
+
await rm(paths.partial, {
|
|
17592
|
+
recursive: true,
|
|
17593
|
+
force: true
|
|
17594
|
+
});
|
|
17595
|
+
}
|
|
17596
|
+
async #assertCurrent(stage, signal) {
|
|
17597
|
+
await awaitFenceWithSignal(this.#fence.assertCurrent(stage, signal), signal);
|
|
17598
|
+
}
|
|
17599
|
+
async #downloadFile(path, file, signal) {
|
|
17600
|
+
await this.#assertCurrent("retry", signal);
|
|
17601
|
+
await ownerOnlyDirectory$2(dirname(path));
|
|
17602
|
+
for (let attempt = 0; attempt < MAX_FILE_ATTEMPTS; attempt += 1) {
|
|
17603
|
+
abortIfNeeded(signal);
|
|
17604
|
+
let current = await partialSize(path);
|
|
17605
|
+
if (current > 0) try {
|
|
17606
|
+
await verifyPartialFile(path, file, current);
|
|
17607
|
+
} catch {
|
|
17608
|
+
await rm(path, { force: true });
|
|
17609
|
+
current = 0;
|
|
17610
|
+
}
|
|
17611
|
+
try {
|
|
17612
|
+
let wrote = false;
|
|
17613
|
+
await this.#fetcher.fetchModelFileTo(file, current === 0 ? void 0 : current, async (chunk, fetchSignal) => {
|
|
17614
|
+
abortIfNeeded(fetchSignal);
|
|
17615
|
+
await this.#assertCurrent("retry", fetchSignal);
|
|
17616
|
+
abortIfNeeded(fetchSignal);
|
|
17617
|
+
if (current === 0 && !wrote) await writeFile(path, chunk, {
|
|
17618
|
+
mode: OWNER_ONLY_FILE_MODE$1,
|
|
17619
|
+
flag: "wx"
|
|
17620
|
+
});
|
|
17621
|
+
else await appendFile(path, chunk, { mode: OWNER_ONLY_FILE_MODE$1 });
|
|
17622
|
+
abortIfNeeded(fetchSignal);
|
|
17623
|
+
wrote = true;
|
|
17624
|
+
}, { signal });
|
|
17625
|
+
abortIfNeeded(signal);
|
|
17626
|
+
await this.#assertCurrent("retry", signal);
|
|
17627
|
+
await chmod(path, OWNER_ONLY_FILE_MODE$1);
|
|
17628
|
+
const final = await hashFile(path);
|
|
17629
|
+
await this.#assertCurrent("retry", signal);
|
|
17630
|
+
if (final.size !== file.size || final.sha256 !== file.sha256) throw new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17631
|
+
return final.size;
|
|
17632
|
+
} catch (error) {
|
|
17633
|
+
await rm(path, { force: true });
|
|
17634
|
+
if (error instanceof LayaFetchError) {
|
|
17635
|
+
const terminal = downloadErrorForFetch(error);
|
|
17636
|
+
if (terminal.code === "CANCELLED" || terminal.code === "DEADLINE") throw terminal;
|
|
17637
|
+
}
|
|
17638
|
+
if (error instanceof LayaModelDownloadError && (error.code === "CANCELLED" || error.code === "DEADLINE")) throw error;
|
|
17639
|
+
if (attempt + 1 === MAX_FILE_ATTEMPTS) {
|
|
17640
|
+
if (error instanceof LayaModelDownloadError) throw error;
|
|
17641
|
+
if (error instanceof LayaFetchError) throw downloadErrorForFetch(error);
|
|
17642
|
+
throw new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17643
|
+
}
|
|
17644
|
+
}
|
|
17645
|
+
}
|
|
17646
|
+
throw new LayaModelDownloadError("DOWNLOAD_FAILED");
|
|
17647
|
+
}
|
|
17648
|
+
};
|
|
17649
|
+
//#endregion
|
|
17650
|
+
//#region src/runtime/laya/tar.ts
|
|
17651
|
+
const BLOCK_SIZE = 512;
|
|
17652
|
+
const OWNER_ONLY_DIRECTORY_MODE$2 = 448;
|
|
17653
|
+
const OWNER_ONLY_FILE_MODE = 384;
|
|
17654
|
+
const EXECUTABLE_FILE_MODE = 448;
|
|
17655
|
+
var LayaTarError = class extends Error {
|
|
17656
|
+
code;
|
|
17657
|
+
constructor(code) {
|
|
17658
|
+
super(`Laya runtime archive refused: ${code}`);
|
|
17659
|
+
this.name = "LayaTarError";
|
|
17660
|
+
this.code = code;
|
|
17661
|
+
}
|
|
17662
|
+
};
|
|
17663
|
+
var ByteReader = class {
|
|
17664
|
+
#iterator;
|
|
17665
|
+
#buffer = Buffer.alloc(0);
|
|
17666
|
+
constructor(source) {
|
|
17667
|
+
this.#iterator = source[Symbol.asyncIterator]();
|
|
17668
|
+
}
|
|
17669
|
+
async readExactly(size) {
|
|
17670
|
+
while (this.#buffer.length < size) {
|
|
17671
|
+
const next = await this.#iterator.next();
|
|
17672
|
+
if (next.done) throw new LayaTarError("TAR_TRUNCATED");
|
|
17673
|
+
this.#buffer = Buffer.concat([this.#buffer, Buffer.from(next.value)]);
|
|
17674
|
+
}
|
|
17675
|
+
const value = this.#buffer.subarray(0, size);
|
|
17676
|
+
this.#buffer = this.#buffer.subarray(size);
|
|
17677
|
+
return value;
|
|
17678
|
+
}
|
|
17679
|
+
async ensureEndIsZero() {
|
|
17680
|
+
while (true) {
|
|
17681
|
+
const next = await this.#iterator.next();
|
|
17682
|
+
if (next.done) break;
|
|
17683
|
+
this.#buffer = Buffer.concat([this.#buffer, Buffer.from(next.value)]);
|
|
17684
|
+
}
|
|
17685
|
+
if (this.#buffer.some((byte) => byte !== 0)) throw new LayaTarError("TAR_HEADER");
|
|
17686
|
+
}
|
|
17687
|
+
};
|
|
17688
|
+
var TarBody = class {
|
|
17689
|
+
#reader;
|
|
17690
|
+
#remaining;
|
|
17691
|
+
constructor(reader, size) {
|
|
17692
|
+
this.#reader = reader;
|
|
17693
|
+
this.#remaining = size;
|
|
17694
|
+
}
|
|
17695
|
+
async *chunks() {
|
|
17696
|
+
while (this.#remaining > 0) {
|
|
17697
|
+
const count = Math.min(this.#remaining, 65536);
|
|
17698
|
+
const chunk = await this.#reader.readExactly(count);
|
|
17699
|
+
this.#remaining -= chunk.length;
|
|
17700
|
+
yield chunk;
|
|
17701
|
+
}
|
|
17702
|
+
}
|
|
17703
|
+
async drain() {
|
|
17704
|
+
for await (const _chunk of this.chunks());
|
|
17705
|
+
}
|
|
17706
|
+
};
|
|
17707
|
+
function isRegularFile(type) {
|
|
17708
|
+
return type === 0 || type === "0".charCodeAt(0);
|
|
17709
|
+
}
|
|
17710
|
+
function isSafeRelativePath(path) {
|
|
17711
|
+
return path.length > 0 && !path.startsWith("/") && !path.endsWith("/") && !path.includes("\\") && path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
17712
|
+
}
|
|
17713
|
+
function textField(bytes) {
|
|
17714
|
+
const firstNul = bytes.indexOf(0);
|
|
17715
|
+
const textBytes = firstNul === -1 ? bytes : bytes.subarray(0, firstNul);
|
|
17716
|
+
if (firstNul !== -1 && bytes.subarray(firstNul).some((byte) => byte !== 0)) throw new LayaTarError("TAR_PATH");
|
|
17717
|
+
try {
|
|
17718
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(textBytes);
|
|
17719
|
+
} catch {
|
|
17720
|
+
throw new LayaTarError("TAR_PATH");
|
|
17721
|
+
}
|
|
17722
|
+
}
|
|
17723
|
+
function strictOctalField(bytes) {
|
|
17724
|
+
if (bytes.length < 2) throw new LayaTarError("TAR_HEADER");
|
|
17725
|
+
if (bytes.every((byte) => byte === 0)) return 0;
|
|
17726
|
+
const terminator = bytes[bytes.length - 1];
|
|
17727
|
+
if (terminator !== 0 && terminator !== 32) throw new LayaTarError("TAR_HEADER");
|
|
17728
|
+
let value = 0;
|
|
17729
|
+
let sawDigit = false;
|
|
17730
|
+
for (let index = 0; index < bytes.length - 1; index += 1) {
|
|
17731
|
+
const byte = bytes[index] ?? 0;
|
|
17732
|
+
if (byte === 32) {
|
|
17733
|
+
if (sawDigit) throw new LayaTarError("TAR_HEADER");
|
|
17734
|
+
continue;
|
|
17735
|
+
}
|
|
17736
|
+
if (byte < "0".charCodeAt(0) || byte > "7".charCodeAt(0)) throw new LayaTarError("TAR_HEADER");
|
|
17737
|
+
const digit = byte - "0".charCodeAt(0);
|
|
17738
|
+
if (value > Math.floor((Number.MAX_SAFE_INTEGER - digit) / 8)) throw new LayaTarError("TAR_HEADER");
|
|
17739
|
+
value = value * 8 + digit;
|
|
17740
|
+
sawDigit = true;
|
|
17741
|
+
}
|
|
17742
|
+
if (!sawDigit) throw new LayaTarError("TAR_HEADER");
|
|
17743
|
+
return value;
|
|
17744
|
+
}
|
|
17745
|
+
function checksumField(bytes) {
|
|
17746
|
+
if (bytes.length !== 8) throw new LayaTarError("TAR_CHECKSUM");
|
|
17747
|
+
const standardLayout = bytes[6] === 0 && bytes[7] === 32;
|
|
17748
|
+
const archiverLayout = bytes[7] === 0 || bytes[7] === 32;
|
|
17749
|
+
if (!standardLayout && !archiverLayout) throw new LayaTarError("TAR_CHECKSUM");
|
|
17750
|
+
return strictOctalField(bytes.subarray(0, standardLayout ? 7 : 8));
|
|
17751
|
+
}
|
|
17752
|
+
function numericHeaderFields(header) {
|
|
17753
|
+
const mode = strictOctalField(header.subarray(100, 108));
|
|
17754
|
+
strictOctalField(header.subarray(108, 116));
|
|
17755
|
+
strictOctalField(header.subarray(116, 124));
|
|
17756
|
+
const size = strictOctalField(header.subarray(124, 136));
|
|
17757
|
+
strictOctalField(header.subarray(136, 148));
|
|
17758
|
+
strictOctalField(header.subarray(329, 337));
|
|
17759
|
+
strictOctalField(header.subarray(337, 345));
|
|
17760
|
+
return {
|
|
17761
|
+
mode,
|
|
17762
|
+
size
|
|
17763
|
+
};
|
|
17764
|
+
}
|
|
17765
|
+
function headerChecksum(header) {
|
|
17766
|
+
let sum = 0;
|
|
17767
|
+
for (let index = 0; index < header.length; index += 1) sum += index >= 148 && index < 156 ? 32 : header[index] ?? 0;
|
|
17768
|
+
return sum;
|
|
17769
|
+
}
|
|
17770
|
+
function packagePathIdentity(path) {
|
|
17771
|
+
if (path !== path.normalize("NFC")) throw new LayaTarError("TAR_PATH");
|
|
17772
|
+
return path.toUpperCase().toLowerCase().normalize("NFC");
|
|
17773
|
+
}
|
|
17774
|
+
function recordPathIdentities(path, identities) {
|
|
17775
|
+
const parts = path.split("/");
|
|
17776
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
17777
|
+
const candidate = parts.slice(0, index).join("/");
|
|
17778
|
+
const kind = index === parts.length ? "file" : "directory";
|
|
17779
|
+
const identity = packagePathIdentity(candidate);
|
|
17780
|
+
const previous = identities.get(identity);
|
|
17781
|
+
if (previous === void 0) {
|
|
17782
|
+
identities.set(identity, {
|
|
17783
|
+
path: candidate,
|
|
17784
|
+
kind
|
|
17785
|
+
});
|
|
17786
|
+
continue;
|
|
17787
|
+
}
|
|
17788
|
+
if (previous.path !== candidate || previous.kind !== kind) throw new LayaTarError("TAR_DUPLICATE");
|
|
17789
|
+
if (kind === "file") throw new LayaTarError("TAR_DUPLICATE");
|
|
17790
|
+
}
|
|
17791
|
+
}
|
|
17792
|
+
function parseEntry(header) {
|
|
17793
|
+
if (!header.subarray(257, 263).equals(Buffer.from("ustar\0", "ascii")) || !header.subarray(263, 265).equals(Buffer.from("00"))) throw new LayaTarError("TAR_HEADER");
|
|
17794
|
+
if (checksumField(header.subarray(148, 156)) !== headerChecksum(header)) throw new LayaTarError("TAR_CHECKSUM");
|
|
17795
|
+
const numeric = numericHeaderFields(header);
|
|
17796
|
+
if (!isRegularFile(header[156] ?? 0)) throw new LayaTarError("TAR_ENTRY_TYPE");
|
|
17797
|
+
const prefix = textField(header.subarray(345, 500));
|
|
17798
|
+
const name = textField(header.subarray(0, 100));
|
|
17799
|
+
const path = prefix.length === 0 ? name : `${prefix}/${name}`;
|
|
17800
|
+
if (path.includes("\\") || !isSafeRelativePath(path)) throw new LayaTarError("TAR_PATH");
|
|
17801
|
+
if (!path.startsWith("package/")) throw new LayaTarError("TAR_PATH");
|
|
17802
|
+
const relative = path.slice(8);
|
|
17803
|
+
packagePathIdentity(relative);
|
|
17804
|
+
return {
|
|
17805
|
+
path: relative,
|
|
17806
|
+
size: numeric.size,
|
|
17807
|
+
executable: (numeric.mode & 73) !== 0
|
|
17808
|
+
};
|
|
17809
|
+
}
|
|
17810
|
+
async function parseGzippedUstar(archive, onEntry) {
|
|
17811
|
+
const reader = new ByteReader(Readable.from([archive]).pipe(createGunzip()));
|
|
17812
|
+
const allPathIdentities = /* @__PURE__ */ new Map();
|
|
17813
|
+
while (true) {
|
|
17814
|
+
const header = await reader.readExactly(BLOCK_SIZE);
|
|
17815
|
+
if (header.every((byte) => byte === 0)) {
|
|
17816
|
+
if (!(await reader.readExactly(BLOCK_SIZE)).every((byte) => byte === 0)) throw new LayaTarError("TAR_HEADER");
|
|
17817
|
+
await reader.ensureEndIsZero();
|
|
17818
|
+
return;
|
|
17819
|
+
}
|
|
17820
|
+
const entry = parseEntry(header);
|
|
17821
|
+
recordPathIdentities(entry.path, allPathIdentities);
|
|
17822
|
+
const body = new TarBody(reader, entry.size);
|
|
17823
|
+
await onEntry(entry, body);
|
|
17824
|
+
await body.drain();
|
|
17825
|
+
const padding = (BLOCK_SIZE - entry.size % BLOCK_SIZE) % BLOCK_SIZE;
|
|
17826
|
+
if (padding > 0) await reader.readExactly(padding);
|
|
17827
|
+
}
|
|
17828
|
+
}
|
|
17829
|
+
function expectedFiles$1(files) {
|
|
17830
|
+
const result = /* @__PURE__ */ new Map();
|
|
17831
|
+
const identities = /* @__PURE__ */ new Map();
|
|
17832
|
+
for (const file of files) {
|
|
17833
|
+
if (!isSafeRelativePath(file.path) || !Number.isSafeInteger(file.size) || file.size < 0 || !/^[a-f0-9]{64}$/.test(file.sha256) || typeof file.executable !== "boolean") throw new LayaTarError("TAR_PATH");
|
|
17834
|
+
recordPathIdentities(file.path, identities);
|
|
17835
|
+
if (result.has(file.path)) throw new LayaTarError("TAR_DUPLICATE");
|
|
17836
|
+
result.set(file.path, file);
|
|
17837
|
+
}
|
|
17838
|
+
return result;
|
|
17839
|
+
}
|
|
17840
|
+
/** Validates a complete runtime tree listing before it is used to create or inspect a filesystem tree. */
|
|
17841
|
+
function validateRuntimeTreeFiles(files) {
|
|
17842
|
+
return expectedFiles$1(files);
|
|
17843
|
+
}
|
|
17844
|
+
async function archiveBytes(source) {
|
|
17845
|
+
const chunks = [];
|
|
17846
|
+
for await (const chunk of source) chunks.push(Buffer.from(chunk));
|
|
17847
|
+
return Buffer.concat(chunks);
|
|
17848
|
+
}
|
|
17849
|
+
async function validateExpectedPackageArchive(archive, expectedByPath) {
|
|
17850
|
+
const found = /* @__PURE__ */ new Set();
|
|
17851
|
+
await parseGzippedUstar(archive, async (entry, body) => {
|
|
17852
|
+
const expected = expectedByPath.get(entry.path);
|
|
17853
|
+
if (expected === void 0) return;
|
|
17854
|
+
if (entry.size !== expected.size) throw new LayaTarError("TAR_SIZE");
|
|
17855
|
+
if (entry.executable !== expected.executable) throw new LayaTarError("TAR_EXECUTABLE");
|
|
17856
|
+
const hash = createHash("sha256");
|
|
17857
|
+
for await (const chunk of body.chunks()) hash.update(chunk);
|
|
17858
|
+
if (hash.digest("hex") !== expected.sha256) throw new LayaTarError("TAR_HASH");
|
|
17859
|
+
found.add(entry.path);
|
|
17860
|
+
});
|
|
17861
|
+
for (const path of expectedByPath.keys()) if (!found.has(path)) throw new LayaTarError("TAR_MISSING");
|
|
17862
|
+
}
|
|
17863
|
+
async function makeOwnerOnlyDirectory(path) {
|
|
17864
|
+
await mkdir(path, {
|
|
17865
|
+
recursive: true,
|
|
17866
|
+
mode: OWNER_ONLY_DIRECTORY_MODE$2
|
|
17867
|
+
});
|
|
17868
|
+
await chmod(path, OWNER_ONLY_DIRECTORY_MODE$2);
|
|
17869
|
+
}
|
|
17870
|
+
async function writeAll(handle, bytes) {
|
|
17871
|
+
let offset = 0;
|
|
17872
|
+
while (offset < bytes.length) {
|
|
17873
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
|
|
17874
|
+
if (bytesWritten === 0) throw new LayaTarError("TAR_TRUNCATED");
|
|
17875
|
+
offset += bytesWritten;
|
|
17876
|
+
}
|
|
17877
|
+
}
|
|
17878
|
+
async function assertAbsent$1(path) {
|
|
17879
|
+
try {
|
|
17880
|
+
await lstat(path);
|
|
17881
|
+
} catch (error) {
|
|
17882
|
+
if (error.code === "ENOENT") return;
|
|
17883
|
+
throw error;
|
|
17884
|
+
}
|
|
17885
|
+
throw new LayaTarError("TAR_PATH");
|
|
17886
|
+
}
|
|
17887
|
+
/** Extracts only expected files from a strict package archive into a caller-owned owner-only partial tree. */
|
|
17888
|
+
async function extractPackageArchive(options) {
|
|
17889
|
+
const expectedByPath = expectedFiles$1(options.files);
|
|
17890
|
+
const archive = await archiveBytes(options.source);
|
|
17891
|
+
await validateExpectedPackageArchive(archive, expectedByPath);
|
|
17892
|
+
const written = /* @__PURE__ */ new Set();
|
|
17893
|
+
await makeOwnerOnlyDirectory(dirname(options.destination));
|
|
17894
|
+
await assertAbsent$1(options.destination);
|
|
17895
|
+
const staging = await mkdtemp(join(dirname(options.destination), `.${basename(options.destination)}.staging-`));
|
|
17896
|
+
await chmod(staging, OWNER_ONLY_DIRECTORY_MODE$2);
|
|
17897
|
+
let promoted = false;
|
|
17898
|
+
try {
|
|
17899
|
+
await parseGzippedUstar(archive, async (entry, body) => {
|
|
17900
|
+
const expected = expectedByPath.get(entry.path);
|
|
17901
|
+
if (expected === void 0) return;
|
|
17902
|
+
if (entry.size !== expected.size) throw new LayaTarError("TAR_SIZE");
|
|
17903
|
+
if (entry.executable !== expected.executable) throw new LayaTarError("TAR_EXECUTABLE");
|
|
17904
|
+
const target = join(staging, entry.path);
|
|
17905
|
+
await makeOwnerOnlyDirectory(dirname(target));
|
|
17906
|
+
const handle = await open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, OWNER_ONLY_FILE_MODE);
|
|
17907
|
+
const hash = createHash("sha256");
|
|
17908
|
+
try {
|
|
17909
|
+
for await (const chunk of body.chunks()) {
|
|
17910
|
+
hash.update(chunk);
|
|
17911
|
+
await writeAll(handle, chunk);
|
|
17912
|
+
}
|
|
17913
|
+
} finally {
|
|
17914
|
+
await handle.close();
|
|
17915
|
+
}
|
|
17916
|
+
if (hash.digest("hex") !== expected.sha256) {
|
|
17917
|
+
await rm(target, { force: true });
|
|
17918
|
+
throw new LayaTarError("TAR_HASH");
|
|
17919
|
+
}
|
|
17920
|
+
await chmod(target, expected.executable ? EXECUTABLE_FILE_MODE : OWNER_ONLY_FILE_MODE);
|
|
17921
|
+
written.add(entry.path);
|
|
17922
|
+
});
|
|
17923
|
+
for (const path of expectedByPath.keys()) if (!written.has(path)) throw new LayaTarError("TAR_MISSING");
|
|
17924
|
+
await assertAbsent$1(options.destination);
|
|
17925
|
+
await rename(staging, options.destination);
|
|
17926
|
+
promoted = true;
|
|
17927
|
+
} finally {
|
|
17928
|
+
if (!promoted) await rm(staging, {
|
|
17929
|
+
recursive: true,
|
|
17930
|
+
force: true
|
|
17931
|
+
});
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
//#endregion
|
|
17935
|
+
//#region src/runtime/laya/tree-verifier.ts
|
|
17936
|
+
function expectedFiles(files) {
|
|
17937
|
+
try {
|
|
17938
|
+
return new Map(validateRuntimeTreeFiles(files));
|
|
17939
|
+
} catch {
|
|
17940
|
+
return null;
|
|
17941
|
+
}
|
|
17942
|
+
}
|
|
17943
|
+
function expectedDirectories(expected) {
|
|
17944
|
+
const directories = /* @__PURE__ */ new Set();
|
|
17945
|
+
for (const path of expected.keys()) {
|
|
17946
|
+
const parts = path.split("/");
|
|
17947
|
+
for (let index = 1; index < parts.length; index += 1) directories.add(parts.slice(0, index).join("/"));
|
|
17948
|
+
}
|
|
17949
|
+
return directories;
|
|
17950
|
+
}
|
|
17951
|
+
async function sha256(path) {
|
|
17952
|
+
const hash = createHash("sha256");
|
|
17953
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
17954
|
+
return hash.digest("hex");
|
|
17955
|
+
}
|
|
17956
|
+
async function verifyDirectory(root, relative, expected, directories) {
|
|
17957
|
+
let entries;
|
|
17958
|
+
try {
|
|
17959
|
+
entries = await readdir(join(root, relative), {
|
|
17960
|
+
encoding: "utf8",
|
|
17961
|
+
withFileTypes: true
|
|
17962
|
+
});
|
|
17963
|
+
} catch {
|
|
17964
|
+
return {
|
|
17965
|
+
ok: false,
|
|
17966
|
+
code: "TREE_IO"
|
|
17967
|
+
};
|
|
17968
|
+
}
|
|
17969
|
+
entries.sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)));
|
|
17970
|
+
for (const entry of entries) {
|
|
17971
|
+
const relativePath = relative.length === 0 ? entry.name : `${relative}/${entry.name}`;
|
|
17972
|
+
let info;
|
|
17973
|
+
try {
|
|
17974
|
+
info = await lstat(join(root, relativePath));
|
|
17975
|
+
} catch {
|
|
17976
|
+
return {
|
|
17977
|
+
ok: false,
|
|
17978
|
+
code: "TREE_IO"
|
|
17979
|
+
};
|
|
17980
|
+
}
|
|
17981
|
+
if (info.isDirectory()) {
|
|
17982
|
+
if (!directories.has(relativePath)) return {
|
|
17983
|
+
ok: false,
|
|
17984
|
+
code: "TREE_EXTRA"
|
|
17985
|
+
};
|
|
17986
|
+
const child = await verifyDirectory(root, relativePath, expected, directories);
|
|
17987
|
+
if (!child.ok) return child;
|
|
17988
|
+
continue;
|
|
17989
|
+
}
|
|
17990
|
+
const expectedFile = expected.get(relativePath);
|
|
17991
|
+
if (expectedFile === void 0) return {
|
|
17992
|
+
ok: false,
|
|
17993
|
+
code: "TREE_EXTRA"
|
|
17994
|
+
};
|
|
17995
|
+
if (!info.isFile()) return {
|
|
17996
|
+
ok: false,
|
|
17997
|
+
code: "TREE_TYPE"
|
|
17998
|
+
};
|
|
17999
|
+
if (info.size !== expectedFile.size) return {
|
|
18000
|
+
ok: false,
|
|
18001
|
+
code: "TREE_SIZE"
|
|
18002
|
+
};
|
|
18003
|
+
if ((info.mode & 73) !== 0 !== expectedFile.executable) return {
|
|
18004
|
+
ok: false,
|
|
18005
|
+
code: "TREE_EXECUTABLE"
|
|
18006
|
+
};
|
|
18007
|
+
let digest;
|
|
18008
|
+
try {
|
|
18009
|
+
digest = await sha256(join(root, relativePath));
|
|
18010
|
+
} catch {
|
|
18011
|
+
return {
|
|
18012
|
+
ok: false,
|
|
18013
|
+
code: "TREE_IO"
|
|
18014
|
+
};
|
|
18015
|
+
}
|
|
18016
|
+
if (digest !== expectedFile.sha256) return {
|
|
18017
|
+
ok: false,
|
|
18018
|
+
code: "TREE_HASH"
|
|
18019
|
+
};
|
|
18020
|
+
expected.delete(relativePath);
|
|
18021
|
+
}
|
|
18022
|
+
return { ok: true };
|
|
18023
|
+
}
|
|
18024
|
+
/** Compares the complete selected runtime or model tree without following links or returning a path. */
|
|
18025
|
+
async function verifyRuntimeTree(options) {
|
|
18026
|
+
const expected = expectedFiles(options.files);
|
|
18027
|
+
if (expected === null) return {
|
|
18028
|
+
ok: false,
|
|
18029
|
+
code: "TREE_EXPECTED"
|
|
18030
|
+
};
|
|
18031
|
+
try {
|
|
18032
|
+
const root = await lstat(options.root);
|
|
18033
|
+
if (!root.isDirectory() || root.isSymbolicLink()) return {
|
|
18034
|
+
ok: false,
|
|
18035
|
+
code: "TREE_TYPE"
|
|
18036
|
+
};
|
|
18037
|
+
} catch {
|
|
18038
|
+
return {
|
|
18039
|
+
ok: false,
|
|
18040
|
+
code: "TREE_MISSING"
|
|
18041
|
+
};
|
|
18042
|
+
}
|
|
18043
|
+
const result = await verifyDirectory(options.root, "", expected, expectedDirectories(expected));
|
|
18044
|
+
if (!result.ok) return result;
|
|
18045
|
+
return expected.size === 0 ? { ok: true } : {
|
|
18046
|
+
ok: false,
|
|
18047
|
+
code: "TREE_MISSING"
|
|
18048
|
+
};
|
|
18049
|
+
}
|
|
18050
|
+
//#endregion
|
|
18051
|
+
//#region src/runtime/laya/runtime-installer.ts
|
|
18052
|
+
const OWNER_ONLY_DIRECTORY_MODE$1 = 448;
|
|
18053
|
+
var LayaRuntimeInstallError = class extends Error {
|
|
18054
|
+
code;
|
|
18055
|
+
constructor(code) {
|
|
18056
|
+
super(`Laya runtime installation refused: ${code}`);
|
|
18057
|
+
this.name = "LayaRuntimeInstallError";
|
|
18058
|
+
this.code = code;
|
|
18059
|
+
}
|
|
18060
|
+
};
|
|
18061
|
+
function isSafePackageName(packageName) {
|
|
18062
|
+
const parts = packageName.split("/");
|
|
18063
|
+
return (parts.length === 1 || parts.length === 2 && parts[0] !== void 0 && parts[0].startsWith("@")) && parts.every((part) => /^[a-z0-9][a-z0-9._-]*$/i.test(part.replace(/^@/, "")) && part !== "." && part !== "..");
|
|
18064
|
+
}
|
|
18065
|
+
async function ownerOnlyDirectory$1(path) {
|
|
18066
|
+
await mkdir(path, {
|
|
18067
|
+
recursive: true,
|
|
18068
|
+
mode: OWNER_ONLY_DIRECTORY_MODE$1
|
|
18069
|
+
});
|
|
18070
|
+
const info = await lstat(path);
|
|
18071
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new LayaRuntimeInstallError("RUNTIME_PATH");
|
|
18072
|
+
await chmod(path, OWNER_ONLY_DIRECTORY_MODE$1);
|
|
18073
|
+
}
|
|
18074
|
+
async function assertAbsent(path) {
|
|
18075
|
+
try {
|
|
18076
|
+
await lstat(path);
|
|
18077
|
+
} catch (error) {
|
|
18078
|
+
if (error.code === "ENOENT") return;
|
|
18079
|
+
throw error;
|
|
18080
|
+
}
|
|
18081
|
+
throw new LayaRuntimeInstallError("RUNTIME_EXISTS");
|
|
18082
|
+
}
|
|
18083
|
+
function filesForPackage(packageName, files) {
|
|
18084
|
+
const prefix = `node_modules/${packageName}/`;
|
|
18085
|
+
const selected = [];
|
|
18086
|
+
for (const file of files) {
|
|
18087
|
+
if (file.packageName !== packageName || !file.path.startsWith(prefix)) {
|
|
18088
|
+
if (file.packageName === packageName) throw new LayaRuntimeInstallError("RUNTIME_PATH");
|
|
18089
|
+
continue;
|
|
18090
|
+
}
|
|
18091
|
+
selected.push({
|
|
18092
|
+
...file,
|
|
18093
|
+
path: file.path.slice(prefix.length)
|
|
18094
|
+
});
|
|
18095
|
+
}
|
|
18096
|
+
return selected;
|
|
18097
|
+
}
|
|
18098
|
+
function validateInstallation(packages, files) {
|
|
18099
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
18100
|
+
for (const runtimePackage of packages) {
|
|
18101
|
+
if (!isSafePackageName(runtimePackage.packageName) || packageNames.has(runtimePackage.packageName)) throw new LayaRuntimeInstallError("RUNTIME_PATH");
|
|
18102
|
+
packageNames.add(runtimePackage.packageName);
|
|
18103
|
+
}
|
|
18104
|
+
for (const file of files) {
|
|
18105
|
+
if (!packageNames.has(file.packageName)) throw new LayaRuntimeInstallError("RUNTIME_PATH");
|
|
18106
|
+
if (!file.path.startsWith(`node_modules/${file.packageName}/`)) throw new LayaRuntimeInstallError("RUNTIME_PATH");
|
|
18107
|
+
}
|
|
18108
|
+
}
|
|
18109
|
+
/** Installs a complete selected lock tree into an owner-only partial directory and atomically promotes it once. */
|
|
18110
|
+
async function installRuntime(options) {
|
|
18111
|
+
if (!/^[a-f0-9]{64}$/.test(options.lockDigest)) throw new LayaRuntimeInstallError("RUNTIME_DIGEST");
|
|
18112
|
+
validateInstallation(options.packages, options.files);
|
|
18113
|
+
await ownerOnlyDirectory$1(options.runtimeDirectory);
|
|
18114
|
+
const root = join(options.runtimeDirectory, options.lockDigest);
|
|
18115
|
+
const partial = `${root}.partial`;
|
|
18116
|
+
await assertAbsent(root);
|
|
18117
|
+
await rm(partial, {
|
|
18118
|
+
recursive: true,
|
|
18119
|
+
force: true
|
|
18120
|
+
});
|
|
18121
|
+
await ownerOnlyDirectory$1(partial);
|
|
18122
|
+
try {
|
|
18123
|
+
for (const runtimePackage of options.packages) {
|
|
18124
|
+
const files = filesForPackage(runtimePackage.packageName, options.files);
|
|
18125
|
+
await extractPackageArchive({
|
|
18126
|
+
source: runtimePackage.source,
|
|
18127
|
+
destination: join(partial, "node_modules", runtimePackage.packageName),
|
|
18128
|
+
files
|
|
18129
|
+
});
|
|
18130
|
+
}
|
|
18131
|
+
if (!(await verifyRuntimeTree({
|
|
18132
|
+
root: partial,
|
|
18133
|
+
files: options.files
|
|
18134
|
+
})).ok) throw new LayaRuntimeInstallError("RUNTIME_TREE");
|
|
18135
|
+
await assertAbsent(root);
|
|
18136
|
+
await rename(partial, root);
|
|
18137
|
+
return { root };
|
|
18138
|
+
} catch (error) {
|
|
18139
|
+
await rm(partial, {
|
|
18140
|
+
recursive: true,
|
|
18141
|
+
force: true
|
|
18142
|
+
});
|
|
18143
|
+
if (error instanceof LayaTarError && error.code === "TAR_MISSING") throw new LayaRuntimeInstallError("RUNTIME_INCOMPLETE");
|
|
18144
|
+
throw error;
|
|
18145
|
+
}
|
|
18146
|
+
}
|
|
18147
|
+
//#endregion
|
|
18148
|
+
//#region src/runtime/laya/manager.ts
|
|
18149
|
+
const OWNER_ONLY_DIRECTORY_MODE = 448;
|
|
18150
|
+
/**
|
|
18151
|
+
* A content-free refusal a person or app can act on: its stable `details.reason` is `LAYA_ENABLED` (remove while the
|
|
18152
|
+
* kind is enabled) or `LAYA_NOT_READY` (a worker start before the kind is enabled); it names no path or content.
|
|
18153
|
+
*/
|
|
18154
|
+
function layaRefusal(reason, message) {
|
|
18155
|
+
return new CommsError(reason === "LAYA_ENABLED" ? "USAGE" : "CONFIG", message, { details: { reason } });
|
|
18156
|
+
}
|
|
18157
|
+
/** A job or repair whose selection, pointer or epoch moved on: it stops without writing anything. */
|
|
18158
|
+
var ArtifactWorkStale = class extends Error {
|
|
18159
|
+
constructor() {
|
|
18160
|
+
super("the Laya artifact work no longer owns the active selection");
|
|
18161
|
+
this.name = "ArtifactWorkStale";
|
|
18162
|
+
}
|
|
18163
|
+
};
|
|
18164
|
+
/** A closed failure the job records for `doctor`, the app and `judge model status`. */
|
|
18165
|
+
var ArtifactFailure = class extends Error {
|
|
18166
|
+
failureCode;
|
|
18167
|
+
constructor(failureCode) {
|
|
18168
|
+
super(`Laya artifact work failed: ${failureCode}`);
|
|
18169
|
+
this.name = "ArtifactFailure";
|
|
18170
|
+
this.failureCode = failureCode;
|
|
18171
|
+
}
|
|
18172
|
+
};
|
|
18173
|
+
/** One FIFO lock: every artifact promotion, deletion, disable, removal and update tightening runs inside it. */
|
|
18174
|
+
var ArtifactMutex = class {
|
|
18175
|
+
#tail = Promise.resolve();
|
|
18176
|
+
run(work) {
|
|
18177
|
+
const result = this.#tail.then(work, work);
|
|
18178
|
+
this.#tail = result.then(() => void 0, () => void 0);
|
|
18179
|
+
return result;
|
|
18180
|
+
}
|
|
18181
|
+
};
|
|
18182
|
+
/** The selected platform's runtime files as the installer and verifier read them: rooted at `node_modules/`. */
|
|
18183
|
+
function runtimeFiles(selection) {
|
|
18184
|
+
return selection.runtimeLock.platform.files.map((file) => ({
|
|
18185
|
+
packageName: file.package,
|
|
18186
|
+
path: `node_modules/${file.package}/${file.path}`,
|
|
18187
|
+
size: file.size,
|
|
18188
|
+
sha256: file.sha256,
|
|
18189
|
+
executable: file.executable
|
|
18190
|
+
}));
|
|
18191
|
+
}
|
|
18192
|
+
function modelFiles(selection) {
|
|
18193
|
+
return selection.manifest.files.map((file) => ({
|
|
18194
|
+
...file,
|
|
18195
|
+
executable: false
|
|
18196
|
+
}));
|
|
18197
|
+
}
|
|
18198
|
+
function bytesOf(files) {
|
|
18199
|
+
return files.reduce((sum, file) => sum + file.size, 0);
|
|
18200
|
+
}
|
|
18201
|
+
function totalBytes(selection) {
|
|
18202
|
+
return bytesOf(selection.manifest.files) + bytesOf(selection.runtimeLock.platform.files);
|
|
18203
|
+
}
|
|
18204
|
+
function record(value) {
|
|
18205
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
18206
|
+
}
|
|
18207
|
+
/**
|
|
18208
|
+
* D11's update rule over the complete approved entries: the enabled document must equal this release's document
|
|
18209
|
+
* byte for byte — its embedded manifest, its complete package list with every exact URL, version and SRI, and the
|
|
18210
|
+
* selected platform's complete file list, not merely a digest. A manifest difference is `model-changed`; any other
|
|
18211
|
+
* difference, a downgrade among them, is `runtime-changed`.
|
|
18212
|
+
*/
|
|
18213
|
+
function layaUpdateReason(stored, release) {
|
|
18214
|
+
if (canonicalJson(stored) === canonicalJson(release)) return null;
|
|
18215
|
+
return canonicalJson(record(stored)?.manifest ?? null) !== canonicalJson(release.manifest) ? "model-changed" : "runtime-changed";
|
|
18216
|
+
}
|
|
18217
|
+
async function isDirectory(path) {
|
|
18218
|
+
try {
|
|
18219
|
+
const info = await lstat(path);
|
|
18220
|
+
return info.isDirectory() && !info.isSymbolicLink();
|
|
18221
|
+
} catch {
|
|
18222
|
+
return false;
|
|
18223
|
+
}
|
|
18224
|
+
}
|
|
18225
|
+
async function ownerOnlyDirectory(path) {
|
|
18226
|
+
await mkdir(path, {
|
|
18227
|
+
recursive: true,
|
|
18228
|
+
mode: OWNER_ONLY_DIRECTORY_MODE
|
|
18229
|
+
});
|
|
18230
|
+
}
|
|
18231
|
+
async function availableBytes(path) {
|
|
18232
|
+
const stats = await statfs(path);
|
|
18233
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
18234
|
+
}
|
|
18235
|
+
/** The production fetcher, counting each streamed model byte for status only after the downloader's sink took it. */
|
|
18236
|
+
var ProgressReportingArtifactFetcher = class extends LayaArtifactFetcher {
|
|
18237
|
+
#progress;
|
|
18238
|
+
constructor(options, progress) {
|
|
18239
|
+
super(options);
|
|
18240
|
+
this.#progress = progress;
|
|
18241
|
+
}
|
|
18242
|
+
async fetchModelFileTo(file, resumeFrom, onChunk, options) {
|
|
18243
|
+
let received = resumeFrom ?? 0;
|
|
18244
|
+
this.#progress.set(file.path, received);
|
|
18245
|
+
return super.fetchModelFileTo(file, resumeFrom, async (chunk, signal) => {
|
|
18246
|
+
await onChunk(chunk, signal);
|
|
18247
|
+
received += chunk.length;
|
|
18248
|
+
this.#progress.set(file.path, received);
|
|
18249
|
+
}, options);
|
|
18250
|
+
}
|
|
18251
|
+
};
|
|
18252
|
+
/** How long a failed availability probe stands before the next request that needs it probes again. */
|
|
18253
|
+
const AVAILABILITY_RETRY_MS = 6e4;
|
|
18254
|
+
/**
|
|
18255
|
+
* The one serial owner of Laya's installation-wide artifacts. E stays the sole authority: the active Laya
|
|
18256
|
+
* judge-kind pointer starts every job, every post-await step rechecks that exact pointer, its embedded document and
|
|
18257
|
+
* this manager's epoch, and E's disable, purge and update tightenings stop the work they orphan. Only content-free
|
|
18258
|
+
* D8 model state is persisted; trees live under `runtime/<lockDigest>` and `models/<manifestId>` and nowhere else.
|
|
18259
|
+
*/
|
|
18260
|
+
var LayaArtifactManager = class {
|
|
18261
|
+
kind = JUDGE_KIND_LAYA;
|
|
18262
|
+
#store;
|
|
18263
|
+
#root;
|
|
18264
|
+
#inputs;
|
|
18265
|
+
#reviewed;
|
|
18266
|
+
#network;
|
|
18267
|
+
#freeBytes;
|
|
18268
|
+
#audit;
|
|
18269
|
+
#now;
|
|
18270
|
+
#failpoint;
|
|
18271
|
+
#models;
|
|
18272
|
+
#mutex = new ArtifactMutex();
|
|
18273
|
+
#tracked = /* @__PURE__ */ new Map();
|
|
18274
|
+
#availability;
|
|
18275
|
+
#availabilityProbedAt = 0;
|
|
18276
|
+
/** Bumped by every disable, removal, update tightening and stop: a job captures it and stops once it moves. */
|
|
18277
|
+
#epoch = 0;
|
|
18278
|
+
#job;
|
|
18279
|
+
#stopped = false;
|
|
18280
|
+
#worker;
|
|
18281
|
+
constructor(options) {
|
|
18282
|
+
this.#store = options.store;
|
|
18283
|
+
this.#root = options.artifactRoot;
|
|
18284
|
+
this.#inputs = options.inputs ?? {};
|
|
18285
|
+
this.#network = options.network ?? {};
|
|
18286
|
+
this.#freeBytes = options.freeBytes;
|
|
18287
|
+
this.#audit = options.audit;
|
|
18288
|
+
this.#now = options.now ?? Date.now;
|
|
18289
|
+
this.#failpoint = options.failpoint;
|
|
18290
|
+
this.#models = new LayaModelRepository(options.store.database);
|
|
18291
|
+
}
|
|
18292
|
+
/**
|
|
18293
|
+
* The reviewed definition, parsed on first use: an owner whose Laya is never asked about never reads its assets,
|
|
18294
|
+
* so a start-up needs them only once a Laya pointer, status or operation does.
|
|
18295
|
+
*/
|
|
18296
|
+
get #definition() {
|
|
18297
|
+
this.#reviewed ??= layaJudgeKindDefinitionFor(this.#inputs);
|
|
18298
|
+
return this.#reviewed;
|
|
18299
|
+
}
|
|
18300
|
+
/**
|
|
18301
|
+
* The content-free availability probe: a fact about this machine, never a document input. Only a success is kept for
|
|
18302
|
+
* the owner's life (E2 review round 1, B7); after a failure the next request that needs it probes again, at most once
|
|
18303
|
+
* a minute, so a sandbox that failed once is not taken as missing until the daemon restarts.
|
|
18304
|
+
*/
|
|
18305
|
+
availability() {
|
|
18306
|
+
const now = this.#now();
|
|
18307
|
+
if (this.#availability === void 0 || !this.#availability.available && now - this.#availabilityProbedAt >= AVAILABILITY_RETRY_MS) {
|
|
18308
|
+
this.#availability = this.#definition.availability();
|
|
18309
|
+
this.#availabilityProbedAt = now;
|
|
18310
|
+
}
|
|
18311
|
+
return this.#availability;
|
|
18312
|
+
}
|
|
18313
|
+
/**
|
|
18314
|
+
* The judge-kind version a Laya enable prepares: the newest live version if it is exactly this release's reviewed
|
|
18315
|
+
* document, otherwise a new immutable version of it. Unavailable refuses before any version, intent or file exists.
|
|
18316
|
+
*/
|
|
18317
|
+
enablementVersion() {
|
|
18318
|
+
const availability = this.availability();
|
|
18319
|
+
if (!availability.available) return unavailableResult(availability.code);
|
|
18320
|
+
return this.#store.immediate(() => {
|
|
18321
|
+
const latest = this.#store.database.prepare(`SELECT version, document FROM judge_kind_versions
|
|
18322
|
+
WHERE kind = ? AND revoked_at IS NULL ORDER BY version DESC LIMIT 1`).get(JUDGE_KIND_LAYA);
|
|
18323
|
+
if (latest !== void 0) {
|
|
18324
|
+
const release = this.#definition.preview({ version: latest.version });
|
|
18325
|
+
if (canonicalJson(JSON.parse(latest.document)) === canonicalJson(release)) return { version: latest.version };
|
|
18326
|
+
}
|
|
18327
|
+
const version = (this.#store.database.prepare("SELECT max(version) AS version FROM judge_kind_versions WHERE kind = ?").get("laya").version ?? 0) + 1;
|
|
18328
|
+
new ImmutableVersions(this.#store.database, this.#inputs).createJudgeKind(this.#definition.prepareEnablement({ version }));
|
|
18329
|
+
return { version };
|
|
18330
|
+
});
|
|
18331
|
+
}
|
|
18332
|
+
/** E's kind-list entry: the real pointer state while Laya is offered, its closed reason while it is not. */
|
|
18333
|
+
kindEntry() {
|
|
18334
|
+
const active = this.#activeEnablement();
|
|
18335
|
+
if (active !== null) return {
|
|
18336
|
+
kind: JUDGE_KIND_LAYA,
|
|
18337
|
+
state: "enabled",
|
|
18338
|
+
version: active.version
|
|
18339
|
+
};
|
|
18340
|
+
const availability = this.availability();
|
|
18341
|
+
if (!availability.available) return {
|
|
18342
|
+
kind: JUDGE_KIND_LAYA,
|
|
18343
|
+
state: "unavailable",
|
|
18344
|
+
code: availability.code
|
|
18345
|
+
};
|
|
18346
|
+
return {
|
|
18347
|
+
kind: JUDGE_KIND_LAYA,
|
|
18348
|
+
state: "disabled",
|
|
18349
|
+
version: null
|
|
18350
|
+
};
|
|
18351
|
+
}
|
|
18352
|
+
/**
|
|
18353
|
+
* Starts (or joins) the install for the active pointer. Before the enablement's claim there is no pointer, so this
|
|
18354
|
+
* does nothing: no request, row, directory or file. It never rejects; a failure becomes closed model state.
|
|
18355
|
+
*/
|
|
18356
|
+
ensureInstalled() {
|
|
18357
|
+
try {
|
|
18358
|
+
return this.#ensureInstalled();
|
|
18359
|
+
} catch {
|
|
18360
|
+
return Promise.resolve();
|
|
18361
|
+
}
|
|
18362
|
+
}
|
|
18363
|
+
#ensureInstalled() {
|
|
18364
|
+
if (this.#stopped) return Promise.resolve();
|
|
18365
|
+
const active = this.#activeEnablement();
|
|
18366
|
+
if (active === null) return Promise.resolve();
|
|
18367
|
+
const running = this.#job;
|
|
18368
|
+
if (running !== void 0 && running.epoch === this.#epoch && running.version === active.version) return running.settled;
|
|
18369
|
+
const selection = this.#selectionFor(active);
|
|
18370
|
+
if (selection === null || !this.availability().available) return Promise.resolve();
|
|
18371
|
+
const job = {
|
|
18372
|
+
epoch: this.#epoch,
|
|
18373
|
+
version: active.version,
|
|
18374
|
+
digest: active.digest,
|
|
18375
|
+
selection,
|
|
18376
|
+
controller: new AbortController(),
|
|
18377
|
+
phase: "installing",
|
|
18378
|
+
runtimeBytes: 0,
|
|
18379
|
+
modelProgress: /* @__PURE__ */ new Map(),
|
|
18380
|
+
settled: Promise.resolve()
|
|
18381
|
+
};
|
|
18382
|
+
const previous = running?.settled ?? Promise.resolve();
|
|
18383
|
+
this.#job = job;
|
|
18384
|
+
job.settled = previous.then(() => this.#install(job)).finally(() => {
|
|
18385
|
+
if (this.#job === job) this.#job = void 0;
|
|
18386
|
+
});
|
|
18387
|
+
return job.settled;
|
|
18388
|
+
}
|
|
18389
|
+
/** Start-up's resume: an approved, unfinished install continues; a ready or failed model waits for its person. */
|
|
18390
|
+
async resume() {
|
|
18391
|
+
const active = this.#activeEnablement();
|
|
18392
|
+
if (active === null) return;
|
|
18393
|
+
const selection = this.#selectionFor(active);
|
|
18394
|
+
if (selection === null) return;
|
|
18395
|
+
const row = this.#models.get(selection.manifest.manifestId);
|
|
18396
|
+
if (row?.state === "ready" || row?.state === "failed") return;
|
|
18397
|
+
await this.ensureInstalled();
|
|
18398
|
+
}
|
|
18399
|
+
/**
|
|
18400
|
+
* E's immediate kind disable, wrapped: this manager's work is fenced and aborted first, E's whole disable
|
|
18401
|
+
* transaction runs under the artifact lock (so no promotion can land between its last recheck and its rename), and
|
|
18402
|
+
* then the partial and staging trees the disable orphaned are deleted. Verified trees survive.
|
|
18403
|
+
*/
|
|
18404
|
+
async disableKind(disable) {
|
|
18405
|
+
const job = this.#fence();
|
|
18406
|
+
const result = await this.#mutex.run(async () => {
|
|
18407
|
+
const disabled = disable();
|
|
18408
|
+
await this.#removeUnverified();
|
|
18409
|
+
return disabled;
|
|
18410
|
+
});
|
|
18411
|
+
await job?.settled.catch(() => void 0);
|
|
18412
|
+
await this.#worker?.stop();
|
|
18413
|
+
return result;
|
|
18414
|
+
}
|
|
18415
|
+
/**
|
|
18416
|
+
* D11's start-up update rule. While Laya is enabled, its approved document is compared with this release's; any
|
|
18417
|
+
* difference applies E's kind-disable tightening with `model-changed` or `runtime-changed`, keeps verified trees,
|
|
18418
|
+
* and never re-approves. The tightening rechecks that the compared pointer is still the active one.
|
|
18419
|
+
*/
|
|
18420
|
+
async reconcileRelease(disable) {
|
|
18421
|
+
const compared = this.#activeEnablement();
|
|
18422
|
+
if (compared === null) return null;
|
|
18423
|
+
let release;
|
|
18424
|
+
try {
|
|
18425
|
+
release = this.#definition.preview({ version: compared.version });
|
|
18426
|
+
} catch {
|
|
18427
|
+
release = null;
|
|
18428
|
+
}
|
|
18429
|
+
const reason = release === null ? "runtime-changed" : layaUpdateReason(compared.document, release);
|
|
18430
|
+
if (reason === null) return null;
|
|
18431
|
+
const job = this.#fence();
|
|
18432
|
+
const applied = await this.#locked("update-disable", async () => {
|
|
18433
|
+
const live = this.#activeEnablement();
|
|
18434
|
+
if (live === null || live.version !== compared.version || live.digest !== compared.digest) return false;
|
|
18435
|
+
disable();
|
|
18436
|
+
await this.#removeUnverified();
|
|
18437
|
+
return true;
|
|
18438
|
+
});
|
|
18439
|
+
await job?.settled.catch(() => void 0);
|
|
18440
|
+
if (!applied) return null;
|
|
18441
|
+
await this.#worker?.stop();
|
|
18442
|
+
await this.#audit?.append({
|
|
18443
|
+
inboxId: "",
|
|
18444
|
+
operation: "events.disableJudgeKind",
|
|
18445
|
+
outcome: "ok",
|
|
18446
|
+
reason,
|
|
18447
|
+
ids: {
|
|
18448
|
+
kind: JUDGE_KIND_LAYA,
|
|
18449
|
+
version: String(compared.version)
|
|
18450
|
+
},
|
|
18451
|
+
surface: "daemon",
|
|
18452
|
+
origin: "daemon"
|
|
18453
|
+
});
|
|
18454
|
+
return reason;
|
|
18455
|
+
}
|
|
18456
|
+
/**
|
|
18457
|
+
* `judge model remove` (D11): only while Laya is disabled, deletes every verified runtime and model tree under the
|
|
18458
|
+
* Laya root — an earlier release's first, then this release's own with its unverified copies. Another selection's
|
|
18459
|
+
* partial or staging tree is a disable's to delete, never a removal's. Each asynchronous deletion is followed by a
|
|
18460
|
+
* recheck that Laya is still disabled; an enablement claimed meanwhile stops the removal and owns what remains.
|
|
18461
|
+
*/
|
|
18462
|
+
async remove() {
|
|
18463
|
+
await this.#mutex.run(async () => {
|
|
18464
|
+
if (this.#activeEnablement() !== null) throw layaRefusal("LAYA_ENABLED", "disable the Laya judge kind before removing its model");
|
|
18465
|
+
if (!await this.#removeEarlierVerifiedTrees()) return;
|
|
18466
|
+
const manifestId = this.#manifestId();
|
|
18467
|
+
const recorded = manifestId === null ? null : this.#models.get(manifestId);
|
|
18468
|
+
if (recorded === null || recorded.state === "removed") return;
|
|
18469
|
+
const release = this.#releaseSelection();
|
|
18470
|
+
if (release !== null) {
|
|
18471
|
+
const runtimeRoot = this.#runtimeRoot(release);
|
|
18472
|
+
await rm(runtimeRoot, {
|
|
18473
|
+
recursive: true,
|
|
18474
|
+
force: true
|
|
18475
|
+
});
|
|
18476
|
+
await rm(`${runtimeRoot}.staging`, {
|
|
18477
|
+
recursive: true,
|
|
18478
|
+
force: true
|
|
18479
|
+
});
|
|
18480
|
+
}
|
|
18481
|
+
await this.#failpoint?.("remove-model");
|
|
18482
|
+
if (this.#activeEnablement() !== null) return;
|
|
18483
|
+
if (manifestId !== null) {
|
|
18484
|
+
const paths = layaModelArtifactPaths(this.#root, manifestId);
|
|
18485
|
+
await rm(paths.complete, {
|
|
18486
|
+
recursive: true,
|
|
18487
|
+
force: true
|
|
18488
|
+
});
|
|
18489
|
+
await rm(paths.partial, {
|
|
18490
|
+
recursive: true,
|
|
18491
|
+
force: true
|
|
18492
|
+
});
|
|
18493
|
+
}
|
|
18494
|
+
if (this.#activeEnablement() !== null || manifestId === null) return;
|
|
18495
|
+
this.#store.immediate(() => this.#models.markRemovedWhileDisabled({
|
|
18496
|
+
manifestId,
|
|
18497
|
+
now: this.#now()
|
|
18498
|
+
}));
|
|
18499
|
+
});
|
|
18500
|
+
return this.status();
|
|
18501
|
+
}
|
|
18502
|
+
/**
|
|
18503
|
+
* Immediately before a worker may start: both whole trees are walked and compared with the enabled lock and
|
|
18504
|
+
* manifest. A failing tree is recorded as `integrity`, deleted and reinstalled once from its pinned source; if the
|
|
18505
|
+
* fresh copy fails too, Laya stays `failed` until its person acts. Paths stay inside the daemon.
|
|
18506
|
+
*/
|
|
18507
|
+
async verifyForWorkerStart() {
|
|
18508
|
+
const active = this.#activeEnablement();
|
|
18509
|
+
if (active === null) throw layaRefusal("LAYA_NOT_READY", "the Laya judge kind is not enabled");
|
|
18510
|
+
const selection = this.#selectionFor(active);
|
|
18511
|
+
const row = selection === null ? null : this.#models.get(selection.manifest.manifestId);
|
|
18512
|
+
if (selection === null || row?.state !== "ready" || this.#job !== void 0) return {
|
|
18513
|
+
ok: false,
|
|
18514
|
+
failureCode: row?.failureCode ?? null
|
|
18515
|
+
};
|
|
18516
|
+
const walkedEpoch = this.#epoch;
|
|
18517
|
+
const runtimeRoot = this.#runtimeRoot(selection);
|
|
18518
|
+
const modelRoot = layaModelArtifactPaths(this.#root, selection.manifest.manifestId).complete;
|
|
18519
|
+
const runtimeOk = (await verifyRuntimeTree({
|
|
18520
|
+
root: runtimeRoot,
|
|
18521
|
+
files: runtimeFiles(selection)
|
|
18522
|
+
})).ok;
|
|
18523
|
+
const modelOk = (await verifyRuntimeTree({
|
|
18524
|
+
root: modelRoot,
|
|
18525
|
+
files: modelFiles(selection)
|
|
18526
|
+
})).ok;
|
|
18527
|
+
await this.#failpoint?.("worker-start-verify");
|
|
18528
|
+
if (runtimeOk && modelOk) return {
|
|
18529
|
+
ok: true,
|
|
18530
|
+
runtimeRoot,
|
|
18531
|
+
modelRoot
|
|
18532
|
+
};
|
|
18533
|
+
if (walkedEpoch !== this.#epoch || this.#job !== void 0) return {
|
|
18534
|
+
ok: false,
|
|
18535
|
+
failureCode: null
|
|
18536
|
+
};
|
|
18537
|
+
const job = this.#repairJob(active, selection);
|
|
18538
|
+
const settled = this.#repair(job, {
|
|
18539
|
+
runtime: !runtimeOk,
|
|
18540
|
+
model: !modelOk
|
|
18541
|
+
});
|
|
18542
|
+
job.settled = settled.then(() => void 0, () => void 0);
|
|
18543
|
+
this.#job = job;
|
|
18544
|
+
try {
|
|
18545
|
+
return await settled ? {
|
|
18546
|
+
ok: true,
|
|
18547
|
+
runtimeRoot,
|
|
18548
|
+
modelRoot
|
|
18549
|
+
} : {
|
|
18550
|
+
ok: false,
|
|
18551
|
+
failureCode: "integrity"
|
|
18552
|
+
};
|
|
18553
|
+
} finally {
|
|
18554
|
+
if (this.#job === job) this.#job = void 0;
|
|
18555
|
+
}
|
|
18556
|
+
}
|
|
18557
|
+
/** The stable status answer: the DTO `judge model status`, `doctor` and the desktop app read. */
|
|
18558
|
+
status() {
|
|
18559
|
+
const availability = this.availability();
|
|
18560
|
+
const active = this.#activeEnablement();
|
|
18561
|
+
if (active === null) {
|
|
18562
|
+
if (!availability.available) {
|
|
18563
|
+
const blocked = this.#definition.status();
|
|
18564
|
+
return {
|
|
18565
|
+
state: "unavailable",
|
|
18566
|
+
manifestId: blocked.manifestId,
|
|
18567
|
+
runtimeLockDigest: blocked.runtimeLockDigest,
|
|
18568
|
+
bytesVerified: blocked.bytesVerified,
|
|
18569
|
+
totalBytes: blocked.totalBytes,
|
|
18570
|
+
failureCode: null,
|
|
18571
|
+
reason: availability.code,
|
|
18572
|
+
updatedAt: blocked.updatedAt
|
|
18573
|
+
};
|
|
18574
|
+
}
|
|
18575
|
+
const release = this.#releaseSelection();
|
|
18576
|
+
if (release === null) return this.#definition.status();
|
|
18577
|
+
const row = this.#models.get(release.manifest.manifestId);
|
|
18578
|
+
return {
|
|
18579
|
+
state: "idle",
|
|
18580
|
+
manifestId: release.manifest.manifestId,
|
|
18581
|
+
runtimeLockDigest: release.runtimeLock.digest,
|
|
18582
|
+
bytesVerified: row?.state === "ready" ? row.bytesVerified : 0,
|
|
18583
|
+
totalBytes: totalBytes(release),
|
|
18584
|
+
failureCode: null,
|
|
18585
|
+
reason: null,
|
|
18586
|
+
updatedAt: row?.updatedAt ?? null
|
|
18587
|
+
};
|
|
18588
|
+
}
|
|
18589
|
+
const enabled = this.#enabledShape(active);
|
|
18590
|
+
const row = enabled === null ? null : this.#models.get(enabled.manifestId);
|
|
18591
|
+
const answer = (state, bytesVerified, failureCode, reason) => ({
|
|
18592
|
+
state,
|
|
18593
|
+
manifestId: enabled?.manifestId ?? null,
|
|
18594
|
+
runtimeLockDigest: enabled?.runtimeLockDigest ?? null,
|
|
18595
|
+
bytesVerified,
|
|
18596
|
+
totalBytes: enabled?.totalBytes ?? 0,
|
|
18597
|
+
failureCode,
|
|
18598
|
+
reason,
|
|
18599
|
+
updatedAt: row?.updatedAt ?? null
|
|
18600
|
+
});
|
|
18601
|
+
if (!availability.available) return answer("failed", row?.bytesVerified ?? 0, "boundary-failed", availability.code);
|
|
18602
|
+
const job = this.#job;
|
|
18603
|
+
if (job !== void 0 && job.version === active.version && job.epoch === this.#epoch) return answer(job.phase, this.#progress(job), null, null);
|
|
18604
|
+
if (row === null || row.state === "removed") return answer("installing", 0, null, null);
|
|
18605
|
+
return answer(row.state, row.bytesVerified, row.state === "failed" ? row.failureCode : null, null);
|
|
18606
|
+
}
|
|
18607
|
+
/** The doctor's view: the status answer plus the runtime and model states and the boundary reason, never a path. */
|
|
18608
|
+
async doctor() {
|
|
18609
|
+
const status = this.status();
|
|
18610
|
+
const availability = this.availability();
|
|
18611
|
+
const job = this.#job;
|
|
18612
|
+
let runtime = "absent";
|
|
18613
|
+
if (job !== void 0 && job.phase === "installing") runtime = "installing";
|
|
18614
|
+
else if (status.runtimeLockDigest !== null && /^[a-f0-9]{64}$/.test(status.runtimeLockDigest)) runtime = await isDirectory(join(this.#root, "runtime", status.runtimeLockDigest)) ? "installed" : "absent";
|
|
18615
|
+
const row = status.manifestId === null ? null : this.#models.get(status.manifestId);
|
|
18616
|
+
return {
|
|
18617
|
+
state: status.state,
|
|
18618
|
+
manifestId: status.manifestId,
|
|
18619
|
+
runtimeLockDigest: status.runtimeLockDigest,
|
|
18620
|
+
runtime,
|
|
18621
|
+
model: row?.state ?? "absent",
|
|
18622
|
+
boundary: availability.available ? {
|
|
18623
|
+
status: "active",
|
|
18624
|
+
reason: null
|
|
18625
|
+
} : {
|
|
18626
|
+
status: "unavailable",
|
|
18627
|
+
reason: availability.code
|
|
18628
|
+
},
|
|
18629
|
+
failureCode: status.failureCode,
|
|
18630
|
+
bytesVerified: status.bytesVerified,
|
|
18631
|
+
totalBytes: status.totalBytes
|
|
18632
|
+
};
|
|
18633
|
+
}
|
|
18634
|
+
/** Task 8's one worker supervisor; the returned function detaches exactly that supervisor. */
|
|
18635
|
+
attachWorker(worker) {
|
|
18636
|
+
if (this.#worker !== void 0) throw new Error("a Laya worker supervisor is already attached");
|
|
18637
|
+
this.#worker = worker;
|
|
18638
|
+
return () => {
|
|
18639
|
+
if (this.#worker === worker) this.#worker = void 0;
|
|
18640
|
+
};
|
|
18641
|
+
}
|
|
18642
|
+
/**
|
|
18643
|
+
* The selection a worker may start for: the active pointer, only while its document is exactly this release's
|
|
18644
|
+
* reviewed one, with the manager's current epoch. Null before an enablement, after a disable, an update or a stop.
|
|
18645
|
+
*/
|
|
18646
|
+
workerSelection() {
|
|
18647
|
+
if (this.#stopped) return null;
|
|
18648
|
+
const active = this.#activeEnablement();
|
|
18649
|
+
if (active === null) return null;
|
|
18650
|
+
const selection = this.#selectionFor(active);
|
|
18651
|
+
if (selection === null) return null;
|
|
18652
|
+
return {
|
|
18653
|
+
epoch: this.#epoch,
|
|
18654
|
+
version: active.version,
|
|
18655
|
+
digest: active.digest,
|
|
18656
|
+
manifestId: selection.manifest.manifestId,
|
|
18657
|
+
promptTemplates: selection.manifest.promptTemplates
|
|
18658
|
+
};
|
|
18659
|
+
}
|
|
18660
|
+
/**
|
|
18661
|
+
* D11: a worker whose per-start boundary proof failed, or whose verified runtime would not load, leaves Laya
|
|
18662
|
+
* `failed` with that closed code until its person acts — no later request starts another child for it. The write is
|
|
18663
|
+
* made only while the selection that worker verified is still the current one, in the same transaction.
|
|
18664
|
+
*/
|
|
18665
|
+
recordWorkerFailure(started, failureCode) {
|
|
18666
|
+
try {
|
|
18667
|
+
this.#store.immediate(() => {
|
|
18668
|
+
const current = this.workerSelection();
|
|
18669
|
+
if (current === null || current.epoch !== started.epoch || current.version !== started.version || current.digest !== started.digest) return;
|
|
18670
|
+
const row = this.#models.get(started.manifestId);
|
|
18671
|
+
if (row?.state !== "ready") return;
|
|
18672
|
+
this.#models.write({
|
|
18673
|
+
manifestId: started.manifestId,
|
|
18674
|
+
state: "failed",
|
|
18675
|
+
bytesVerified: row.bytesVerified,
|
|
18676
|
+
failureCode,
|
|
18677
|
+
now: this.#now()
|
|
18678
|
+
});
|
|
18679
|
+
});
|
|
18680
|
+
} catch {}
|
|
18681
|
+
}
|
|
18682
|
+
/**
|
|
18683
|
+
* Task 8's supervisor registers each in-flight worker request by its E reservation. The returned signal fires when
|
|
18684
|
+
* an E purge terminalises exactly that reservation; nothing else is touched.
|
|
18685
|
+
*/
|
|
18686
|
+
trackWork(reservationId) {
|
|
18687
|
+
const controller = new AbortController();
|
|
18688
|
+
const work = {
|
|
18689
|
+
reservationId,
|
|
18690
|
+
controller
|
|
18691
|
+
};
|
|
18692
|
+
this.#tracked.set(reservationId, work);
|
|
18693
|
+
return {
|
|
18694
|
+
signal: controller.signal,
|
|
18695
|
+
release: () => {
|
|
18696
|
+
if (this.#tracked.get(reservationId) === work) this.#tracked.delete(reservationId);
|
|
18697
|
+
}
|
|
18698
|
+
};
|
|
18699
|
+
}
|
|
18700
|
+
/**
|
|
18701
|
+
* E's purge participant calls this inside the purge transaction, after its recovery transition. Only tracked work
|
|
18702
|
+
* whose own reservation that transition settled or released is stopped: an account, rule, hold, visibility,
|
|
18703
|
+
* expiry or disable-all purge never touches an installation-wide verified tree or another scope's work.
|
|
18704
|
+
*/
|
|
18705
|
+
onWorkPurged() {
|
|
18706
|
+
for (const work of [...this.#tracked.values()]) {
|
|
18707
|
+
const row = this.#store.database.prepare("SELECT state FROM judge_budget_reservations WHERE id = ?").get(work.reservationId);
|
|
18708
|
+
if (row !== void 0 && (row.state === "reserved" || row.state === "in-flight")) continue;
|
|
18709
|
+
this.#tracked.delete(work.reservationId);
|
|
18710
|
+
work.controller.abort(new ArtifactWorkStale());
|
|
18711
|
+
}
|
|
18712
|
+
}
|
|
18713
|
+
/** Owner stop: no job outlives the database it writes to. */
|
|
18714
|
+
async stop() {
|
|
18715
|
+
this.#stopped = true;
|
|
18716
|
+
const job = this.#fence();
|
|
18717
|
+
for (const work of this.#tracked.values()) work.controller.abort(new ArtifactWorkStale());
|
|
18718
|
+
this.#tracked.clear();
|
|
18719
|
+
await job?.settled.catch(() => void 0);
|
|
18720
|
+
await this.#worker?.stop();
|
|
18721
|
+
}
|
|
18722
|
+
#fence() {
|
|
18723
|
+
this.#epoch += 1;
|
|
18724
|
+
const job = this.#job;
|
|
18725
|
+
job?.controller.abort(new ArtifactWorkStale());
|
|
18726
|
+
return job;
|
|
18727
|
+
}
|
|
18728
|
+
#locked(edge, work) {
|
|
18729
|
+
return this.#mutex.run(async () => {
|
|
18730
|
+
await this.#failpoint?.(edge);
|
|
18731
|
+
return work();
|
|
18732
|
+
});
|
|
18733
|
+
}
|
|
18734
|
+
#activeEnablement() {
|
|
18735
|
+
const row = this.#store.database.prepare(`SELECT active.version AS version, versions.document AS document, versions.digest AS digest
|
|
18736
|
+
FROM active_versions AS active
|
|
18737
|
+
JOIN judge_kind_versions AS versions
|
|
18738
|
+
ON versions.kind = active.object_id AND versions.version = active.version
|
|
18739
|
+
WHERE active.kind = 'judge-kind' AND active.object_id = ?`).get(JUDGE_KIND_LAYA);
|
|
18740
|
+
if (row === void 0) return null;
|
|
18741
|
+
let document;
|
|
18742
|
+
try {
|
|
18743
|
+
document = JSON.parse(row.document);
|
|
18744
|
+
} catch {
|
|
18745
|
+
document = null;
|
|
18746
|
+
}
|
|
18747
|
+
return {
|
|
18748
|
+
version: row.version,
|
|
18749
|
+
digest: row.digest,
|
|
18750
|
+
document
|
|
18751
|
+
};
|
|
18752
|
+
}
|
|
18753
|
+
/** The active document, only when it is exactly this release's reviewed document; an update leaves it null. */
|
|
18754
|
+
#selectionFor(active) {
|
|
18755
|
+
try {
|
|
18756
|
+
const release = this.#definition.preview({ version: active.version });
|
|
18757
|
+
return layaUpdateReason(active.document, release) === null ? release : null;
|
|
18758
|
+
} catch {
|
|
18759
|
+
return null;
|
|
18760
|
+
}
|
|
18761
|
+
}
|
|
18762
|
+
#releaseSelection() {
|
|
18763
|
+
try {
|
|
18764
|
+
return this.#definition.preview({ version: 1 });
|
|
18765
|
+
} catch {
|
|
18766
|
+
return null;
|
|
18767
|
+
}
|
|
18768
|
+
}
|
|
18769
|
+
#manifestId() {
|
|
18770
|
+
return this.#releaseSelection()?.manifest.manifestId ?? this.#definition.status().manifestId;
|
|
18771
|
+
}
|
|
18772
|
+
/** The content-free identity of an enabled document, read even when this release no longer matches it. */
|
|
18773
|
+
#enabledShape(active) {
|
|
18774
|
+
const selection = this.#selectionFor(active);
|
|
18775
|
+
if (selection !== null) return {
|
|
18776
|
+
manifestId: selection.manifest.manifestId,
|
|
18777
|
+
runtimeLockDigest: selection.runtimeLock.digest,
|
|
18778
|
+
totalBytes: totalBytes(selection)
|
|
18779
|
+
};
|
|
18780
|
+
const document = active.document;
|
|
18781
|
+
const manifestId = document?.manifest?.manifestId;
|
|
18782
|
+
const digest = document?.runtimeLock?.digest;
|
|
18783
|
+
if (typeof manifestId !== "string" || typeof digest !== "string") return null;
|
|
18784
|
+
return {
|
|
18785
|
+
manifestId,
|
|
18786
|
+
runtimeLockDigest: digest,
|
|
18787
|
+
totalBytes: bytesOf([...document?.manifest?.files ?? [], ...document?.runtimeLock?.platform?.files ?? []])
|
|
18788
|
+
};
|
|
18789
|
+
}
|
|
18790
|
+
#runtimeRoot(selection) {
|
|
18791
|
+
const digest = selection.runtimeLock.digest;
|
|
18792
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) throw new ArtifactFailure("runtime-failed");
|
|
18793
|
+
return join(this.#root, "runtime", digest);
|
|
18794
|
+
}
|
|
18795
|
+
#progress(job) {
|
|
18796
|
+
let model = 0;
|
|
18797
|
+
for (const bytes of job.modelProgress.values()) model += bytes;
|
|
18798
|
+
return Math.min(job.runtimeBytes + model, totalBytes(job.selection));
|
|
18799
|
+
}
|
|
18800
|
+
/** The synchronous recheck every post-await step makes: same pointer version, same document, same epoch. */
|
|
18801
|
+
#assertCurrent(job) {
|
|
18802
|
+
if (this.#stopped || job.epoch !== this.#epoch || job.controller.signal.aborted) throw new ArtifactWorkStale();
|
|
18803
|
+
const active = this.#activeEnablement();
|
|
18804
|
+
if (active === null || active.version !== job.version || active.digest !== job.digest) throw new ArtifactWorkStale();
|
|
18805
|
+
}
|
|
18806
|
+
#isCurrent(job) {
|
|
18807
|
+
try {
|
|
18808
|
+
this.#assertCurrent(job);
|
|
18809
|
+
return true;
|
|
18810
|
+
} catch {
|
|
18811
|
+
return false;
|
|
18812
|
+
}
|
|
18813
|
+
}
|
|
18814
|
+
#write(job, state, bytesVerified, failureCode) {
|
|
18815
|
+
this.#store.immediate(() => {
|
|
18816
|
+
this.#assertCurrent(job);
|
|
18817
|
+
this.#models.write({
|
|
18818
|
+
manifestId: job.selection.manifest.manifestId,
|
|
18819
|
+
state,
|
|
18820
|
+
bytesVerified,
|
|
18821
|
+
...state === "ready" ? { verifiedAt: this.#now() } : {},
|
|
18822
|
+
...failureCode === void 0 ? {} : { failureCode },
|
|
18823
|
+
now: this.#now()
|
|
18824
|
+
});
|
|
18825
|
+
});
|
|
18826
|
+
}
|
|
18827
|
+
#fenceFor(job) {
|
|
18828
|
+
return {
|
|
18829
|
+
assertCurrent: async (stage) => {
|
|
18830
|
+
if (stage === "promotion") await this.#failpoint?.("model-promotion");
|
|
18831
|
+
this.#assertCurrent(job);
|
|
18832
|
+
},
|
|
18833
|
+
withArtifactLock: (work) => this.#mutex.run(work)
|
|
18834
|
+
};
|
|
18835
|
+
}
|
|
18836
|
+
async #install(job) {
|
|
18837
|
+
try {
|
|
18838
|
+
await this.#locked("install-start", async () => {
|
|
18839
|
+
this.#write(job, "installing", 0);
|
|
18840
|
+
});
|
|
18841
|
+
const selection = job.selection;
|
|
18842
|
+
const runtimeRoot = this.#runtimeRoot(selection);
|
|
18843
|
+
if (!(await verifyRuntimeTree({
|
|
18844
|
+
root: runtimeRoot,
|
|
18845
|
+
files: runtimeFiles(selection)
|
|
18846
|
+
})).ok) {
|
|
18847
|
+
this.#assertCurrent(job);
|
|
18848
|
+
await this.#installRuntime(job, { replace: await isDirectory(runtimeRoot) });
|
|
18849
|
+
}
|
|
18850
|
+
this.#assertCurrent(job);
|
|
18851
|
+
job.runtimeBytes = bytesOf(selection.runtimeLock.platform.files);
|
|
18852
|
+
this.#write(job, "downloading", job.runtimeBytes);
|
|
18853
|
+
job.phase = "downloading";
|
|
18854
|
+
const modelRoot = layaModelArtifactPaths(this.#root, selection.manifest.manifestId).complete;
|
|
18855
|
+
if (!(await verifyRuntimeTree({
|
|
18856
|
+
root: modelRoot,
|
|
18857
|
+
files: modelFiles(selection)
|
|
18858
|
+
})).ok) {
|
|
18859
|
+
this.#assertCurrent(job);
|
|
18860
|
+
await this.#downloadModel(job, { replace: await isDirectory(modelRoot) });
|
|
18861
|
+
}
|
|
18862
|
+
job.phase = "verifying";
|
|
18863
|
+
const runtime = await verifyRuntimeTree({
|
|
18864
|
+
root: runtimeRoot,
|
|
18865
|
+
files: runtimeFiles(selection)
|
|
18866
|
+
});
|
|
18867
|
+
const model = await verifyRuntimeTree({
|
|
18868
|
+
root: modelRoot,
|
|
18869
|
+
files: modelFiles(selection)
|
|
18870
|
+
});
|
|
18871
|
+
if (!runtime.ok || !model.ok) throw new ArtifactFailure("integrity");
|
|
18872
|
+
await this.#locked("model-ready", async () => {
|
|
18873
|
+
this.#write(job, "ready", totalBytes(selection));
|
|
18874
|
+
await this.#removeObsoleteTrees(job);
|
|
18875
|
+
});
|
|
18876
|
+
} catch (error) {
|
|
18877
|
+
this.#recordFailure(job, error);
|
|
18878
|
+
}
|
|
18879
|
+
}
|
|
18880
|
+
#recordFailure(job, error) {
|
|
18881
|
+
if (error instanceof ArtifactWorkStale || !this.#isCurrent(job)) return;
|
|
18882
|
+
const failureCode = error instanceof ArtifactFailure ? error.failureCode : job.phase === "installing" ? "runtime-failed" : job.phase === "downloading" ? "download-failed" : "integrity";
|
|
18883
|
+
try {
|
|
18884
|
+
this.#write(job, "failed", this.#progress(job), failureCode);
|
|
18885
|
+
} catch {}
|
|
18886
|
+
}
|
|
18887
|
+
/**
|
|
18888
|
+
* Fetches every SRI-pinned archive, extracts only the selected platform's listed files into a staging tree that
|
|
18889
|
+
* the strict installer verifies, then promotes that tree inside the artifact lock after the exact recheck.
|
|
18890
|
+
*/
|
|
18891
|
+
async #installRuntime(job, options) {
|
|
18892
|
+
const selection = job.selection;
|
|
18893
|
+
const runtimeRoot = this.#runtimeRoot(selection);
|
|
18894
|
+
const staging = `${runtimeRoot}.staging`;
|
|
18895
|
+
const files = runtimeFiles(selection);
|
|
18896
|
+
await ownerOnlyDirectory(join(this.#root, "runtime"));
|
|
18897
|
+
const free = await (this.#freeBytes ?? (() => availableBytes(join(this.#root, "runtime"))))();
|
|
18898
|
+
if (!Number.isSafeInteger(free) || free < Math.ceil(bytesOf(files) * 1.1)) throw new ArtifactFailure("runtime-failed");
|
|
18899
|
+
const fetcher = this.#fetcher(job);
|
|
18900
|
+
const packages = [];
|
|
18901
|
+
for (const pkg of selection.runtimeLock.packages) {
|
|
18902
|
+
if (!files.some((file) => file.packageName === pkg.name)) continue;
|
|
18903
|
+
this.#assertCurrent(job);
|
|
18904
|
+
const bytes = await fetcher.fetchRuntimeArchive(pkg, { signal: job.controller.signal });
|
|
18905
|
+
packages.push({
|
|
18906
|
+
packageName: pkg.name,
|
|
18907
|
+
source: (async function* () {
|
|
18908
|
+
yield bytes;
|
|
18909
|
+
})()
|
|
18910
|
+
});
|
|
18911
|
+
}
|
|
18912
|
+
this.#assertCurrent(job);
|
|
18913
|
+
await rm(staging, {
|
|
18914
|
+
recursive: true,
|
|
18915
|
+
force: true
|
|
18916
|
+
});
|
|
18917
|
+
try {
|
|
18918
|
+
const installed = await installRuntime({
|
|
18919
|
+
runtimeDirectory: staging,
|
|
18920
|
+
lockDigest: selection.runtimeLock.digest,
|
|
18921
|
+
packages,
|
|
18922
|
+
files
|
|
18923
|
+
});
|
|
18924
|
+
await this.#locked("runtime-promotion", async () => {
|
|
18925
|
+
this.#assertCurrent(job);
|
|
18926
|
+
if (options.replace) await rm(runtimeRoot, {
|
|
18927
|
+
recursive: true,
|
|
18928
|
+
force: true
|
|
18929
|
+
});
|
|
18930
|
+
await rename(installed.root, runtimeRoot);
|
|
18931
|
+
});
|
|
18932
|
+
} finally {
|
|
18933
|
+
await rm(staging, {
|
|
18934
|
+
recursive: true,
|
|
18935
|
+
force: true
|
|
18936
|
+
});
|
|
18937
|
+
}
|
|
18938
|
+
}
|
|
18939
|
+
async #downloadModel(job, options) {
|
|
18940
|
+
const selection = job.selection;
|
|
18941
|
+
const paths = layaModelArtifactPaths(this.#root, selection.manifest.manifestId);
|
|
18942
|
+
if (options.replace) await this.#mutex.run(async () => {
|
|
18943
|
+
this.#assertCurrent(job);
|
|
18944
|
+
await rm(paths.complete, {
|
|
18945
|
+
recursive: true,
|
|
18946
|
+
force: true
|
|
18947
|
+
});
|
|
18948
|
+
});
|
|
18949
|
+
await new LayaModelDownloader({
|
|
18950
|
+
dataDirectory: this.#root,
|
|
18951
|
+
manifest: selection.manifest,
|
|
18952
|
+
fetcher: this.#fetcher(job),
|
|
18953
|
+
fence: this.#fenceFor(job)
|
|
18954
|
+
}).download({
|
|
18955
|
+
signal: job.controller.signal,
|
|
18956
|
+
...this.#freeBytes === void 0 ? {} : { freeBytes: this.#freeBytes }
|
|
18957
|
+
});
|
|
18958
|
+
}
|
|
18959
|
+
#fetcher(job) {
|
|
18960
|
+
return new ProgressReportingArtifactFetcher({
|
|
18961
|
+
manifest: job.selection.manifest,
|
|
18962
|
+
runtimePackages: job.selection.runtimeLock.packages,
|
|
18963
|
+
resolver: this.#network.resolver,
|
|
18964
|
+
tcpConnect: this.#network.tcpConnect,
|
|
18965
|
+
tlsConnect: this.#network.tlsConnect
|
|
18966
|
+
}, job.modelProgress);
|
|
18967
|
+
}
|
|
18968
|
+
#repairJob(active, selection) {
|
|
18969
|
+
return {
|
|
18970
|
+
epoch: this.#epoch,
|
|
18971
|
+
version: active.version,
|
|
18972
|
+
digest: active.digest,
|
|
18973
|
+
selection,
|
|
18974
|
+
controller: new AbortController(),
|
|
18975
|
+
phase: "installing",
|
|
18976
|
+
runtimeBytes: 0,
|
|
18977
|
+
modelProgress: /* @__PURE__ */ new Map(),
|
|
18978
|
+
settled: Promise.resolve()
|
|
18979
|
+
};
|
|
18980
|
+
}
|
|
18981
|
+
/** One repair per failing tree; the fresh copy must verify or Laya records `integrity` and stays failed. */
|
|
18982
|
+
async #repair(job, failing) {
|
|
18983
|
+
const selection = job.selection;
|
|
18984
|
+
const runtimeRoot = this.#runtimeRoot(selection);
|
|
18985
|
+
const modelRoot = layaModelArtifactPaths(this.#root, selection.manifest.manifestId).complete;
|
|
18986
|
+
try {
|
|
18987
|
+
await this.#locked("repair-delete", async () => {
|
|
18988
|
+
this.#assertCurrent(job);
|
|
18989
|
+
if (failing.runtime) await rm(runtimeRoot, {
|
|
18990
|
+
recursive: true,
|
|
18991
|
+
force: true
|
|
18992
|
+
});
|
|
18993
|
+
if (failing.model) await rm(modelRoot, {
|
|
18994
|
+
recursive: true,
|
|
18995
|
+
force: true
|
|
18996
|
+
});
|
|
18997
|
+
this.#write(job, "failed", 0, "integrity");
|
|
18998
|
+
});
|
|
18999
|
+
if (failing.runtime) await this.#installRuntime(job, { replace: false });
|
|
19000
|
+
job.runtimeBytes = bytesOf(selection.runtimeLock.platform.files);
|
|
19001
|
+
job.phase = "downloading";
|
|
19002
|
+
if (failing.model) await this.#downloadModel(job, { replace: false });
|
|
19003
|
+
job.phase = "verifying";
|
|
19004
|
+
const runtime = await verifyRuntimeTree({
|
|
19005
|
+
root: runtimeRoot,
|
|
19006
|
+
files: runtimeFiles(selection)
|
|
19007
|
+
});
|
|
19008
|
+
const model = await verifyRuntimeTree({
|
|
19009
|
+
root: modelRoot,
|
|
19010
|
+
files: modelFiles(selection)
|
|
19011
|
+
});
|
|
19012
|
+
if (!runtime.ok || !model.ok) throw new ArtifactFailure("integrity");
|
|
19013
|
+
await this.#locked("model-ready", async () => {
|
|
19014
|
+
this.#write(job, "ready", totalBytes(selection));
|
|
19015
|
+
await this.#removeObsoleteTrees(job);
|
|
19016
|
+
});
|
|
19017
|
+
return true;
|
|
19018
|
+
} catch (error) {
|
|
19019
|
+
if (error instanceof ArtifactWorkStale || !this.#isCurrent(job)) return false;
|
|
19020
|
+
try {
|
|
19021
|
+
this.#write(job, "failed", 0, "integrity");
|
|
19022
|
+
} catch {}
|
|
19023
|
+
return false;
|
|
19024
|
+
}
|
|
19025
|
+
}
|
|
19026
|
+
/**
|
|
19027
|
+
* D11 and E2-5: once a selection's model is ready, a tree no enabled entry names is obsolete. Inside the
|
|
19028
|
+
* `model-ready` lock, every `runtime/` and `models/` entry but the selection's own two is deleted, partial and staging
|
|
19029
|
+
* trees included, and the sweep stops at its first recheck that finds the job fenced by a disable, update or stop.
|
|
19030
|
+
*/
|
|
19031
|
+
#removeObsoleteTrees(job) {
|
|
19032
|
+
const named = {
|
|
19033
|
+
runtime: job.selection.runtimeLock.digest,
|
|
19034
|
+
models: job.selection.manifest.manifestId
|
|
19035
|
+
};
|
|
19036
|
+
return this.#sweepTrees((directory, name) => name === named[directory], () => this.#isCurrent(job));
|
|
19037
|
+
}
|
|
19038
|
+
/**
|
|
19039
|
+
* `judge model remove`'s sweep of every verified tree but this release's own, which the removal then deletes with
|
|
19040
|
+
* its unverified copies. It stops at its first recheck that finds an enablement claimed meanwhile.
|
|
19041
|
+
*/
|
|
19042
|
+
#removeEarlierVerifiedTrees() {
|
|
19043
|
+
const own = {
|
|
19044
|
+
runtime: this.#releaseSelection()?.runtimeLock.digest,
|
|
19045
|
+
models: this.#manifestId()
|
|
19046
|
+
};
|
|
19047
|
+
return this.#sweepTrees((directory, name) => name.endsWith(".partial") || name.endsWith(".staging") || name === own[directory], () => this.#activeEnablement() === null);
|
|
19048
|
+
}
|
|
19049
|
+
/**
|
|
19050
|
+
* Deletes, one at a time in raw-byte name order, every entry under `runtime/` and `models/` that `keep` does not
|
|
19051
|
+
* name, rechecking `current` after every await: a tightening or a claim that lands meanwhile stops the sweep before
|
|
19052
|
+
* its next deletion. A deletion that fails is left for the next sweep. Resolves whether the sweep finished. Its
|
|
19053
|
+
* callers hold the artifact lock.
|
|
19054
|
+
*/
|
|
19055
|
+
async #sweepTrees(keep, current) {
|
|
19056
|
+
for (const directory of ["runtime", "models"]) {
|
|
19057
|
+
let names;
|
|
19058
|
+
try {
|
|
19059
|
+
names = await readdir(join(this.#root, directory));
|
|
19060
|
+
} catch {
|
|
19061
|
+
names = [];
|
|
19062
|
+
}
|
|
19063
|
+
if (!current()) return false;
|
|
19064
|
+
names.sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
19065
|
+
for (const name of names) {
|
|
19066
|
+
if (keep(directory, name)) continue;
|
|
19067
|
+
await rm(join(this.#root, directory, name), {
|
|
19068
|
+
recursive: true,
|
|
19069
|
+
force: true
|
|
19070
|
+
}).catch(() => void 0);
|
|
19071
|
+
await this.#failpoint?.("tree-sweep");
|
|
19072
|
+
if (!current()) return false;
|
|
19073
|
+
}
|
|
19074
|
+
}
|
|
19075
|
+
return true;
|
|
19076
|
+
}
|
|
19077
|
+
/** Deletes every partial and staging tree beneath the artifact root, and never a verified one. */
|
|
19078
|
+
async #removeUnverified() {
|
|
19079
|
+
for (const directory of ["runtime", "models"]) {
|
|
19080
|
+
const parent = join(this.#root, directory);
|
|
19081
|
+
let names;
|
|
19082
|
+
try {
|
|
19083
|
+
names = await readdir(parent);
|
|
19084
|
+
} catch {
|
|
19085
|
+
continue;
|
|
19086
|
+
}
|
|
19087
|
+
for (const name of names) if (name.endsWith(".partial") || name.endsWith(".staging")) await rm(join(parent, name), {
|
|
19088
|
+
recursive: true,
|
|
19089
|
+
force: true
|
|
19090
|
+
});
|
|
19091
|
+
}
|
|
19092
|
+
}
|
|
19093
|
+
};
|
|
19094
|
+
function unavailableResult(code) {
|
|
19095
|
+
return {
|
|
19096
|
+
kind: "unavailable",
|
|
19097
|
+
code,
|
|
19098
|
+
phase: "E2"
|
|
19099
|
+
};
|
|
19100
|
+
}
|
|
19101
|
+
//#endregion
|
|
19102
|
+
//#region src/runtime/lifecycle.ts
|
|
19103
|
+
var EventLifecycle = class {
|
|
19104
|
+
store;
|
|
19105
|
+
now;
|
|
19106
|
+
constructor(store, now = Date.now) {
|
|
16208
19107
|
this.store = store;
|
|
16209
19108
|
this.now = now;
|
|
16210
19109
|
}
|
|
@@ -16799,10 +19698,6 @@ function closedReason(result) {
|
|
|
16799
19698
|
if (result.kind === "answer") return "completed";
|
|
16800
19699
|
return result.code.toLowerCase().replaceAll("_", "-");
|
|
16801
19700
|
}
|
|
16802
|
-
/**
|
|
16803
|
-
* The sole agentic execution coordinator. It owns no ledger transition and no socket path: Task 5's budget
|
|
16804
|
-
* functions and Task 6's sealed transport remain the only owners of those effects.
|
|
16805
|
-
*/
|
|
16806
19701
|
var JudgeCoordinator = class {
|
|
16807
19702
|
#store;
|
|
16808
19703
|
#taint;
|
|
@@ -16838,6 +19733,7 @@ var JudgeCoordinator = class {
|
|
|
16838
19733
|
channel: input.rule.source.channel,
|
|
16839
19734
|
classification: envelope.classification
|
|
16840
19735
|
});
|
|
19736
|
+
if (agentic.judge.kind === "laya") return this.#evaluateLaya(input, agentic.judge, envelope);
|
|
16841
19737
|
let hostedApiKey;
|
|
16842
19738
|
try {
|
|
16843
19739
|
hostedApiKey = agentic.judge.kind === "typesafe" ? await this.#hostedApiKeyFor({
|
|
@@ -16974,6 +19870,127 @@ var JudgeCoordinator = class {
|
|
|
16974
19870
|
}
|
|
16975
19871
|
};
|
|
16976
19872
|
}
|
|
19873
|
+
/**
|
|
19874
|
+
* E2's Laya adapter, in E's order: the deterministic prefilter, the minimised envelope and the taint flush have run;
|
|
19875
|
+
* here the database owner's Laya executor checks the enabled entry and template and bounds the request before any
|
|
19876
|
+
* reservation, E reserves the exact request payload, E's gate moves the reservation in flight immediately before the
|
|
19877
|
+
* one frame is written, and only the worker's bounded value and token count come back for E's final settlement.
|
|
19878
|
+
* Every failure is a closed reason and no-match: pre-I/O E releases the reservation, post-I/O E settles it.
|
|
19879
|
+
*/
|
|
19880
|
+
async #evaluateLaya(input, judge, envelope) {
|
|
19881
|
+
const agentic = input.rule.agentic;
|
|
19882
|
+
if (agentic === void 0) return { kind: "not-applicable" };
|
|
19883
|
+
const executor = layaJudgeExecutorFor(this.#store.database);
|
|
19884
|
+
if (executor === void 0) return {
|
|
19885
|
+
kind: "closed",
|
|
19886
|
+
reason: "adapter-reference-blocked",
|
|
19887
|
+
reservationId: null
|
|
19888
|
+
};
|
|
19889
|
+
const laya = executor.prepare({
|
|
19890
|
+
judge,
|
|
19891
|
+
state: envelope.state,
|
|
19892
|
+
instructions: agentic.question
|
|
19893
|
+
});
|
|
19894
|
+
if (!laya.ok) return {
|
|
19895
|
+
kind: "closed",
|
|
19896
|
+
reason: laya.code.toLowerCase(),
|
|
19897
|
+
reservationId: null
|
|
19898
|
+
};
|
|
19899
|
+
let reservation;
|
|
19900
|
+
try {
|
|
19901
|
+
reservation = reserveJudgeWork({
|
|
19902
|
+
store: this.#store,
|
|
19903
|
+
id: this.#newId(),
|
|
19904
|
+
workId: `judge:${input.eventId}:${input.ruleId}:${input.ruleVersion}`,
|
|
19905
|
+
scope: {
|
|
19906
|
+
scope: "source-owned",
|
|
19907
|
+
source: input.rule.source.channel,
|
|
19908
|
+
accountId: input.accountId,
|
|
19909
|
+
eventId: input.eventId,
|
|
19910
|
+
ruleId: input.ruleId,
|
|
19911
|
+
ruleVersion: input.ruleVersion,
|
|
19912
|
+
judgeId: judge.judgeId,
|
|
19913
|
+
judgeVersion: judge.version
|
|
19914
|
+
},
|
|
19915
|
+
judge,
|
|
19916
|
+
limits: {
|
|
19917
|
+
callsPerRollingHour: agentic.callsPerRollingHour,
|
|
19918
|
+
inputTokensPerRolling30Days: agentic.inputTokensPerRolling30Days,
|
|
19919
|
+
concurrency: agentic.concurrency,
|
|
19920
|
+
maxInputTokens: agentic.maxInputTokens
|
|
19921
|
+
},
|
|
19922
|
+
finalEnvelope: laya.request.body,
|
|
19923
|
+
switchGeneration: input.switchGeneration,
|
|
19924
|
+
now: this.#now()
|
|
19925
|
+
});
|
|
19926
|
+
} catch (error) {
|
|
19927
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "budget-refused";
|
|
19928
|
+
if (code === "JUDGE_PAUSED") return {
|
|
19929
|
+
kind: "deferred",
|
|
19930
|
+
reservationId: null
|
|
19931
|
+
};
|
|
19932
|
+
if (code === "JUDGE_WORK_OWNER_HELD") return { kind: "owner-held" };
|
|
19933
|
+
return {
|
|
19934
|
+
kind: "closed",
|
|
19935
|
+
reason: code,
|
|
19936
|
+
reservationId: null
|
|
19937
|
+
};
|
|
19938
|
+
}
|
|
19939
|
+
const ioGate = new JudgeReservationIoGate({
|
|
19940
|
+
store: this.#store,
|
|
19941
|
+
reservationId: reservation.id,
|
|
19942
|
+
judgeKind: judge.kind,
|
|
19943
|
+
now: this.#now
|
|
19944
|
+
});
|
|
19945
|
+
const result = await executor.execute(laya.request, {
|
|
19946
|
+
reservationId: reservation.id,
|
|
19947
|
+
finalGate: () => ioGate.finalGate("write")
|
|
19948
|
+
});
|
|
19949
|
+
if (result.kind === "answer") return {
|
|
19950
|
+
kind: "completed",
|
|
19951
|
+
outcome: uncertainOutcome(input.rule, result.value),
|
|
19952
|
+
value: result.value,
|
|
19953
|
+
valueLabel: "probability",
|
|
19954
|
+
actualInputTokens: result.inputTokens,
|
|
19955
|
+
reservationId: reservation.id,
|
|
19956
|
+
metadata: {
|
|
19957
|
+
judgeId: judge.judgeId,
|
|
19958
|
+
judgeVersion: judge.version,
|
|
19959
|
+
provider: judge.provider,
|
|
19960
|
+
model: judge.model,
|
|
19961
|
+
promptTemplate: judge.promptTemplate,
|
|
19962
|
+
threshold: agentic.threshold,
|
|
19963
|
+
value: result.value,
|
|
19964
|
+
valueLabel: "probability"
|
|
19965
|
+
}
|
|
19966
|
+
};
|
|
19967
|
+
const disposition = finalizeJudgeTransportRefusal({
|
|
19968
|
+
store: this.#store,
|
|
19969
|
+
reservationId: reservation.id,
|
|
19970
|
+
result: result.code === "laya-timeout" ? "timeout" : "unknown",
|
|
19971
|
+
now: this.#now()
|
|
19972
|
+
});
|
|
19973
|
+
if (ioGate.firstGateError !== void 0) {
|
|
19974
|
+
if (ioGate.firstGateError instanceof JudgeBudgetError && ioGate.firstGateError.code === "JUDGE_PAUSED") return {
|
|
19975
|
+
kind: "deferred",
|
|
19976
|
+
reservationId: reservation.id
|
|
19977
|
+
};
|
|
19978
|
+
return {
|
|
19979
|
+
kind: "closed",
|
|
19980
|
+
reason: ioGate.firstGateError instanceof Error && "code" in ioGate.firstGateError ? String(ioGate.firstGateError.code) : "authority-changed",
|
|
19981
|
+
reservationId: reservation.id
|
|
19982
|
+
};
|
|
19983
|
+
}
|
|
19984
|
+
if (disposition === "released" && result.code === "laya-stopped") return {
|
|
19985
|
+
kind: "deferred",
|
|
19986
|
+
reservationId: reservation.id
|
|
19987
|
+
};
|
|
19988
|
+
return {
|
|
19989
|
+
kind: "closed",
|
|
19990
|
+
reason: result.code.toLowerCase(),
|
|
19991
|
+
reservationId: reservation.id
|
|
19992
|
+
};
|
|
19993
|
+
}
|
|
16977
19994
|
};
|
|
16978
19995
|
//#endregion
|
|
16979
19996
|
//#region src/runtime/evaluate.ts
|
|
@@ -20167,7 +23184,8 @@ function createOwnerB3ControlService(options) {
|
|
|
20167
23184
|
now,
|
|
20168
23185
|
...judgeActivations === void 0 ? {} : { activations: judgeActivations },
|
|
20169
23186
|
...options.judgeTest === void 0 ? {} : { judgeTest: { test: options.judgeTest.test.bind(options.judgeTest) } },
|
|
20170
|
-
...options.holdResolver === void 0 ? {} : { holdResolver: options.holdResolver }
|
|
23187
|
+
...options.holdResolver === void 0 ? {} : { holdResolver: options.holdResolver },
|
|
23188
|
+
...options.laya === void 0 ? {} : { laya: options.laya }
|
|
20171
23189
|
});
|
|
20172
23190
|
const target = (pendingOrigin) => new B3TargetTestControl({
|
|
20173
23191
|
store: options.store,
|
|
@@ -20228,7 +23246,7 @@ function createOwnerB3ControlService(options) {
|
|
|
20228
23246
|
}
|
|
20229
23247
|
}
|
|
20230
23248
|
switch (request.operation) {
|
|
20231
|
-
case "approvalView": return approvalViewFromOwner(options.store, options.activations, now, request.args);
|
|
23249
|
+
case "approvalView": return approvalViewFromOwner(options.store, options.activations, now, request.args, options.laya !== void 0);
|
|
20232
23250
|
case "ruleTest": return runCatalogueRuleTest({
|
|
20233
23251
|
document: request.args.document,
|
|
20234
23252
|
catalogue: request.args.catalogue
|
|
@@ -20396,16 +23414,21 @@ function terminalSecretResult(receipt) {
|
|
|
20396
23414
|
terminalReveal: reveal
|
|
20397
23415
|
};
|
|
20398
23416
|
}
|
|
20399
|
-
|
|
23417
|
+
/**
|
|
23418
|
+
* The authoritative approval view. With E2's artifact manager composed the owner answers B3 control v3, whose view of
|
|
23419
|
+
* a rule naming a Laya judge also carries D11's Laya limits, from the same reviewed assets as the enablement.
|
|
23420
|
+
*/
|
|
23421
|
+
async function approvalViewFromOwner(store, activations, now, args, layaJudges = false) {
|
|
20400
23422
|
const approvalId = requiredText(args.approvalId, "approval id");
|
|
20401
23423
|
const digest = requiredText(args.digest, "approval digest");
|
|
20402
23424
|
const row = store.database.prepare("SELECT id, document, digest, status FROM activation_intents WHERE approval_id = ?").get(approvalId);
|
|
20403
23425
|
if (row === void 0 || row.status !== "pending") return { kind: "STALE_APPROVAL" };
|
|
20404
23426
|
if (row.digest !== digest) return { kind: "DIGEST_MISMATCH" };
|
|
23427
|
+
const preview = normaliseActivationDocument(JSON.parse(row.document));
|
|
23428
|
+
const layaJudge = layaJudges ? layaJudgePreviewFor(preview, layaJudgeTermsFor(layaEnablementInputsFor(store.database))) : void 0;
|
|
20405
23429
|
const nextExpiry = now() + 3e5;
|
|
20406
23430
|
const challenge = await activations.issueApprovalViewChallenge(approvalId, nextExpiry);
|
|
20407
23431
|
if (challenge === null) return { kind: "CHALLENGE_EXPIRED" };
|
|
20408
|
-
const preview = normaliseActivationDocument(JSON.parse(row.document));
|
|
20409
23432
|
return {
|
|
20410
23433
|
kind: "READY",
|
|
20411
23434
|
approvalId,
|
|
@@ -20423,7 +23446,8 @@ async function approvalViewFromOwner(store, activations, now, args) {
|
|
|
20423
23446
|
digest: row.digest,
|
|
20424
23447
|
challenge,
|
|
20425
23448
|
expiresAt: nextExpiry
|
|
20426
|
-
}
|
|
23449
|
+
},
|
|
23450
|
+
...layaJudge === void 0 ? {} : { layaJudge }
|
|
20427
23451
|
};
|
|
20428
23452
|
}
|
|
20429
23453
|
/**
|
|
@@ -20771,6 +23795,34 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
20771
23795
|
}
|
|
20772
23796
|
});
|
|
20773
23797
|
};
|
|
23798
|
+
const layaInputs = {
|
|
23799
|
+
...options.laya?.assets === void 0 ? {} : { layaAssetSource: options.laya.assets },
|
|
23800
|
+
...options.laya?.platform === void 0 ? {} : { layaPlatformProbe: options.laya.platform }
|
|
23801
|
+
};
|
|
23802
|
+
const layaArtifacts = new LayaArtifactManager({
|
|
23803
|
+
store: database,
|
|
23804
|
+
artifactRoot: join(options.dataDir ?? core.paths.dataDir, "events", JUDGE_KIND_LAYA),
|
|
23805
|
+
inputs: layaInputs,
|
|
23806
|
+
network: options.laya?.network,
|
|
23807
|
+
freeBytes: options.laya?.freeBytes,
|
|
23808
|
+
failpoint: options.laya?.failpoint,
|
|
23809
|
+
audit: core.audit,
|
|
23810
|
+
now
|
|
23811
|
+
});
|
|
23812
|
+
const unregisterLayaPurge = registerJudgeWorkPurgeListener(database.database, layaArtifacts);
|
|
23813
|
+
const unregisterLayaInputs = registerLayaEnablementInputs(database.database, layaInputs);
|
|
23814
|
+
const layaWorker = new LayaWorkerSupervisor({
|
|
23815
|
+
artifacts: layaArtifacts,
|
|
23816
|
+
startWorker: options.laya?.startWorker
|
|
23817
|
+
});
|
|
23818
|
+
const unregisterLayaWorker = registerLayaJudgeExecutor(database.database, layaWorker);
|
|
23819
|
+
onDatabase({ close: () => {
|
|
23820
|
+
unregisterLayaWorker();
|
|
23821
|
+
unregisterLayaInputs();
|
|
23822
|
+
unregisterLayaPurge();
|
|
23823
|
+
layaWorker.close();
|
|
23824
|
+
database.close();
|
|
23825
|
+
} });
|
|
20774
23826
|
const activations = new ActivationRuntime({
|
|
20775
23827
|
store: database,
|
|
20776
23828
|
approvals: core.approvals,
|
|
@@ -20909,7 +23961,11 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
20909
23961
|
retainedContentHooks: whatsappComposition.retainedContentHooks,
|
|
20910
23962
|
now,
|
|
20911
23963
|
...options.newIntentId === void 0 ? {} : { newIntentId: options.newIntentId },
|
|
20912
|
-
assertObjectSecretReady: b3Ports.assertObjectSecretReady
|
|
23964
|
+
assertObjectSecretReady: b3Ports.assertObjectSecretReady,
|
|
23965
|
+
...layaInputs,
|
|
23966
|
+
onJudgeKindEnabled: ({ kind }) => {
|
|
23967
|
+
if (kind === "laya") layaArtifacts.ensureInstalled();
|
|
23968
|
+
}
|
|
20913
23969
|
});
|
|
20914
23970
|
const scheduler = new EventScheduler({
|
|
20915
23971
|
store: database,
|
|
@@ -21063,10 +24119,11 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
21063
24119
|
}),
|
|
21064
24120
|
whatsappVisibilityFence: whatsappComposition.visibilityFence
|
|
21065
24121
|
}),
|
|
24122
|
+
laya: layaArtifacts,
|
|
21066
24123
|
now
|
|
21067
24124
|
});
|
|
21068
24125
|
const doctorReport = async () => {
|
|
21069
|
-
return
|
|
24126
|
+
return projectE2Doctor({
|
|
21070
24127
|
database: database.database,
|
|
21071
24128
|
installationId: database.installationId,
|
|
21072
24129
|
lifecycle: lifecycle.status(),
|
|
@@ -21075,6 +24132,7 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
21075
24132
|
config: core.config,
|
|
21076
24133
|
approvals: core.approvals,
|
|
21077
24134
|
decryptSourceState: decryptSourceStage,
|
|
24135
|
+
laya: layaArtifacts,
|
|
21078
24136
|
now
|
|
21079
24137
|
});
|
|
21080
24138
|
};
|
|
@@ -21102,7 +24160,12 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
21102
24160
|
try {
|
|
21103
24161
|
await scheduler.stop();
|
|
21104
24162
|
await control?.close();
|
|
24163
|
+
await layaWorker.close();
|
|
24164
|
+
await layaArtifacts.stop();
|
|
21105
24165
|
} finally {
|
|
24166
|
+
unregisterLayaWorker();
|
|
24167
|
+
unregisterLayaInputs();
|
|
24168
|
+
unregisterLayaPurge();
|
|
21106
24169
|
database.close();
|
|
21107
24170
|
await removeInstance(paths, instance.instanceId);
|
|
21108
24171
|
await rm(paths.controlToken, { force: true });
|
|
@@ -21119,7 +24182,13 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
|
|
|
21119
24182
|
database: database.database,
|
|
21120
24183
|
now: now()
|
|
21121
24184
|
}));
|
|
24185
|
+
await withEventWriterLease(writerQuiescer, () => layaArtifacts.reconcileRelease(() => new JudgeKinds({
|
|
24186
|
+
store: database,
|
|
24187
|
+
now,
|
|
24188
|
+
activations
|
|
24189
|
+
}).disable(JUDGE_KIND_LAYA)));
|
|
21122
24190
|
await withEventWriterLease(writerQuiescer, () => recoverActivations(activations));
|
|
24191
|
+
layaArtifacts.resume().catch(() => void 0);
|
|
21123
24192
|
await writeToken(paths, token);
|
|
21124
24193
|
control = await (options.startControlServer ?? startControlServer)({
|
|
21125
24194
|
endpoint: instance.endpoint,
|
|
@@ -21204,10 +24273,10 @@ async function handleControl(owner, lifecycle, database, activations, dispatcher
|
|
|
21204
24273
|
case OWNER_CONTROL_WIRES.ruleShow: return ruleShow(database, requiredText(request.args.ruleId, "rule id"));
|
|
21205
24274
|
case OWNER_CONTROL_WIRES.createRule:
|
|
21206
24275
|
case OWNER_CONTROL_WIRES.updateRule: return createRule(database, request.args.document);
|
|
21207
|
-
case OWNER_CONTROL_WIRES.enableRule: return activations.prepareRule({
|
|
24276
|
+
case OWNER_CONTROL_WIRES.enableRule: return domainAwaited(() => activations.prepareRule({
|
|
21208
24277
|
ruleId: requiredText(request.args.ruleId, "rule id"),
|
|
21209
24278
|
version: requiredVersion(request.args.version, "rule version")
|
|
21210
|
-
});
|
|
24279
|
+
}));
|
|
21211
24280
|
case OWNER_CONTROL_WIRES.disableRule:
|
|
21212
24281
|
case OWNER_CONTROL_WIRES.removeRule: return disableRule(database, activations, requiredText(request.args.ruleId, "rule id"));
|
|
21213
24282
|
case OWNER_CONTROL_WIRES.targetsList: return targetRows(database);
|
|
@@ -21252,6 +24321,15 @@ function domain(work) {
|
|
|
21252
24321
|
throw error;
|
|
21253
24322
|
}
|
|
21254
24323
|
}
|
|
24324
|
+
/** `domain` for an operation that awaits: a refusal is the caller's input, with the same fixed reason. */
|
|
24325
|
+
async function domainAwaited(work) {
|
|
24326
|
+
try {
|
|
24327
|
+
return await work();
|
|
24328
|
+
} catch (error) {
|
|
24329
|
+
if (error instanceof EventDomainError) throw new CommsError("BAD_DATA", error.message, { details: { reason: error.code } });
|
|
24330
|
+
throw error;
|
|
24331
|
+
}
|
|
24332
|
+
}
|
|
21255
24333
|
function ruleRows(database) {
|
|
21256
24334
|
return database.database.prepare("SELECT rule_id, version, digest, state, approval_id, authorization_activation_id FROM rule_versions ORDER BY rule_id, version").all();
|
|
21257
24335
|
}
|
|
@@ -21263,8 +24341,9 @@ function ruleShow(database, ruleId) {
|
|
|
21263
24341
|
document: JSON.parse(row.document)
|
|
21264
24342
|
}));
|
|
21265
24343
|
}
|
|
24344
|
+
/** The owner composes E2's artifact manager, so the rules it keeps carry B3 control v3's judge documents. */
|
|
21266
24345
|
function createRule(database, document) {
|
|
21267
|
-
return domain(() =>
|
|
24346
|
+
return domain(() => ImmutableVersions.forJudgeContract(database.database, "b3-control-v3").createRule(document));
|
|
21268
24347
|
}
|
|
21269
24348
|
function targetRows(database) {
|
|
21270
24349
|
return database.database.prepare("SELECT target_id, version, digest, revoked_at FROM target_versions ORDER BY target_id, version").all();
|
|
@@ -21823,7 +24902,7 @@ async function run(argv, deps = {}) {
|
|
|
21823
24902
|
const parsed = jsonArgument(document, "budget document");
|
|
21824
24903
|
print(await humanB3("updateBudget", { document: parsed }, () => updateBudget(parsed, controlOptions(program))));
|
|
21825
24904
|
});
|
|
21826
|
-
judge.command("kinds").description("list disabled
|
|
24905
|
+
judge.command("kinds").description("list each judge kind as disabled or enabled, or Laya as unavailable here with its reason").command("list").action(async () => {
|
|
21827
24906
|
ran = true;
|
|
21828
24907
|
print(await judgeKindsList(controlOptions(program)));
|
|
21829
24908
|
});
|
|
@@ -21836,7 +24915,7 @@ async function run(argv, deps = {}) {
|
|
|
21836
24915
|
ran = true;
|
|
21837
24916
|
print(await humanB3("disableJudgeKind", { kind }, () => disableJudgeKind(kind, controlOptions(program))));
|
|
21838
24917
|
});
|
|
21839
|
-
const judgeModel = judge.command("model").description("local Laya model
|
|
24918
|
+
const judgeModel = judge.command("model").description("report the local Laya runtime and model state, or remove its files while Laya is disabled");
|
|
21840
24919
|
judgeModel.command("status").action(async () => {
|
|
21841
24920
|
ran = true;
|
|
21842
24921
|
print(await judgeModelStatus(controlOptions(program)));
|