@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
package/bin/cliOperations.ts
CHANGED
|
@@ -978,27 +978,37 @@ async function cliOperations(req: any, skipResponseLog = false) {
|
|
|
978
978
|
// when response headers arrive (streamResponse: true), which happens before the full
|
|
979
979
|
// upload completes — calling endUpload() here would snap the bar prematurely.
|
|
980
980
|
|
|
981
|
+
const { statusCode } = response;
|
|
982
|
+
const failedStatus = statusCode < 200 || (statusCode >= 300 && statusCode !== 304);
|
|
983
|
+
const eventStream = useSse && response.headers['content-type']?.startsWith('text/event-stream');
|
|
981
984
|
let responseData;
|
|
982
|
-
if (
|
|
985
|
+
if (eventStream && !failedStatus) {
|
|
983
986
|
// Consume SSE: render phase events live, capture the final result from the `done`
|
|
984
987
|
// event (or the error message from the `error` event). The HTTP status stays 200
|
|
985
988
|
// until end-of-stream; failures are signaled in-band.
|
|
986
989
|
let finalResult;
|
|
987
990
|
let sseError;
|
|
991
|
+
let terminated = false;
|
|
992
|
+
// Older servers write a result as unnamed frames, with no `done` event.
|
|
993
|
+
let lastUnnamedData: string | undefined;
|
|
988
994
|
for await (const message of parseSSE(response)) {
|
|
989
995
|
renderer?.renderEvent(message);
|
|
990
996
|
if (message.event === 'done') {
|
|
997
|
+
terminated = true;
|
|
991
998
|
try {
|
|
992
999
|
finalResult = JSON.parse(message.data)?.result;
|
|
993
1000
|
} catch {
|
|
994
1001
|
finalResult = message.data;
|
|
995
1002
|
}
|
|
996
1003
|
} else if (message.event === 'error') {
|
|
1004
|
+
terminated = true;
|
|
997
1005
|
try {
|
|
998
1006
|
sseError = JSON.parse(message.data);
|
|
999
1007
|
} catch {
|
|
1000
1008
|
sseError = { message: message.data };
|
|
1001
1009
|
}
|
|
1010
|
+
} else if (message.event === 'message') {
|
|
1011
|
+
lastUnnamedData = message.data;
|
|
1002
1012
|
}
|
|
1003
1013
|
}
|
|
1004
1014
|
if (sseError) {
|
|
@@ -1006,6 +1016,13 @@ async function cliOperations(req: any, skipResponseLog = false) {
|
|
|
1006
1016
|
console.error(`error: ${errMsg}`);
|
|
1007
1017
|
process.exit(1);
|
|
1008
1018
|
}
|
|
1019
|
+
if (finalResult === undefined && !terminated && lastUnnamedData !== undefined) {
|
|
1020
|
+
try {
|
|
1021
|
+
finalResult = JSON.parse(lastUnnamedData);
|
|
1022
|
+
} catch {
|
|
1023
|
+
finalResult = { message: lastUnnamedData };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1009
1026
|
responseData = finalResult ?? { message: 'Deploy completed (no result payload).' };
|
|
1010
1027
|
} else {
|
|
1011
1028
|
// When useSse is true, httpRequest returns a raw IncomingMessage (streamResponse mode),
|
|
@@ -1018,6 +1035,12 @@ async function cliOperations(req: any, skipResponseLog = false) {
|
|
|
1018
1035
|
} else {
|
|
1019
1036
|
bodyText = response.body;
|
|
1020
1037
|
}
|
|
1038
|
+
// A request refused before its stream started can still arrive labeled as an event stream,
|
|
1039
|
+
// with the error written as an unnamed frame (older servers). Unwrap it; a body with no
|
|
1040
|
+
// frames at all (a proxy's error page) stays as it came.
|
|
1041
|
+
if (eventStream) {
|
|
1042
|
+
for await (const message of parseSSE(Readable.from([bodyText]))) bodyText = message.data;
|
|
1043
|
+
}
|
|
1021
1044
|
try {
|
|
1022
1045
|
responseData = JSON.parse(bodyText);
|
|
1023
1046
|
} catch {
|
|
@@ -1035,8 +1058,7 @@ async function cliOperations(req: any, skipResponseLog = false) {
|
|
|
1035
1058
|
responseLog = YAML.stringify(responseData).trim();
|
|
1036
1059
|
}
|
|
1037
1060
|
|
|
1038
|
-
|
|
1039
|
-
if (statusCode < 200 || (statusCode >= 300 && statusCode !== 304)) {
|
|
1061
|
+
if (failedStatus) {
|
|
1040
1062
|
const errorPrefix = responseLog.startsWith('error:') ? '' : 'error: ';
|
|
1041
1063
|
console.error(`${errorPrefix}${responseLog}`);
|
|
1042
1064
|
process.exit(1);
|
package/bin/run.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { startHTTPThreads } from '../server/threads/socketRouter.ts';
|
|
|
26
26
|
import { beginProcessShutdown } from '../server/threads/manageThreads.js';
|
|
27
27
|
import * as hdbInfoController from '../dataLayer/hdbInfoController.ts';
|
|
28
28
|
import { isReadOnlyMode } from '../resources/databases.ts';
|
|
29
|
+
import { declareTokenUseTable } from '../security/authn/oidc/tokenUseTable.ts';
|
|
30
|
+
import { ensureCertificateVerificationTables } from '../security/certificateVerification/verificationTables.ts';
|
|
29
31
|
import { getThisNodeName, getThisNodeHostname } from '../server/nodeName.ts';
|
|
30
32
|
import * as hdbTerms from '../utility/hdbTerms.ts';
|
|
31
33
|
import { getHdbPid, isProcessRunning } from '../utility/processManagement/processManagement.js';
|
|
@@ -184,6 +186,22 @@ async function initialize(calledByInstall = false, calledByMain = false) {
|
|
|
184
186
|
process.exit(1);
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
// Every boot: data already at a 5.3.0 pre-release never runs the 5.3.0 directive (compareVersions sorts
|
|
190
|
+
// those versions above 5.3.0). Before worker threads start, so they load the table with its expiration.
|
|
191
|
+
if (!isReadOnlyMode()) {
|
|
192
|
+
try {
|
|
193
|
+
declareTokenUseTable();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
hdbLogger.error('Starting without the declared system.hdb_oidc_token_use; the next start retries', error);
|
|
196
|
+
}
|
|
197
|
+
// A node that never verifies a client certificate still holds the rows its peers replicate.
|
|
198
|
+
try {
|
|
199
|
+
await ensureCertificateVerificationTables();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
hdbLogger.error('Starting without the declared certificate verification tables; the next start retries', error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
// A built-in component only activates when its config key is present. Fresh installs get these
|
|
188
206
|
// keys from defaultConfig.yaml, but an in-place upgrade carries the old config forward without a
|
|
189
207
|
// key added in a newer release (harper-pro#585). Ensure them on every boot — idempotent and
|
|
@@ -55,6 +55,10 @@ import {
|
|
|
55
55
|
writeFile,
|
|
56
56
|
} from 'node:fs/promises';
|
|
57
57
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
58
|
+
import {
|
|
59
|
+
confirmWindowsProcessTreeGone,
|
|
60
|
+
type WindowsProcessTreeIdentity,
|
|
61
|
+
} from '../server/threads/windowsProcessTree.ts';
|
|
58
62
|
import { tmpdir } from 'node:os';
|
|
59
63
|
import { randomUUID } from 'node:crypto';
|
|
60
64
|
import { chmodSync, createReadStream, existsSync, lstatSync, renameSync } from 'node:fs';
|
|
@@ -610,6 +614,19 @@ const RECOVERY_LOCK_WAIT = {
|
|
|
610
614
|
const COMPONENT_RECOVERY_LOCK_PURPOSE = 'component-recovery';
|
|
611
615
|
const MAX_GIT_EXTRACTION_COMMANDS = 4;
|
|
612
616
|
const MAX_INSTALL_COMMANDS = 2;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* The longest one preparation may legitimately run: the longest extraction path runs clone, tag listing,
|
|
620
|
+
* checkout, and npm pack, and a custom package manager configured to warn can then fall back to npm, yielding
|
|
621
|
+
* two install commands. What a preparation lock waits on, and what an origin allows a peer's replicated deploy.
|
|
622
|
+
*/
|
|
623
|
+
export function componentPreparationBudgetMs(installTimeoutMs: number = DEFAULT_COMMAND_TIMEOUT_MS): number {
|
|
624
|
+
return (
|
|
625
|
+
MAX_GIT_EXTRACTION_COMMANDS * DEFAULT_COMMAND_TIMEOUT_MS +
|
|
626
|
+
MAX_INSTALL_COMMANDS * installTimeoutMs +
|
|
627
|
+
COMPONENT_PREPARATION_WAIT_MARGIN_MS
|
|
628
|
+
);
|
|
629
|
+
}
|
|
613
630
|
const PRODUCTION_DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const;
|
|
614
631
|
const INSTALL_LIFECYCLE_SCRIPTS = new Set([
|
|
615
632
|
'preinstall',
|
|
@@ -4474,13 +4491,8 @@ export async function prepareApplication(application: Application, options: Prep
|
|
|
4474
4491
|
}
|
|
4475
4492
|
},
|
|
4476
4493
|
{
|
|
4477
|
-
//
|
|
4478
|
-
|
|
4479
|
-
// commands. Bound orphaned same-process worker locks without rejecting behind a valid holder.
|
|
4480
|
-
timeoutMs:
|
|
4481
|
-
MAX_GIT_EXTRACTION_COMMANDS * DEFAULT_COMMAND_TIMEOUT_MS +
|
|
4482
|
-
MAX_INSTALL_COMMANDS * commandTimeoutMs +
|
|
4483
|
-
COMPONENT_PREPARATION_WAIT_MARGIN_MS,
|
|
4494
|
+
// Bound orphaned same-process worker locks without rejecting behind a valid holder.
|
|
4495
|
+
timeoutMs: componentPreparationBudgetMs(commandTimeoutMs),
|
|
4484
4496
|
onWait: (owner) =>
|
|
4485
4497
|
application.logger.info(
|
|
4486
4498
|
`Waiting for in-progress preparation of ${application.name}` +
|
|
@@ -5209,6 +5221,10 @@ function spawnWithEnv(
|
|
|
5209
5221
|
}
|
|
5210
5222
|
const spawnLogger = logger.loggerWithTag(`${applicationName}:spawn:${command}`);
|
|
5211
5223
|
|
|
5224
|
+
// The child is created inside spawn(), so the interval around the call bounds its creation
|
|
5225
|
+
// time exactly — what the Windows tree scan needs to tell its children from an orphan of the
|
|
5226
|
+
// PID's previous owner.
|
|
5227
|
+
const spawnStartedAt = Date.now();
|
|
5212
5228
|
const childProcess = spawn(command, args, {
|
|
5213
5229
|
shell: true,
|
|
5214
5230
|
cwd,
|
|
@@ -5218,13 +5234,29 @@ function spawnWithEnv(
|
|
|
5218
5234
|
// and install-script descendant before the component preparation lock is released.
|
|
5219
5235
|
detached: process.platform !== 'win32',
|
|
5220
5236
|
});
|
|
5237
|
+
const rootKnownAt = Date.now();
|
|
5221
5238
|
const trackedProcessId = childProcess.pid;
|
|
5222
|
-
|
|
5239
|
+
// Read live by the Windows tree scan: a root that exits mid-wait must bound its children
|
|
5240
|
+
// from that moment, since its PID is reusable from then on.
|
|
5241
|
+
const treeIdentity: WindowsProcessTreeIdentity = {
|
|
5242
|
+
rootPid: trackedProcessId ?? 0,
|
|
5243
|
+
rootKnownAt,
|
|
5244
|
+
// A backward wall-clock step between the two Date.now() calls above (e.g. an NTP
|
|
5245
|
+
// correction) would otherwise make this negative — the same case the registry path
|
|
5246
|
+
// already guards (manageThreads.js's addProcessGroup discards spawnStartedAt entirely
|
|
5247
|
+
// when it reads later than spawnedAt). Falling back to the same allowance here keeps both
|
|
5248
|
+
// paths consistent instead of silently narrowing the window that admits the root's own
|
|
5249
|
+
// earliest children.
|
|
5250
|
+
rootStartedWithinMs: spawnStartedAt <= rootKnownAt ? rootKnownAt - spawnStartedAt : undefined,
|
|
5251
|
+
};
|
|
5252
|
+
const processGroupRegistration = trackedProcessId
|
|
5253
|
+
? registerProcessGroup(trackedProcessId, rootKnownAt, spawnStartedAt)
|
|
5254
|
+
: undefined;
|
|
5223
5255
|
let processGroupIsTracked = Boolean(trackedProcessId);
|
|
5224
5256
|
const untrackProcessGroup = () => {
|
|
5225
5257
|
if (!processGroupIsTracked || !trackedProcessId) return;
|
|
5226
5258
|
processGroupIsTracked = false;
|
|
5227
|
-
unregisterProcessGroup(trackedProcessId);
|
|
5259
|
+
unregisterProcessGroup(trackedProcessId, processGroupRegistration);
|
|
5228
5260
|
};
|
|
5229
5261
|
|
|
5230
5262
|
let didTimeout = false;
|
|
@@ -5235,7 +5267,7 @@ function spawnWithEnv(
|
|
|
5235
5267
|
});
|
|
5236
5268
|
const timeout = setTimeout(() => {
|
|
5237
5269
|
didTimeout = true;
|
|
5238
|
-
void terminateProcessTree(childProcess, closePromise).then(
|
|
5270
|
+
void terminateProcessTree(childProcess, closePromise, treeIdentity).then(
|
|
5239
5271
|
() => {
|
|
5240
5272
|
// Only untrack once terminateProcessTree has confirmed the group is actually gone.
|
|
5241
5273
|
// The direct child can emit 'close' mid-grace-period (e.g. right after SIGTERM)
|
|
@@ -5302,6 +5334,7 @@ function spawnWithEnv(
|
|
|
5302
5334
|
});
|
|
5303
5335
|
|
|
5304
5336
|
childProcess.on('exit', (code, signal) => {
|
|
5337
|
+
treeIdentity.rootExitedAt = Date.now();
|
|
5305
5338
|
spawnLogger.debug?.(`Direct child exited with code ${code}, signal ${signal}; awaiting stdio close`);
|
|
5306
5339
|
});
|
|
5307
5340
|
|
|
@@ -5327,7 +5360,7 @@ function spawnWithEnv(
|
|
|
5327
5360
|
// when a timeout is already driving its own terminateProcessTree call for this process.
|
|
5328
5361
|
if (!didTimeout && trackedProcessId) {
|
|
5329
5362
|
try {
|
|
5330
|
-
await terminateProcessTree(childProcess, closePromise);
|
|
5363
|
+
await terminateProcessTree(childProcess, closePromise, treeIdentity);
|
|
5331
5364
|
} catch (error) {
|
|
5332
5365
|
untrackProcessGroup();
|
|
5333
5366
|
flushOutput();
|
|
@@ -5397,71 +5430,6 @@ export async function waitForConfirmedTermination(
|
|
|
5397
5430
|
while (await isAlive()) await delay(pollMs);
|
|
5398
5431
|
}
|
|
5399
5432
|
|
|
5400
|
-
export async function waitForWindowsTreeTermination(
|
|
5401
|
-
attemptTermination: () => boolean | Promise<boolean>,
|
|
5402
|
-
treeIsAlive: () => boolean | null | Promise<boolean | null>,
|
|
5403
|
-
pollMs: number = PROCESS_TERMINATION_POLL_MS
|
|
5404
|
-
): Promise<void> {
|
|
5405
|
-
for (;;) {
|
|
5406
|
-
// A successful taskkill exit only proves the request was accepted, not that the whole tree
|
|
5407
|
-
// has actually exited — Windows termination is asynchronous, and taskkill can report overall
|
|
5408
|
-
// success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
|
|
5409
|
-
// treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
|
|
5410
|
-
// `true` or `null` (unknown) must keep the loop retrying.
|
|
5411
|
-
await attemptTermination();
|
|
5412
|
-
if ((await treeIsAlive()) === false) return;
|
|
5413
|
-
await delay(pollMs);
|
|
5414
|
-
}
|
|
5415
|
-
}
|
|
5416
|
-
|
|
5417
|
-
async function windowsProcessTreeIsAlive(rootPid: number): Promise<boolean | null> {
|
|
5418
|
-
// Query the process table rather than probing only the parent PID: descendants retain their
|
|
5419
|
-
// ParentProcessId after the parent exits, which is exactly the taskkill "process not found" race.
|
|
5420
|
-
// Exit code 1 must mean "queried the process table and positively found nothing" — never
|
|
5421
|
-
// "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
|
|
5422
|
-
// otherwise read identically to a confirmed-gone tree and release the lock while a descendant
|
|
5423
|
-
// may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
|
|
5424
|
-
// failure into its own exit code (2), which the caller below already treats as unknown.
|
|
5425
|
-
const script =
|
|
5426
|
-
"$ErrorActionPreference = 'Stop'; try { " +
|
|
5427
|
-
`$rootPid = ${rootPid}; ` +
|
|
5428
|
-
'$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
|
|
5429
|
-
'$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
|
|
5430
|
-
'while ($frontier.Count -gt 0) { ' +
|
|
5431
|
-
'$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
|
|
5432
|
-
'$seen[$parentPid] = $true; foreach ($p in $all) { ' +
|
|
5433
|
-
'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
|
|
5434
|
-
'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
|
|
5435
|
-
'$frontier = $next }; ' +
|
|
5436
|
-
'if ($found) { exit 0 } else { exit 1 } ' +
|
|
5437
|
-
'} catch { exit 2 }';
|
|
5438
|
-
return new Promise<boolean | null>((resolve) => {
|
|
5439
|
-
const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
5440
|
-
stdio: 'ignore',
|
|
5441
|
-
windowsHide: true,
|
|
5442
|
-
});
|
|
5443
|
-
query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
|
|
5444
|
-
query.once('error', () => resolve(null));
|
|
5445
|
-
});
|
|
5446
|
-
}
|
|
5447
|
-
|
|
5448
|
-
async function terminateWindowsProcessTree(childProcess: ChildProcess): Promise<void> {
|
|
5449
|
-
if (!childProcess.pid) return;
|
|
5450
|
-
const rootPid = childProcess.pid;
|
|
5451
|
-
await waitForWindowsTreeTermination(
|
|
5452
|
-
() =>
|
|
5453
|
-
new Promise<boolean>((resolve) => {
|
|
5454
|
-
const taskkill = spawn('taskkill', ['/pid', String(rootPid), '/T', '/F'], {
|
|
5455
|
-
stdio: 'ignore',
|
|
5456
|
-
windowsHide: true,
|
|
5457
|
-
});
|
|
5458
|
-
taskkill.once('close', (code) => resolve(code === 0));
|
|
5459
|
-
taskkill.once('error', () => resolve(false));
|
|
5460
|
-
}),
|
|
5461
|
-
() => windowsProcessTreeIsAlive(rootPid)
|
|
5462
|
-
);
|
|
5463
|
-
}
|
|
5464
|
-
|
|
5465
5433
|
async function waitForProcessClose(childProcess: ChildProcess, closePromise: Promise<void>): Promise<void> {
|
|
5466
5434
|
let timer: ReturnType<typeof setTimeout>;
|
|
5467
5435
|
const closeTimeout = new Promise<false>((resolve) => {
|
|
@@ -5478,13 +5446,19 @@ async function waitForProcessClose(childProcess: ChildProcess, closePromise: Pro
|
|
|
5478
5446
|
}
|
|
5479
5447
|
}
|
|
5480
5448
|
|
|
5481
|
-
export async function terminateProcessTree(
|
|
5449
|
+
export async function terminateProcessTree(
|
|
5450
|
+
childProcess: ChildProcess,
|
|
5451
|
+
closePromise: Promise<void>,
|
|
5452
|
+
treeIdentity: WindowsProcessTreeIdentity
|
|
5453
|
+
): Promise<void> {
|
|
5482
5454
|
if (!childProcess.pid) {
|
|
5483
5455
|
await waitForProcessClose(childProcess, closePromise);
|
|
5484
5456
|
return;
|
|
5485
5457
|
}
|
|
5486
5458
|
if (process.platform === 'win32') {
|
|
5487
|
-
await
|
|
5459
|
+
await confirmWindowsProcessTreeGone(treeIdentity, {
|
|
5460
|
+
label: `pid ${childProcess.pid} (${childProcess.spawnargs.join(' ')})`,
|
|
5461
|
+
});
|
|
5488
5462
|
await waitForProcessClose(childProcess, closePromise);
|
|
5489
5463
|
return;
|
|
5490
5464
|
}
|
package/components/DESIGN.md
CHANGED
|
@@ -49,12 +49,45 @@ The ordering is the design. Three things used to be wrong, the first two in a wa
|
|
|
49
49
|
load-error PROBE, not a safety guarantee: it executes the component's own top-level code with
|
|
50
50
|
incomplete side-effect isolation. It also remains a no-op on the main thread, and the operations API
|
|
51
51
|
deploys on the main thread — so operator deploys are still unvalidated, exactly as before. Fixing that
|
|
52
|
-
is separate work; this only fixed the order.
|
|
52
|
+
is separate work; this only fixed the order. Nor does it run on a worker under the default lockdown —
|
|
53
|
+
see the next section.
|
|
53
54
|
- **Root config was written before the build and never rolled back**, so a build or validation that failed
|
|
54
55
|
still left config naming the release, and `installApplications()` installed it at the next restart. The
|
|
55
56
|
entry is now the transaction's third effect, published after the commit — see "Root config is an effect
|
|
56
57
|
of the activation" below.
|
|
57
58
|
|
|
59
|
+
### The load validation runs only where it can reproduce a boot load
|
|
60
|
+
|
|
61
|
+
`validateComponentLoads` (`components/operations.js`) loads the candidate in the thread running the deploy,
|
|
62
|
+
so its verdict is only worth what that thread has in common with a freshly started worker. Two kinds of
|
|
63
|
+
thread have too little, and it runs in neither:
|
|
64
|
+
|
|
65
|
+
- **The main thread**, where the operations API deploys: application code does not run there.
|
|
66
|
+
- **A worker under `applications.lockdown: freeze-after-load`**, the default, which is where a replicated
|
|
67
|
+
peer runs the deploy. The lockdown freezes a worker's intrinsics once its boot load finishes, and a freeze
|
|
68
|
+
is per realm and cannot be undone. A dependency that extends an intrinsic as it loads (reflect-metadata
|
|
69
|
+
defines `Reflect.decorate`) then throws in the deploying worker, yet loads in every worker a restart
|
|
70
|
+
starts. 5.2 hid this by swapping before it validated, so a failed first deploy still landed and
|
|
71
|
+
`restart: true` loaded it. Once validation moved before the swap, every peer refused every retry of a
|
|
72
|
+
working component while the origin, which does not validate, ran it (#2881).
|
|
73
|
+
`laterLoadsMeetFrozenIntrinsics()` (`security/jsLoader.ts`) is the test. It holds in any thread that
|
|
74
|
+
runs the boot load, so a deploy racing that load is excluded as well.
|
|
75
|
+
|
|
76
|
+
Workers still validate under the other modes
|
|
77
|
+
(`integrationTests/deploy/worker-deploy-frozen-intrinsics.test.ts`). `freeze` and `ses` freeze before the
|
|
78
|
+
boot load, so a later load meets exactly the intrinsics it met. `none` freezes nothing, but an intrinsic the
|
|
79
|
+
live version of a component changed, such as a property it made non-configurable, still differs from what a
|
|
80
|
+
fresh worker's load of the candidate meets. That narrower mismatch predates this and is left alone.
|
|
81
|
+
|
|
82
|
+
Under the default, no node load-validates a deploy. A candidate that throws at load goes live on every node
|
|
83
|
+
alike, and each restarted worker fails that component closed and serves the rest. That was always the
|
|
84
|
+
origin's behavior, and it replaces a cluster split between versions. A node on an earlier 5.3 build still
|
|
85
|
+
validates in its frozen workers and keeps refusing a candidate whose dependency extends an intrinsic at load,
|
|
86
|
+
so such a deploy converges only once every node is upgraded; retry it then. The `load` progress phase no
|
|
87
|
+
longer fires on a worker either; the operations API, on the main thread, never emitted it. Validating there
|
|
88
|
+
again needs a load in a fresh realm: #2315 step 2 plans it as a canary rollout, whose first replacement
|
|
89
|
+
worker boots the candidate before it takes traffic.
|
|
90
|
+
|
|
58
91
|
### Staging a build now and activating it later
|
|
59
92
|
|
|
60
93
|
`deploy_component { activate: false }` stops after certification, leaving a dormant artifact; a later
|
|
@@ -94,7 +127,8 @@ did not need it:
|
|
|
94
127
|
them:** nothing stops a dormant artifact being edited while it waits, so the link rule and the load
|
|
95
128
|
validation are both re-run at activation rather than trusted from the marker. Content tampering is still
|
|
96
129
|
not detected — that needs a manifest the marker is bound to, and the load validation that would catch a
|
|
97
|
-
broken entry point is a no-op on the main thread
|
|
130
|
+
broken entry point is a no-op on the main thread, and on a worker under the default lockdown, until
|
|
131
|
+
#2315 step 2.
|
|
98
132
|
|
|
99
133
|
Certification moved out of `activateCandidateApplication` and up into `prepareApplication` for the same
|
|
100
134
|
reason: `markCandidateComplete` fsyncs the whole candidate tree, and a delayed activation must not re-walk
|
|
@@ -196,7 +230,8 @@ and the journal removal: the journal is what would carry the activation forward
|
|
|
196
230
|
|
|
197
231
|
Two limits are deliberate and tracked separately: activation is two renames, so the live _pathname_ is
|
|
198
232
|
briefly absent (in-memory resources are unaffected, but a component that opens its own files during a
|
|
199
|
-
request can still see a gap); and validation does not run on the main-thread deploy path
|
|
233
|
+
request can still see a gap); and validation does not run on the main-thread deploy path, nor on a worker
|
|
234
|
+
under the default lockdown.
|
|
200
235
|
|
|
201
236
|
### Root config is an effect of the activation
|
|
202
237
|
|
|
@@ -346,6 +381,19 @@ starting `handleApplication`; if a deploy begins during the load, the plugin tim
|
|
|
346
381
|
unpaused load time. This prevents a long install from looking like a hung plugin while its entry handlers
|
|
347
382
|
are deliberately paused against the intermediate tree.
|
|
348
383
|
|
|
384
|
+
A deploy's own validation load is exempt: its Scopes (`isTransientValidation`, set at construction from
|
|
385
|
+
`collectScopes`) never read the deploy state, subscribe to `deploy:start`/`deploy:end`, pause their
|
|
386
|
+
watchers, or suspend their plugin timeout. The load runs inside the deploy's lifecycle bracket, between
|
|
387
|
+
build and swap, so any wait for a deploy to end can be a wait on itself. It was, for a component deployed
|
|
388
|
+
as `harper`: the validation load passes no `appName`, so its Scopes take the loader's default name
|
|
389
|
+
`harper`, matched the deploy in flight, and waited for its end while the deploy waited for them. A
|
|
390
|
+
replicated deploy of that component hung on every peer, since only worker threads validate. The same
|
|
391
|
+
cycle formed when a `harper` deploy started on a worker while another component's validation was running
|
|
392
|
+
there, because that deploy's own validation queues behind the running one (`validationChain`). The
|
|
393
|
+
candidate tree is complete before validation starts and nothing else writes to it, so there is nothing to
|
|
394
|
+
wait for. `unitTests/components/componentLoader.test.js` ("deploy validation loads never wait on a
|
|
395
|
+
deploy") holds both routes and the plain timeout.
|
|
396
|
+
|
|
349
397
|
### The component load lock is keyed by plugin type, so a plugin's promise is everyone's clock
|
|
350
398
|
|
|
351
399
|
`sequentiallyHandleApplication` (`components/componentLoader.ts`) holds a cross-thread lock keyed by the
|
|
@@ -424,6 +472,31 @@ inputs retain their existing symlink behavior.
|
|
|
424
472
|
- **Retry is only safe before any byte has reached the consumer.** Once a chunk is handed downstream, re-opening `Blob.stream()` from byte 0 would duplicate it (there's no cheap way to resume from an arbitrary offset across a fresh stream without also plumbing `Blob.slice()`, which was out of scope for this fix). So the helper retries only while the current attempt has yielded nothing yet; a stall after partial content fails immediately, same as before this existed.
|
|
425
473
|
- **Backpressure and cancellation are two different problems, and both are easy to get wrong with a hand-rolled `ReadableStream`.** An early version wrapped the retry loop in `new ReadableStream({ async start(controller) { for await (...) controller.enqueue(chunk) } })` — this eagerly drains `streamFactory()` regardless of `controller.desiredSize`, defeating the whole point of not buffering a multi-GB payload in memory. Switching to an `async function*` consumed via `Readable.from()` restores real backpressure (the generator only resumes when the consumer wants more, matching how the un-wrapped `Blob.stream()` behaved pre-fix). But `Readable.from()`'s `return()`-on-destroy cancellation only takes effect at the generator's _next_ `yield` — while the loop is stuck retrying (no `yield` reached yet), destroying the `Readable` does nothing until the loop naturally exits, verified empirically (a generator that never yields kept retrying long after `.destroy()`). The fix: thread the constructed `Readable` back into the generator via a mutable cell (`readable` doesn't exist until after `Readable.from()` returns, so it can't be closed over directly) and check `.destroyed` explicitly at each loop iteration and after each backoff sleep.
|
|
426
474
|
|
|
475
|
+
## An origin waits for a peer's deploy answer only as long as the peer may take
|
|
476
|
+
|
|
477
|
+
The origin of a replicated `deploy_component` hands `server.replication.replicateOperation` a `timeoutMs`
|
|
478
|
+
from `peerDeployAnswerTimeoutMs(req)` (`components/operations.js`). It is the sum of what the peer is
|
|
479
|
+
allowed for that request: its payload wait (`deployment_timeout`, counted twice when credential references
|
|
480
|
+
must also replicate in), two full preparation budgets (`componentPreparationBudgetMs`: every extraction
|
|
481
|
+
command and both install commands at their allowances), a margin for validation and the swap, and the
|
|
482
|
+
restart ceiling when the peer restarts before answering. It is clamped to the longest delay a timer holds.
|
|
483
|
+
One budget is the peer's own preparation. The other is the preparation lock's wait: a peer already preparing
|
|
484
|
+
the same component for another deploy holds this one at the lock for a budget before the lock re-checks the
|
|
485
|
+
holder.
|
|
486
|
+
|
|
487
|
+
It must never undercut a healthy peer. A shorter bound turns a slow success into a reported failure, and a
|
|
488
|
+
two-command install (a custom package manager falling back to npm) is exactly such a success. So the
|
|
489
|
+
default is hours, and its job is only that the origin eventually settles, rather than holding the
|
|
490
|
+
operation, the deployment row and its own restart for as long as a wedged peer stays wedged. Two things are
|
|
491
|
+
not budgeted. The lock keeps waiting while its holder is alive, so queueing behind a preparation that
|
|
492
|
+
outlasts the lock's wait, or behind several, can run past the deadline. So can plugin `timeout`s a component
|
|
493
|
+
configures beyond the validation margin, which live in the payload the origin does not parse. Covering the
|
|
494
|
+
first would take a deadline that follows the peer's progress rather than a sum of its allowances. The
|
|
495
|
+
deadline is not cancellation: a peer past it may still finish, so the failure the replicator records says
|
|
496
|
+
the outcome there is unknown.
|
|
497
|
+
It stays a `failed` peer result, because `getFailedPeers()` counts only that status, and a new one would
|
|
498
|
+
read as success.
|
|
499
|
+
|
|
427
500
|
## A dangling symlink silently truncates the deploy tarball (`components/packageComponent.ts`)
|
|
428
501
|
|
|
429
502
|
Packaging uses `tar-fs.pack(dir, { dereference: true })` by default (`skip_symlinks` off).
|
package/components/Scope.ts
CHANGED
|
@@ -81,6 +81,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
|
|
|
81
81
|
// Set by the loader on deploy pre-flight validation loads (collectScopes):
|
|
82
82
|
// the scope exists to validate a component, not to run it. Plugins with
|
|
83
83
|
// process-global side effects should validate fully but skip activation.
|
|
84
|
+
// Such a scope never follows a deploy: it loads a finished candidate from inside
|
|
85
|
+
// its own deploy's lifecycle, so waiting or pausing for a deploy waits on itself.
|
|
84
86
|
isTransientValidation?: boolean;
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -103,18 +105,20 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
|
|
|
103
105
|
applicationScope: ApplicationScope,
|
|
104
106
|
origin: string = appName,
|
|
105
107
|
isRootConfig?: boolean,
|
|
106
|
-
mount?: ScopeMount
|
|
108
|
+
mount?: ScopeMount,
|
|
109
|
+
isTransientValidation?: boolean
|
|
107
110
|
) {
|
|
108
111
|
super();
|
|
109
112
|
|
|
110
113
|
this.mount = mount;
|
|
114
|
+
this.isTransientValidation = isTransientValidation;
|
|
111
115
|
this.#appName = appName;
|
|
112
116
|
this.#pluginName = pluginName;
|
|
113
117
|
this.#origin = typeof origin === 'string' ? origin : appName;
|
|
114
118
|
this.#directory = directory;
|
|
115
119
|
this.#configFilePath = configFilePath;
|
|
116
120
|
this.#logger = loggerWithTag(this.#appName);
|
|
117
|
-
this.#deployInFlight = deployLifecycle.loadsAwaitDeploy(this.#appName);
|
|
121
|
+
this.#deployInFlight = !isTransientValidation && deployLifecycle.loadsAwaitDeploy(this.#appName);
|
|
118
122
|
|
|
119
123
|
this.databaseEvents = databaseEventsEmitter;
|
|
120
124
|
this.applicationScope = applicationScope;
|
|
@@ -170,8 +174,10 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
|
|
|
170
174
|
this.#deployEndHandler = (name) => {
|
|
171
175
|
if (name === this.#appName) this.#onDeployEnd(name);
|
|
172
176
|
};
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
if (!isTransientValidation) {
|
|
178
|
+
deployLifecycle.on('deploy:start', this.#deployStartHandler);
|
|
179
|
+
deployLifecycle.on('deploy:end', this.#deployEndHandler);
|
|
180
|
+
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
get logger(): Logger {
|
|
@@ -89,6 +89,15 @@ export interface AnthropicBackendConfig {
|
|
|
89
89
|
* `tools: true` — Anthropic has first-class tool-use support via
|
|
90
90
|
* `tool_use` / `tool_result` content blocks.
|
|
91
91
|
*/
|
|
92
|
+
const CAPABILITIES: ModelCapabilities = Object.freeze({
|
|
93
|
+
embed: false,
|
|
94
|
+
generate: true,
|
|
95
|
+
stream: true,
|
|
96
|
+
tools: true,
|
|
97
|
+
adapters: false,
|
|
98
|
+
structuredOutput: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
export class AnthropicBackend implements ModelBackend {
|
|
93
102
|
readonly name = 'anthropic';
|
|
94
103
|
readonly #baseUrl: string;
|
|
@@ -106,7 +115,7 @@ export class AnthropicBackend implements ModelBackend {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
capabilities(): ModelCapabilities {
|
|
109
|
-
return
|
|
118
|
+
return CAPABILITIES;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
async generate(input: GenerateInput, opts: BackendOpts<GenerateOpts>): Promise<ModelCallResult<GenerateResult>> {
|
|
@@ -145,6 +145,15 @@ export function _injectSdkForTests(sdk: SdkLike): void {
|
|
|
145
145
|
* support tools (or that haven't been wired into the per-family dispatcher
|
|
146
146
|
* yet) raise structured errors.
|
|
147
147
|
*/
|
|
148
|
+
const CAPABILITIES: ModelCapabilities = Object.freeze({
|
|
149
|
+
embed: true,
|
|
150
|
+
generate: true,
|
|
151
|
+
stream: true,
|
|
152
|
+
tools: true,
|
|
153
|
+
adapters: false,
|
|
154
|
+
structuredOutput: false,
|
|
155
|
+
});
|
|
156
|
+
|
|
148
157
|
export class BedrockBackend implements ModelBackend {
|
|
149
158
|
readonly name = 'bedrock';
|
|
150
159
|
readonly #region?: string;
|
|
@@ -159,7 +168,7 @@ export class BedrockBackend implements ModelBackend {
|
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
capabilities(): ModelCapabilities {
|
|
162
|
-
return
|
|
171
|
+
return CAPABILITIES;
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
async embed(input: string | string[], opts: BackendOpts<EmbedOpts>): Promise<ModelCallResult<Float32Array[]>> {
|
|
@@ -49,6 +49,7 @@ import * as auth from '../security/auth.ts';
|
|
|
49
49
|
import * as mqtt from '../server/mqtt.ts';
|
|
50
50
|
import { getConfigFilePath, getConfigObj, getConfigPath } from '../config/configUtils.ts';
|
|
51
51
|
import { bootstrapModels, startModelsConfigHotReload } from '../resources/models/bootstrap.ts';
|
|
52
|
+
import { declareDecisionTablesAtBoot } from '../resources/models/decisionStore.ts';
|
|
52
53
|
import { ErrorResource } from '../resources/ErrorResource.ts';
|
|
53
54
|
import { Scope } from './Scope.ts';
|
|
54
55
|
import { ApplicationScope } from './ApplicationScope.ts';
|
|
@@ -690,13 +691,18 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
|
|
|
690
691
|
whenResolved(sequentiallyHandleApplication(scope, plugin));
|
|
691
692
|
};
|
|
692
693
|
const store = Status.primaryStore;
|
|
693
|
-
|
|
694
|
+
// Keyed per (application, plugin), not the plugin name alone: applications load concurrently,
|
|
695
|
+
// and a plugin-wide lock lets one application's hung handleApplication starve every other
|
|
696
|
+
// application's load of that plugin into the timeout below (#3184). NUL separator: appName
|
|
697
|
+
// can contain dots and slashes, so a printable separator could alias two distinct pairs.
|
|
698
|
+
const lockId = `${scope.appName}\0${scope.pluginName}`;
|
|
699
|
+
const lockAcquired = store.tryLock(lockId, callback);
|
|
694
700
|
|
|
695
701
|
if (!lockAcquired) {
|
|
696
702
|
return new Promise((resolve, reject) => {
|
|
697
703
|
whenResolved = resolve;
|
|
698
704
|
timer = setTimeout(() => {
|
|
699
|
-
reject(new Error(`Timeout waiting for lock on ${scope.pluginName}`));
|
|
705
|
+
reject(new Error(`Timeout waiting for lock on ${scope.pluginName} on behalf of ${scope.appName}`));
|
|
700
706
|
}, timeout + 5_000); // extra time for lock acquisition
|
|
701
707
|
});
|
|
702
708
|
}
|
|
@@ -712,13 +718,14 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
|
|
|
712
718
|
timeout
|
|
713
719
|
);
|
|
714
720
|
} finally {
|
|
715
|
-
Status.primaryStore.unlock(
|
|
721
|
+
Status.primaryStore.unlock(lockId);
|
|
716
722
|
}
|
|
717
723
|
});
|
|
718
724
|
}
|
|
719
725
|
|
|
720
726
|
function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout: number): Promise<T> {
|
|
721
727
|
return new Promise((resolve, reject) => {
|
|
728
|
+
const followsDeploys = !scope.isTransientValidation;
|
|
722
729
|
const absoluteTimeout = timeout + 6 * 60 * 60 * 1000;
|
|
723
730
|
let remaining = timeout;
|
|
724
731
|
let activeSince = 0;
|
|
@@ -739,7 +746,7 @@ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout:
|
|
|
739
746
|
absoluteTimer = setTimeout(() => rejectTimeout(absoluteTimeout), absoluteTimeout);
|
|
740
747
|
absoluteTimer.unref?.();
|
|
741
748
|
const arm = () => {
|
|
742
|
-
if (timer || deployLifecycle.loadsAwaitDeploy(scope.appName)) return;
|
|
749
|
+
if (timer || (followsDeploys && deployLifecycle.loadsAwaitDeploy(scope.appName))) return;
|
|
743
750
|
if (remaining <= 0) return rejectTimeout();
|
|
744
751
|
activeSince = Date.now();
|
|
745
752
|
timer = setTimeout(rejectTimeout, remaining);
|
|
@@ -754,8 +761,10 @@ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout:
|
|
|
754
761
|
if (componentName === scope.appName) arm();
|
|
755
762
|
}
|
|
756
763
|
|
|
757
|
-
|
|
758
|
-
|
|
764
|
+
if (followsDeploys) {
|
|
765
|
+
deployLifecycle.on('deploy:start', handleDeployStart);
|
|
766
|
+
deployLifecycle.on('deploy:end', handleDeployEnd);
|
|
767
|
+
}
|
|
759
768
|
operation.then(
|
|
760
769
|
(value) => {
|
|
761
770
|
cleanup();
|
|
@@ -885,6 +894,7 @@ export async function loadComponent(
|
|
|
885
894
|
if (isRoot) {
|
|
886
895
|
await bootstrapModels(config);
|
|
887
896
|
startModelsConfigHotReload();
|
|
897
|
+
declareDecisionTablesAtBoot();
|
|
888
898
|
}
|
|
889
899
|
|
|
890
900
|
// The `env:` block declares the component's environment expectations (string literal →
|
|
@@ -1073,19 +1083,18 @@ export async function loadComponent(
|
|
|
1073
1083
|
isRoot,
|
|
1074
1084
|
// A root-declared plugin reads its own `host`/`urlPath` straight from this
|
|
1075
1085
|
// config, so only an inherited application mount applies here.
|
|
1076
|
-
mount
|
|
1086
|
+
mount,
|
|
1087
|
+
// Marked so plugins with process-global side effects (e.g. the scheduler
|
|
1088
|
+
// registering jobs into its engine) can validate without activating —
|
|
1089
|
+
// validation scopes may reuse a live component's identity, so activating
|
|
1090
|
+
// from one can displace the real component's registrations.
|
|
1091
|
+
Boolean(options.collectScopes)
|
|
1077
1092
|
);
|
|
1078
1093
|
|
|
1079
1094
|
if (options.collectScopes) {
|
|
1080
1095
|
// A transient/validation load owns these scopes and closes them itself once the
|
|
1081
1096
|
// load is validated (see operations.js deploy pre-flight). Skip the worker-shutdown
|
|
1082
|
-
// auto-close so
|
|
1083
|
-
// accumulate across deploys (#1462).
|
|
1084
|
-
// Mark it so plugins with process-global side effects (e.g. the scheduler
|
|
1085
|
-
// registering jobs into its engine) can validate without activating —
|
|
1086
|
-
// validation scopes may reuse a live component's identity, so activating
|
|
1087
|
-
// from one can displace the real component's registrations.
|
|
1088
|
-
scope.isTransientValidation = true;
|
|
1097
|
+
// auto-close so this SHUTDOWN handler doesn't accumulate across deploys (#1462).
|
|
1089
1098
|
options.collectScopes.add(scope);
|
|
1090
1099
|
} else {
|
|
1091
1100
|
// Track the close so the worker's shutdown path waits for it (and thus for any async
|
|
@@ -360,7 +360,9 @@ export function makeOperationToolHandler(operationName: string) {
|
|
|
360
360
|
} catch (err) {
|
|
361
361
|
const e = err as { message?: string; http_resp_msg?: string; statusCode?: number };
|
|
362
362
|
const message = e?.http_resp_msg ?? e?.message ?? `operation '${operationName}' failed`;
|
|
363
|
-
harperLogger.trace(
|
|
363
|
+
harperLogger.trace(
|
|
364
|
+
`MCP operations/${operationName} threw: ${(err as Error)?.stack ?? (err as Error)?.message ?? message}`
|
|
365
|
+
);
|
|
364
366
|
return {
|
|
365
367
|
isError: true,
|
|
366
368
|
content: [
|