@evomap/evolver-proxy 2.0.0-beta.19 → 2.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/evolver-proxy.d.ts +12 -0
- package/dist/bin/evolver-proxy.js +257 -56
- package/dist/daemon/proxyDaemon.d.ts +12 -0
- package/dist/daemon/proxyDaemon.js +313 -18
- package/dist/daemon/systemdNotifier.d.ts +2 -0
- package/dist/daemon/systemdNotifier.js +11 -1
- package/dist/llm/upstream.js +54 -7
- package/dist/private/accountAssetCompatibility.d.ts +1 -0
- package/dist/private/accountAssetCompatibility.js +3 -3
- package/dist/private/adapterLoader.js +4 -3
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +18 -7
- package/dist/selfUpdate/executor.js +159 -59
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/index.d.ts +2 -1
- package/dist/selfUpdate/index.js +2 -1
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +3 -0
- package/dist/selfUpdate/releaseBinary.js +50 -4
- package/dist/selfUpdate/transaction.d.ts +8 -0
- package/dist/selfUpdate/transaction.js +166 -18
- package/dist/selfUpdate/unixController.d.ts +8 -0
- package/dist/selfUpdate/unixController.js +366 -38
- package/dist/selfUpdate/windowsController.d.ts +14 -2
- package/dist/selfUpdate/windowsController.js +484 -103
- package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
- package/dist/selfUpdate/windowsUpdater.js +174 -7
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import { mailbox, hub as hubNs, shadow as shadow_, assetstore, wire, util } from '@evomap/evolver-core';
|
|
3
|
+
import { mailbox, hub as hubNs, shadow as shadow_, assetstore, wire, util, verify } from '@evomap/evolver-core';
|
|
4
4
|
import { AuthError, HubClientError, HubUnreachableError } from '@evomap/evolver-adapter-public';
|
|
5
5
|
import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
|
|
6
6
|
import { LifecycleManager } from '../lifecycle/manager.js';
|
|
@@ -10,6 +10,7 @@ import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
|
|
|
10
10
|
import { hubAuthFailureHint } from './selectHub.js';
|
|
11
11
|
import { CollaborationFacade } from './collaborationFacade.js';
|
|
12
12
|
import { PublishRecallVerifier, resolvePublishRecallConfig, } from './publishRecallVerifier.js';
|
|
13
|
+
const DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS = 30_000;
|
|
13
14
|
export const DEFAULT_IPC_PORT = 19820;
|
|
14
15
|
// V1 local-proxy compatibility contract; independent of the V2 mailbox envelope schema.
|
|
15
16
|
const PROXY_PROTOCOL_VERSION = '0.1.0';
|
|
@@ -35,6 +36,27 @@ const SYNC_ASSET_SUBMIT_TYPE_RANK = {
|
|
|
35
36
|
const SYNC_ASSET_SUBMIT_SCOPE_STATE_KEY = 'sync_asset_submit:idempotency_scope:v1';
|
|
36
37
|
const SYNC_ASSET_SUBMIT_DIRECT_RETRY_GRACE_MS = 30_000;
|
|
37
38
|
const DEFAULT_SYNC_ASSET_SUBMIT_RESPONSE_TIMEOUT_MS = 15_000;
|
|
39
|
+
function withSynchronousPublishStatus(value, publishStatus) {
|
|
40
|
+
const body = isRecordValue(value) ? { ...value } : { receipt: value };
|
|
41
|
+
return { ...body, publish_status: publishStatus, queued: false };
|
|
42
|
+
}
|
|
43
|
+
function legacySynchronousResultStatus(result) {
|
|
44
|
+
if (result.ok)
|
|
45
|
+
return 'accepted';
|
|
46
|
+
// 202 表示兼容层已持久化但尚未完成;校验或终态/传输失败即使在 mailbox
|
|
47
|
+
// 中保留可重试意图,聚合仍归类为 failed。
|
|
48
|
+
return result.statusCode === 202 || result.error === 'publish_pending' ? 'pending' : 'failed';
|
|
49
|
+
}
|
|
50
|
+
function summarizeLegacySynchronousResults(results) {
|
|
51
|
+
const statuses = new Set(results.map(legacySynchronousResultStatus));
|
|
52
|
+
// publish_status 表示聚合终态,而 queued 独立表示是否仍有 durable mailbox 工作。
|
|
53
|
+
// 因此 pending+failed 必须是 failed/true:不能掩盖失败,也不能谎报没有排队。
|
|
54
|
+
if (statuses.has('failed') || statuses.size === 0)
|
|
55
|
+
return { publishStatus: 'failed', queued: statuses.has('pending') };
|
|
56
|
+
if (statuses.has('pending'))
|
|
57
|
+
return { publishStatus: 'pending', queued: true };
|
|
58
|
+
return { publishStatus: 'accepted', queued: false };
|
|
59
|
+
}
|
|
38
60
|
class OutboundHubModeMismatchError extends Error {
|
|
39
61
|
retryable = true;
|
|
40
62
|
retryAfterMs = 1_000;
|
|
@@ -64,6 +86,8 @@ export class ProxyDaemon {
|
|
|
64
86
|
collaborationFacade;
|
|
65
87
|
publishRecallVerifier;
|
|
66
88
|
proxyHandler;
|
|
89
|
+
hub;
|
|
90
|
+
recipeComposeStarted = new Set();
|
|
67
91
|
ipc;
|
|
68
92
|
now;
|
|
69
93
|
random;
|
|
@@ -98,6 +122,8 @@ export class ProxyDaemon {
|
|
|
98
122
|
scheduledForceUpdateKey;
|
|
99
123
|
traceBackfillDraining = false;
|
|
100
124
|
loopWakeHandler;
|
|
125
|
+
/** 守护进程停止时取消宿主验证。 */
|
|
126
|
+
publishAbortController = new AbortController();
|
|
101
127
|
constructor(deps) {
|
|
102
128
|
this.deps = deps;
|
|
103
129
|
this.now = deps.now ?? (() => Date.now());
|
|
@@ -124,6 +150,7 @@ export class ProxyDaemon {
|
|
|
124
150
|
this.assetStore = deps.assetStore ?? (assetStoreDir ? new assetstore.LocalJsonlProvider(assetStoreDir) : undefined);
|
|
125
151
|
this.atp = deps.atp;
|
|
126
152
|
const hubToUse = shadow ? shadow_.shadowHubCapability(deps.hub, deps.shadowSink, 'shadow') : deps.hub;
|
|
153
|
+
this.hub = hubToUse;
|
|
127
154
|
const hubBindings = hubNs.makeHubBindings(hubToUse, deps.publishSanitizeEnv
|
|
128
155
|
? { sanitize: { env: deps.publishSanitizeEnv } }
|
|
129
156
|
: {});
|
|
@@ -195,6 +222,8 @@ export class ProxyDaemon {
|
|
|
195
222
|
}
|
|
196
223
|
catch { /* best-effort */ }
|
|
197
224
|
}
|
|
225
|
+
// Recipe is the preferred public artifact. Failure here must not retry the already-accepted asset publish.
|
|
226
|
+
this.composeRecipeAfterAcceptedSubmit(envelope);
|
|
198
227
|
},
|
|
199
228
|
onOutboundTerminal: (envelope, error) => {
|
|
200
229
|
this.collaborationFacade.handleOutboundTerminal(envelope, error);
|
|
@@ -260,6 +289,8 @@ export class ProxyDaemon {
|
|
|
260
289
|
async start() {
|
|
261
290
|
if (this.started)
|
|
262
291
|
throw new Error('ProxyDaemon 已启动');
|
|
292
|
+
if (this.publishAbortController.signal.aborted)
|
|
293
|
+
this.publishAbortController = new AbortController();
|
|
263
294
|
try {
|
|
264
295
|
this.daemon.start();
|
|
265
296
|
this.ipc = new mailbox.MailboxIpcServer({
|
|
@@ -507,6 +538,9 @@ export class ProxyDaemon {
|
|
|
507
538
|
async stop() {
|
|
508
539
|
this.started = false;
|
|
509
540
|
this.lifecycleArmed = false;
|
|
541
|
+
if (!this.publishAbortController.signal.aborted) {
|
|
542
|
+
this.publishAbortController.abort(new Error('proxy_daemon_stopped'));
|
|
543
|
+
}
|
|
510
544
|
this.nextTickDueAt = undefined;
|
|
511
545
|
if (this.forceUpdateTimer) {
|
|
512
546
|
clearTimeout(this.forceUpdateTimer);
|
|
@@ -631,6 +665,7 @@ export class ProxyDaemon {
|
|
|
631
665
|
return { ok: false, reason: 'self_update_not_configured' };
|
|
632
666
|
const currentVersion = this.deps.selfUpdate.currentVersion ?? this.deps.evolverVersion ?? '0.0.0';
|
|
633
667
|
const originalTelemetry = this.deps.selfUpdate.onTelemetry;
|
|
668
|
+
const originalCleanupWarning = this.deps.selfUpdate.onCleanupWarning;
|
|
634
669
|
const selfUpdateDeps = {
|
|
635
670
|
...this.deps.selfUpdate,
|
|
636
671
|
currentVersion,
|
|
@@ -641,6 +676,15 @@ export class ProxyDaemon {
|
|
|
641
676
|
});
|
|
642
677
|
originalTelemetry?.(result);
|
|
643
678
|
},
|
|
679
|
+
onCleanupWarning: (warning, result) => {
|
|
680
|
+
try {
|
|
681
|
+
this.store.setState('self_update:last_cleanup_warning', safeDaemonMessage(`self_update_cleanup_warning:${warning}`, MAX_PROXY_TICK_ERROR_LENGTH));
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// The returned result still carries cleanupWarning when operator state is unavailable.
|
|
685
|
+
}
|
|
686
|
+
originalCleanupWarning?.(warning, result);
|
|
687
|
+
},
|
|
644
688
|
};
|
|
645
689
|
return executeForceUpdate(directive, selfUpdateDeps);
|
|
646
690
|
}
|
|
@@ -799,6 +843,65 @@ export class ProxyDaemon {
|
|
|
799
843
|
ctx.json(200, { results, assets: results, query: body });
|
|
800
844
|
return true;
|
|
801
845
|
}
|
|
846
|
+
if (ctx.route === 'POST /recipe/search') {
|
|
847
|
+
const body = (await ctx.readJson());
|
|
848
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
849
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
const recipes = this.hub.recipes;
|
|
853
|
+
if (!recipes) {
|
|
854
|
+
ctx.json(501, { error: 'recipe_unsupported' });
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
const limit = boundedRequestLimit(body.limit, 10, 50);
|
|
858
|
+
if (limit === undefined) {
|
|
859
|
+
ctx.json(400, { error: 'invalid_limit' });
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
const q = [body.q, body.query, body.text].find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
863
|
+
const cursor = typeof body.cursor === 'string' && body.cursor.trim() ? body.cursor.trim() : undefined;
|
|
864
|
+
const sort = typeof body.sort === 'string' && body.sort.trim() ? body.sort.trim() : undefined;
|
|
865
|
+
const request = {
|
|
866
|
+
...(q ? { q } : {}),
|
|
867
|
+
limit,
|
|
868
|
+
...(cursor ? { cursor } : {}),
|
|
869
|
+
...(sort ? { sort } : {}),
|
|
870
|
+
};
|
|
871
|
+
const receipt = q ? await recipes.search(request) : await recipes.list(request);
|
|
872
|
+
ctx.json(200, {
|
|
873
|
+
recipes: receipt.recipes,
|
|
874
|
+
...(receipt.nextCursor ? { nextCursor: receipt.nextCursor } : {}),
|
|
875
|
+
...(receipt.hasMore !== undefined ? { hasMore: receipt.hasMore } : {}),
|
|
876
|
+
query: body,
|
|
877
|
+
});
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
if (ctx.route === 'POST /recipe/express') {
|
|
881
|
+
const body = asRecord(await ctx.readJson());
|
|
882
|
+
if (hubModeMismatch(body['expected_hub_mode'], this.deps.hubMode)) {
|
|
883
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
const recipes = this.hub.recipes;
|
|
887
|
+
if (!recipes) {
|
|
888
|
+
ctx.json(501, { error: 'recipe_unsupported' });
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
const recipeId = typeof body['recipe_id'] === 'string'
|
|
892
|
+
? body['recipe_id']
|
|
893
|
+
: typeof body['recipeId'] === 'string'
|
|
894
|
+
? body['recipeId']
|
|
895
|
+
: '';
|
|
896
|
+
if (!recipeId.trim()) {
|
|
897
|
+
ctx.json(400, { error: 'recipe_id_required' });
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const inputPayload = asRecord(body['input_payload']) ?? asRecord(body['inputPayload']) ?? {};
|
|
901
|
+
const receipt = await recipes.express(recipeId.trim(), { inputPayload });
|
|
902
|
+
ctx.json(200, receipt);
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
802
905
|
if (ctx.route === 'POST /asset/fetch') {
|
|
803
906
|
const body = (await ctx.readJson());
|
|
804
907
|
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
@@ -871,11 +974,12 @@ export class ProxyDaemon {
|
|
|
871
974
|
ctx.json(400, { error: 'mode=sync requires a full asset bundle' });
|
|
872
975
|
return true;
|
|
873
976
|
}
|
|
874
|
-
await this.publishAssetSubmitSynchronously(ctx, outboundBundle);
|
|
977
|
+
await this.publishAssetSubmitSynchronously(ctx, outboundBundle, hubNs.recipeComposeRequested(body));
|
|
875
978
|
return true;
|
|
876
979
|
}
|
|
877
980
|
const payload = {
|
|
878
981
|
assets: outboundBundle,
|
|
982
|
+
compose_recipe: hubNs.recipeComposeRequested(body),
|
|
879
983
|
[OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
|
|
880
984
|
};
|
|
881
985
|
const requestId = typeof body['request_id'] === 'string' && ASYNC_ASSET_SUBMIT_REQUEST_ID.test(body['request_id'])
|
|
@@ -954,23 +1058,75 @@ export class ProxyDaemon {
|
|
|
954
1058
|
}
|
|
955
1059
|
if (ctx.route === 'POST /conversation/distill') {
|
|
956
1060
|
const body = (await ctx.readJson());
|
|
1061
|
+
const publishRequested = body['publish'] === true;
|
|
1062
|
+
const abortPublish = () => {
|
|
1063
|
+
if (ctx.signal?.aborted)
|
|
1064
|
+
return true;
|
|
1065
|
+
if (!this.publishAbortController.signal.aborted)
|
|
1066
|
+
return false;
|
|
1067
|
+
// 守护进程停止时仍需结束开放的响应,否则 IPC server.close() 会等待该连接而无法完成停机。
|
|
1068
|
+
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
1069
|
+
// 'blocked' (not 'failed'): the request was never queued, it was intercepted by
|
|
1070
|
+
// shutdown; the 503 + error field already explains why.
|
|
1071
|
+
ctx.json(503, { error: 'proxy_shutting_down', publish_status: 'blocked', queued: false });
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
1074
|
+
};
|
|
957
1075
|
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
958
1076
|
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
959
1077
|
return true;
|
|
960
1078
|
}
|
|
961
|
-
|
|
1079
|
+
// 请求已取消时禁止继续生成或持久化发布草稿,避免把取消前的陈旧状态留下。
|
|
1080
|
+
if (publishRequested && abortPublish())
|
|
1081
|
+
return true;
|
|
1082
|
+
const verifiedExecution = publishRequested
|
|
1083
|
+
? await resolveVerifiedExecutionAfterPreflight(this.deps.publishExecutionVerifier, body, this.deps.publishExecutionVerifierTimeoutMs, [this.publishAbortController.signal, ...(ctx.signal ? [ctx.signal] : [])])
|
|
1084
|
+
: undefined;
|
|
1085
|
+
if (publishRequested && abortPublish())
|
|
1086
|
+
return true;
|
|
1087
|
+
const distill = await hubNs.distillConversation(body, {
|
|
1088
|
+
// 调用方明确要求持久化时,即使发布被证据或质量闸门拦截,也保留可审查草稿;永不因此进入队列。
|
|
1089
|
+
persist: body.persist === true,
|
|
1090
|
+
store: this.assetStore,
|
|
1091
|
+
...(verifiedExecution ? { verifiedExecution } : {}),
|
|
1092
|
+
});
|
|
1093
|
+
if (publishRequested && abortPublish())
|
|
1094
|
+
return true;
|
|
962
1095
|
if (!distill.ok) {
|
|
963
|
-
ctx.json(200, {
|
|
1096
|
+
ctx.json(200, {
|
|
1097
|
+
...distill,
|
|
1098
|
+
queued: false,
|
|
1099
|
+
submission: null,
|
|
1100
|
+
publish_status: publishRequested ? 'blocked' : 'not_requested',
|
|
1101
|
+
});
|
|
964
1102
|
return true;
|
|
965
1103
|
}
|
|
966
1104
|
let submission = null;
|
|
967
|
-
if (
|
|
1105
|
+
if (publishRequested) {
|
|
1106
|
+
if (abortPublish())
|
|
1107
|
+
return true;
|
|
1108
|
+
// 未达到可复用质量阈值的蒸馏结果仍可作为有用草稿,
|
|
1109
|
+
// 但不得进入出站发布队列。安全与内容完整性闸门保持严格,质量负责晋级。
|
|
1110
|
+
if (distill.publishable !== true) {
|
|
1111
|
+
const reason = distill.quality.ok && verifiedExecution === undefined ? 'execution_evidence' : 'quality_gate';
|
|
1112
|
+
ctx.json(200, {
|
|
1113
|
+
...distill,
|
|
1114
|
+
queued: false,
|
|
1115
|
+
submission: null,
|
|
1116
|
+
publish_blocked: reason,
|
|
1117
|
+
publish_status: 'blocked',
|
|
1118
|
+
});
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
968
1121
|
const env = mailbox.createEnvelope({
|
|
969
1122
|
type: 'asset_submit',
|
|
970
1123
|
payload: {
|
|
971
1124
|
source: 'conversation_distillation',
|
|
972
1125
|
distill_id: distill.distill_id,
|
|
973
1126
|
assets: [distill.gene, distill.capsule],
|
|
1127
|
+
compose_recipe: body['publish_recipe'] !== false,
|
|
1128
|
+
title: typeof body.title === 'string' ? body.title : undefined,
|
|
1129
|
+
description: typeof body.summary === 'string' ? body.summary : undefined,
|
|
974
1130
|
[OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
|
|
975
1131
|
},
|
|
976
1132
|
runtimeNamespace: this.deps.runtimeNamespace ?? 'default',
|
|
@@ -981,7 +1137,12 @@ export class ProxyDaemon {
|
|
|
981
1137
|
this.notifyNewOutbound();
|
|
982
1138
|
submission = { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored };
|
|
983
1139
|
}
|
|
984
|
-
ctx.json(200, {
|
|
1140
|
+
ctx.json(200, {
|
|
1141
|
+
...distill,
|
|
1142
|
+
queued: submission !== null,
|
|
1143
|
+
submission,
|
|
1144
|
+
publish_status: publishRequested ? 'queued' : 'not_requested',
|
|
1145
|
+
});
|
|
985
1146
|
return true;
|
|
986
1147
|
}
|
|
987
1148
|
if (ctx.route === 'POST /agent/search') {
|
|
@@ -1128,25 +1289,39 @@ export class ProxyDaemon {
|
|
|
1128
1289
|
staleUntil: now + this.assetSearchCacheTtlMs + this.assetSearchStaleGraceMs,
|
|
1129
1290
|
});
|
|
1130
1291
|
}
|
|
1131
|
-
async publishAssetSubmitSynchronously(ctx, items) {
|
|
1292
|
+
async publishAssetSubmitSynchronously(ctx, items, composeRecipe = true) {
|
|
1293
|
+
const abortSynchronousPublish = () => {
|
|
1294
|
+
if (ctx.signal?.aborted)
|
|
1295
|
+
return true;
|
|
1296
|
+
if (!this.publishAbortController.signal.aborted)
|
|
1297
|
+
return false;
|
|
1298
|
+
if (!ctx.res.destroyed && !ctx.res.writableEnded) {
|
|
1299
|
+
ctx.json(503, { error: 'proxy_shutting_down', publish_status: 'failed', queued: false });
|
|
1300
|
+
}
|
|
1301
|
+
return true;
|
|
1302
|
+
};
|
|
1132
1303
|
const classified = classifySynchronousAssetSubmit(items);
|
|
1133
1304
|
if (!classified.ok) {
|
|
1134
1305
|
ctx.json(422, { error: classified.error, code: 'invalid_asset_submit' });
|
|
1135
1306
|
return;
|
|
1136
1307
|
}
|
|
1137
1308
|
if (classified.kind === 'wire') {
|
|
1138
|
-
|
|
1309
|
+
if (abortSynchronousPublish())
|
|
1310
|
+
return;
|
|
1311
|
+
const envelope = this.createSynchronousAssetSubmitEnvelope(classified.bundle, undefined, ctx.now, composeRecipe);
|
|
1139
1312
|
this.writeSynchronousAssetSubmitOutcome(ctx, await this.publishSynchronousBundle(envelope));
|
|
1140
1313
|
return;
|
|
1141
1314
|
}
|
|
1142
1315
|
const results = [];
|
|
1143
1316
|
for (const item of classified.items) {
|
|
1144
|
-
const converted = await convertLegacyLooseAsset(item);
|
|
1317
|
+
const converted = await convertLegacyLooseAsset(item, this.deps.publishExecutionVerifier, this.deps.publishExecutionVerifierTimeoutMs, ctx.signal, this.publishAbortController.signal);
|
|
1145
1318
|
if (!converted.ok) {
|
|
1146
1319
|
results.push({ ok: false, error: converted.error, statusCode: 422 });
|
|
1147
1320
|
continue;
|
|
1148
1321
|
}
|
|
1149
|
-
|
|
1322
|
+
if (abortSynchronousPublish())
|
|
1323
|
+
return;
|
|
1324
|
+
const envelope = this.createSynchronousAssetSubmitEnvelope(converted.bundle, 'v1_loose_asset_compat', ctx.now, composeRecipe);
|
|
1150
1325
|
const outcome = await this.publishSynchronousBundle(envelope);
|
|
1151
1326
|
if (outcome.kind === 'accepted') {
|
|
1152
1327
|
const receipt = outcome.receipt;
|
|
@@ -1175,13 +1350,16 @@ export class ProxyDaemon {
|
|
|
1175
1350
|
});
|
|
1176
1351
|
}
|
|
1177
1352
|
}
|
|
1353
|
+
const summary = summarizeLegacySynchronousResults(results);
|
|
1178
1354
|
ctx.json(200, {
|
|
1179
1355
|
published: results.filter((result) => result.ok).length,
|
|
1180
1356
|
total: results.length,
|
|
1181
1357
|
results,
|
|
1358
|
+
publish_status: summary.publishStatus,
|
|
1359
|
+
queued: summary.queued,
|
|
1182
1360
|
});
|
|
1183
1361
|
}
|
|
1184
|
-
createSynchronousAssetSubmitEnvelope(bundle, source, now) {
|
|
1362
|
+
createSynchronousAssetSubmitEnvelope(bundle, source, now, composeRecipe = true) {
|
|
1185
1363
|
const canonicalBundle = [...bundle].sort(compareSynchronousAssetSubmitAssets);
|
|
1186
1364
|
const runtimeNamespace = this.deps.runtimeNamespace ?? 'default';
|
|
1187
1365
|
const idempotencyKey = synchronousAssetSubmitKey(this.synchronousAssetSubmitScope, runtimeNamespace, this.currentHubMode(), canonicalBundle);
|
|
@@ -1191,6 +1369,7 @@ export class ProxyDaemon {
|
|
|
1191
1369
|
payload: {
|
|
1192
1370
|
...(source ? { source } : {}),
|
|
1193
1371
|
assets: canonicalBundle,
|
|
1372
|
+
compose_recipe: composeRecipe,
|
|
1194
1373
|
[OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
|
|
1195
1374
|
},
|
|
1196
1375
|
idempotencyKey,
|
|
@@ -1198,6 +1377,15 @@ export class ProxyDaemon {
|
|
|
1198
1377
|
now,
|
|
1199
1378
|
});
|
|
1200
1379
|
}
|
|
1380
|
+
composeRecipeAfterAcceptedSubmit(envelope) {
|
|
1381
|
+
if (envelope.type !== 'asset_submit')
|
|
1382
|
+
return;
|
|
1383
|
+
const key = envelope.idempotencyKey || envelope.id;
|
|
1384
|
+
if (this.recipeComposeStarted.has(key))
|
|
1385
|
+
return;
|
|
1386
|
+
this.recipeComposeStarted.add(key);
|
|
1387
|
+
void hubNs.composeRecipeAfterAssetPublish(this.hub, asRecord(envelope.payload)).catch(() => { });
|
|
1388
|
+
}
|
|
1201
1389
|
currentHubMode() {
|
|
1202
1390
|
return this.deps.hubMode ?? 'public';
|
|
1203
1391
|
}
|
|
@@ -1267,6 +1455,7 @@ export class ProxyDaemon {
|
|
|
1267
1455
|
this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
|
|
1268
1456
|
}
|
|
1269
1457
|
catch { /* best-effort */ }
|
|
1458
|
+
this.composeRecipeAfterAcceptedSubmit(envelope);
|
|
1270
1459
|
}
|
|
1271
1460
|
if (this.store.getById(envelope.id)?.status !== 'in_flight')
|
|
1272
1461
|
this.store.complete(envelope.id, this.now());
|
|
@@ -1348,6 +1537,7 @@ export class ProxyDaemon {
|
|
|
1348
1537
|
this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
|
|
1349
1538
|
}
|
|
1350
1539
|
catch { /* best-effort */ }
|
|
1540
|
+
this.composeRecipeAfterAcceptedSubmit(envelope);
|
|
1351
1541
|
}
|
|
1352
1542
|
return receipt;
|
|
1353
1543
|
}
|
|
@@ -1420,13 +1610,19 @@ export class ProxyDaemon {
|
|
|
1420
1610
|
}
|
|
1421
1611
|
writeSynchronousAssetSubmitOutcome(ctx, outcome) {
|
|
1422
1612
|
if (outcome.kind === 'accepted') {
|
|
1423
|
-
ctx.json(200, outcome.receipt);
|
|
1613
|
+
ctx.json(200, withSynchronousPublishStatus(outcome.receipt, 'accepted'));
|
|
1424
1614
|
}
|
|
1425
1615
|
else if (outcome.kind === 'failed') {
|
|
1426
|
-
ctx.json(outcome.statusCode, outcome.body);
|
|
1616
|
+
ctx.json(outcome.statusCode, withSynchronousPublishStatus(outcome.body, 'failed'));
|
|
1427
1617
|
}
|
|
1428
1618
|
else {
|
|
1429
|
-
ctx.json(202, {
|
|
1619
|
+
ctx.json(202, {
|
|
1620
|
+
status: 'pending',
|
|
1621
|
+
message_id: outcome.messageId,
|
|
1622
|
+
durable: true,
|
|
1623
|
+
publish_status: 'pending',
|
|
1624
|
+
queued: true,
|
|
1625
|
+
});
|
|
1430
1626
|
}
|
|
1431
1627
|
}
|
|
1432
1628
|
async handleAtpRoute(ctx) {
|
|
@@ -1670,14 +1866,113 @@ function isClearlyLegacyLooseAsset(value) {
|
|
|
1670
1866
|
return false;
|
|
1671
1867
|
return ['content', 'summary', 'strategy'].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
1672
1868
|
}
|
|
1673
|
-
async function
|
|
1869
|
+
async function resolveVerifiedExecution(verifier, input, expectedValidation, timeoutMs = DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS, parentSignals = []) {
|
|
1870
|
+
if (!verifier)
|
|
1871
|
+
return undefined;
|
|
1872
|
+
if (parentSignals.some((signal) => signal.aborted))
|
|
1873
|
+
return undefined;
|
|
1874
|
+
const boundedTimeout = Number.isSafeInteger(timeoutMs) && timeoutMs > 0
|
|
1875
|
+
? timeoutMs
|
|
1876
|
+
: DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS;
|
|
1877
|
+
const controller = new AbortController();
|
|
1878
|
+
let timer;
|
|
1879
|
+
let resolveAborted;
|
|
1880
|
+
const aborted = new Promise((resolve) => {
|
|
1881
|
+
resolveAborted = () => resolve(null);
|
|
1882
|
+
});
|
|
1883
|
+
const onParentAbort = () => {
|
|
1884
|
+
if (!controller.signal.aborted)
|
|
1885
|
+
controller.abort(new Error('publish_verification_aborted'));
|
|
1886
|
+
resolveAborted();
|
|
1887
|
+
};
|
|
1888
|
+
for (const signal of parentSignals)
|
|
1889
|
+
signal.addEventListener('abort', onParentAbort, { once: true });
|
|
1890
|
+
try {
|
|
1891
|
+
const timeout = new Promise((resolve) => {
|
|
1892
|
+
timer = setTimeout(() => {
|
|
1893
|
+
if (!controller.signal.aborted)
|
|
1894
|
+
controller.abort(new Error('publish_verification_timeout'));
|
|
1895
|
+
resolve(null);
|
|
1896
|
+
}, boundedTimeout);
|
|
1897
|
+
timer.unref?.();
|
|
1898
|
+
});
|
|
1899
|
+
const candidate = await Promise.race([verifier(input, controller.signal), timeout, aborted]);
|
|
1900
|
+
if (!candidate || !Array.isArray(candidate.trace) || candidate.trace.length === 0)
|
|
1901
|
+
return undefined;
|
|
1902
|
+
if (controller.signal.aborted)
|
|
1903
|
+
return undefined;
|
|
1904
|
+
if (candidate.trace.some((row) => (!row
|
|
1905
|
+
|| typeof row.command !== 'string'
|
|
1906
|
+
|| row.command.trim().length === 0
|
|
1907
|
+
|| !Number.isInteger(row.exit)
|
|
1908
|
+
|| row.exit !== 0)))
|
|
1909
|
+
return undefined;
|
|
1910
|
+
if (candidate.trace.length !== expectedValidation.length)
|
|
1911
|
+
return undefined;
|
|
1912
|
+
if (candidate.trace.some((row, index) => row.command.trim() !== expectedValidation[index]))
|
|
1913
|
+
return undefined;
|
|
1914
|
+
return { ...candidate, validation: expectedValidation };
|
|
1915
|
+
}
|
|
1916
|
+
catch {
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
if (timer !== undefined)
|
|
1921
|
+
clearTimeout(timer);
|
|
1922
|
+
for (const signal of parentSignals)
|
|
1923
|
+
signal.removeEventListener('abort', onParentAbort);
|
|
1924
|
+
if (!controller.signal.aborted)
|
|
1925
|
+
controller.abort(new Error('publish_verification_complete'));
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function declaredValidationCommands(input) {
|
|
1929
|
+
const raw = input.validation ?? input.verification ?? (input.execution && typeof input.execution === 'object' && !Array.isArray(input.execution)
|
|
1930
|
+
? input.execution['validation']
|
|
1931
|
+
: undefined);
|
|
1932
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > 8)
|
|
1933
|
+
return undefined;
|
|
1934
|
+
const commands = raw.map((command) => typeof command === 'string' ? command.trim() : '');
|
|
1935
|
+
if (commands.some((command) => command.length === 0 || command.length > 180 || !verify.isValidationCommandAllowed(command))) {
|
|
1936
|
+
return undefined;
|
|
1937
|
+
}
|
|
1938
|
+
return commands;
|
|
1939
|
+
}
|
|
1940
|
+
async function resolveVerifiedExecutionAfterPreflight(verifier, input, timeoutMs, parentSignals = []) {
|
|
1941
|
+
if (!verifier)
|
|
1942
|
+
return undefined;
|
|
1943
|
+
if (parentSignals.some((signal) => signal.aborted))
|
|
1944
|
+
return undefined;
|
|
1945
|
+
const preflight = await hubNs.distillConversation(input, { persist: false });
|
|
1946
|
+
if (parentSignals.some((signal) => signal.aborted) || !preflight.ok || !preflight.quality.ok)
|
|
1947
|
+
return undefined;
|
|
1948
|
+
const validation = declaredValidationCommands(input);
|
|
1949
|
+
if (!validation)
|
|
1950
|
+
return undefined;
|
|
1951
|
+
// 只把通过质量与命令策略预检的最小验证输入交给宿主,绝不转发调用方的 execution/status/trace。
|
|
1952
|
+
return resolveVerifiedExecution(verifier, { validation }, validation, timeoutMs, parentSignals);
|
|
1953
|
+
}
|
|
1954
|
+
async function convertLegacyLooseAsset(value, verifyExecution, verifyExecutionTimeoutMs, ...parentSignals) {
|
|
1674
1955
|
const normalized = legacyLooseDistillInput(value);
|
|
1675
1956
|
if (!normalized.ok)
|
|
1676
1957
|
return normalized;
|
|
1677
1958
|
try {
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1959
|
+
const verifiedExecution = await resolveVerifiedExecutionAfterPreflight(verifyExecution, normalized.input, verifyExecutionTimeoutMs, parentSignals.filter((signal) => signal !== undefined));
|
|
1960
|
+
const distilled = await hubNs.distillConversation(normalized.input, {
|
|
1961
|
+
persist: false,
|
|
1962
|
+
...(verifiedExecution ? { verifiedExecution } : {}),
|
|
1963
|
+
});
|
|
1964
|
+
if (!distilled.ok)
|
|
1680
1965
|
return { ok: false, error: `legacy_distill_${safeIdentifier(distilled.reason)}` };
|
|
1966
|
+
// Mirror the /conversation/distill route: a draft that passed the quality gate but was
|
|
1967
|
+
// blocked only by the missing host execution evidence must not be reported as a
|
|
1968
|
+
// quality-gate failure.
|
|
1969
|
+
if (distilled.publishable !== true) {
|
|
1970
|
+
return {
|
|
1971
|
+
ok: false,
|
|
1972
|
+
error: distilled.quality.ok && verifiedExecution === undefined
|
|
1973
|
+
? 'legacy_distill_execution_evidence'
|
|
1974
|
+
: 'legacy_distill_quality_gate',
|
|
1975
|
+
};
|
|
1681
1976
|
}
|
|
1682
1977
|
const gene = {
|
|
1683
1978
|
...distilled.gene,
|
|
@@ -1809,7 +2104,7 @@ function parseLegacyCategory(value) {
|
|
|
1809
2104
|
return { ok: false, error: 'legacy category is invalid' };
|
|
1810
2105
|
}
|
|
1811
2106
|
function deterministicDistilledAsset(asset) {
|
|
1812
|
-
const draft = { ...asset, asset_id: '' };
|
|
2107
|
+
const draft = wire.stripGeneHints({ ...asset, asset_id: '' });
|
|
1813
2108
|
// `_source` is local distiller provenance and is not part of the current GEP Gene schema.
|
|
1814
2109
|
// It also contains a wall-clock timestamp, so it must not influence compatibility asset ids.
|
|
1815
2110
|
delete draft['_source'];
|
|
@@ -19,6 +19,7 @@ interface SystemdNotifierOptions {
|
|
|
19
19
|
execFile?: SystemdNotifyExec;
|
|
20
20
|
readyRetryDelaysMs?: readonly number[];
|
|
21
21
|
sleep?: (delayMs: number) => Promise<void>;
|
|
22
|
+
notifyCommand?: string;
|
|
22
23
|
}
|
|
23
24
|
export declare function systemdWatchdogIntervalMs(env?: NodeJS.ProcessEnv): number;
|
|
24
25
|
export declare class SystemdNotifier {
|
|
@@ -29,6 +30,7 @@ export declare class SystemdNotifier {
|
|
|
29
30
|
private readonly execFile;
|
|
30
31
|
private readonly readyRetryDelaysMs;
|
|
31
32
|
private readonly sleep;
|
|
33
|
+
private readonly notifyCommand;
|
|
32
34
|
private timer;
|
|
33
35
|
private readySent;
|
|
34
36
|
private readyInFlight;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
|
|
3
4
|
const MIN_WATCHDOG_INTERVAL_MS = 1_000;
|
|
4
5
|
const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
|
|
5
6
|
const MAX_READY_RETRIES = 4;
|
|
6
7
|
const MAX_READY_RETRY_DELAY_MS = 5_000;
|
|
8
|
+
const SYSTEMD_NOTIFY_CANDIDATES = [
|
|
9
|
+
'/usr/bin/systemd-notify',
|
|
10
|
+
'/bin/systemd-notify',
|
|
11
|
+
'/run/current-system/sw/bin/systemd-notify',
|
|
12
|
+
];
|
|
7
13
|
const defaultSystemdNotifyExec = (command, args, options, callback) => {
|
|
8
14
|
execFile(command, [...args], options, (error) => { callback(error); });
|
|
9
15
|
};
|
|
@@ -21,6 +27,7 @@ export class SystemdNotifier {
|
|
|
21
27
|
execFile;
|
|
22
28
|
readyRetryDelaysMs;
|
|
23
29
|
sleep;
|
|
30
|
+
notifyCommand;
|
|
24
31
|
timer;
|
|
25
32
|
readySent = false;
|
|
26
33
|
readyInFlight;
|
|
@@ -32,6 +39,9 @@ export class SystemdNotifier {
|
|
|
32
39
|
this.execFile = options.execFile ?? defaultSystemdNotifyExec;
|
|
33
40
|
this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
|
|
34
41
|
this.sleep = options.sleep ?? sleepMs;
|
|
42
|
+
this.notifyCommand = options.notifyCommand
|
|
43
|
+
?? SYSTEMD_NOTIFY_CANDIDATES.find((candidate) => existsSync(candidate))
|
|
44
|
+
?? SYSTEMD_NOTIFY_CANDIDATES[0];
|
|
35
45
|
}
|
|
36
46
|
async ready() {
|
|
37
47
|
if (!this.active())
|
|
@@ -123,7 +133,7 @@ export class SystemdNotifier {
|
|
|
123
133
|
notify(state) {
|
|
124
134
|
return new Promise((resolve) => {
|
|
125
135
|
try {
|
|
126
|
-
this.execFile(
|
|
136
|
+
this.execFile(this.notifyCommand, [state], {
|
|
127
137
|
env: this.env,
|
|
128
138
|
timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
|
|
129
139
|
windowsHide: true,
|