@loadstrike/loadstrike-sdk 1.0.20801 → 1.0.21101
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/cjs/cluster.js +27 -2
- package/dist/cjs/correlation.js +132 -0
- package/dist/cjs/local.js +113 -6
- package/dist/cjs/reporting.js +12 -0
- package/dist/cjs/runtime.js +929 -14
- package/dist/esm/cluster.js +27 -2
- package/dist/esm/correlation.js +132 -0
- package/dist/esm/local.js +113 -6
- package/dist/esm/reporting.js +12 -0
- package/dist/esm/runtime.js +929 -14
- package/dist/types/cluster.d.ts +21 -1
- package/dist/types/correlation.d.ts +132 -0
- package/dist/types/local.d.ts +6 -0
- package/dist/types/reporting.d.ts +12 -0
- package/dist/types/runtime.d.ts +868 -0
- package/package.json +1 -1
package/dist/esm/cluster.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { EndpointAdapterFactory } from "./transports.js";
|
|
3
3
|
export class LocalClusterCoordinator {
|
|
4
|
+
/**
|
|
5
|
+
* Exposes the public constructor operation.
|
|
6
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
7
|
+
*/
|
|
4
8
|
constructor(options = {}) {
|
|
5
9
|
this.options = options;
|
|
6
10
|
}
|
|
@@ -59,6 +63,9 @@ export class LocalClusterCoordinator {
|
|
|
59
63
|
return result;
|
|
60
64
|
}
|
|
61
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Exposes the plan agent scenario assignments operation. Use this when interacting with the SDK through this surface.
|
|
68
|
+
*/
|
|
62
69
|
export function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
|
|
63
70
|
const totalAgents = Math.max(agentCount, 0);
|
|
64
71
|
const assignments = Array.from({ length: totalAgents }, () => []);
|
|
@@ -114,10 +121,14 @@ export function planAgentScenarioAssignments(scenarioNames, agentCount, options
|
|
|
114
121
|
return assignments;
|
|
115
122
|
}
|
|
116
123
|
export class DistributedClusterCoordinator {
|
|
124
|
+
/**
|
|
125
|
+
* Exposes the public constructor operation.
|
|
126
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
127
|
+
*/
|
|
117
128
|
constructor(options) {
|
|
118
129
|
this.options = options;
|
|
119
130
|
}
|
|
120
|
-
async dispatch(assignments) {
|
|
131
|
+
async dispatch(assignments, buildAgentRunToken) {
|
|
121
132
|
const expected = Math.max(this.options.expectedAgentResults, 0);
|
|
122
133
|
const timeoutMs = Math.max(this.options.commandTimeoutMs ?? 120000, 1);
|
|
123
134
|
const runSubject = this.buildRunSubject();
|
|
@@ -139,6 +150,9 @@ export class DistributedClusterCoordinator {
|
|
|
139
150
|
targetScenarios: assignments[i] ?? [],
|
|
140
151
|
replySubject
|
|
141
152
|
};
|
|
153
|
+
if (buildAgentRunToken) {
|
|
154
|
+
command.agentRunToken = await buildAgentRunToken(command);
|
|
155
|
+
}
|
|
142
156
|
await commandProducer.produce({
|
|
143
157
|
headers: {
|
|
144
158
|
"x-cluster-command-id": commandId,
|
|
@@ -199,6 +213,10 @@ export class DistributedClusterCoordinator {
|
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
export class DistributedClusterAgent {
|
|
216
|
+
/**
|
|
217
|
+
* Exposes the public constructor operation.
|
|
218
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
219
|
+
*/
|
|
202
220
|
constructor(options) {
|
|
203
221
|
this.commandConsumer = null;
|
|
204
222
|
this.options = options;
|
|
@@ -248,7 +266,13 @@ export class DistributedClusterAgent {
|
|
|
248
266
|
try {
|
|
249
267
|
let result;
|
|
250
268
|
try {
|
|
251
|
-
const nodeResult = await execute({
|
|
269
|
+
const nodeResult = await execute({
|
|
270
|
+
scenarioNames: command.targetScenarios,
|
|
271
|
+
commandId: command.commandId,
|
|
272
|
+
agentRunToken: command.agentRunToken,
|
|
273
|
+
agentIndex: command.agentIndex,
|
|
274
|
+
agentCount: command.agentCount
|
|
275
|
+
});
|
|
252
276
|
result = {
|
|
253
277
|
commandId: command.commandId,
|
|
254
278
|
agentId: this.options.agentId,
|
|
@@ -298,6 +322,7 @@ function parseRunCommand(value) {
|
|
|
298
322
|
agentIndex: numberOrDefault(value.agentIndex, numberOrDefault(value.AgentIndex, 0)),
|
|
299
323
|
agentCount: numberOrDefault(value.agentCount, numberOrDefault(value.AgentCount, 0)),
|
|
300
324
|
targetScenarios,
|
|
325
|
+
agentRunToken: stringOrDefault(value.agentRunToken, stringOrDefault(value.AgentRunToken, "")),
|
|
301
326
|
replySubject: stringOrDefault(value.replySubject, stringOrDefault(value.ReplySubject, ""))
|
|
302
327
|
};
|
|
303
328
|
}
|
package/dist/esm/correlation.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createClient } from "redis";
|
|
3
3
|
export class TrackingPayloadBuilder {
|
|
4
|
+
/**
|
|
5
|
+
* Exposes the public constructor operation.
|
|
6
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
7
|
+
*/
|
|
4
8
|
constructor() {
|
|
5
9
|
this.Headers = {};
|
|
6
10
|
this.body = new Uint8Array();
|
|
@@ -8,12 +12,24 @@ export class TrackingPayloadBuilder {
|
|
|
8
12
|
get Body() {
|
|
9
13
|
return new Uint8Array(this.body);
|
|
10
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Sets the payload body on the builder.
|
|
17
|
+
* Use this when a tracking payload should be created from raw content.
|
|
18
|
+
*/
|
|
11
19
|
setBody(body) {
|
|
12
20
|
this.body = normalizeTrackingBodyToBytes(body);
|
|
13
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Sets the payload body on the builder.
|
|
24
|
+
* Use this when a tracking payload should be created from raw content.
|
|
25
|
+
*/
|
|
14
26
|
SetBody(body) {
|
|
15
27
|
this.setBody(body);
|
|
16
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds the configured payload or helper object.
|
|
31
|
+
* Use this when all builder inputs are ready to be materialized.
|
|
32
|
+
*/
|
|
17
33
|
build() {
|
|
18
34
|
return createTrackingPayload({
|
|
19
35
|
headers: { ...this.Headers },
|
|
@@ -24,6 +40,10 @@ export class TrackingPayloadBuilder {
|
|
|
24
40
|
jsonConvertSettings: cloneRecord(this.JsonConvertSettings)
|
|
25
41
|
});
|
|
26
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds the configured payload or helper object.
|
|
45
|
+
* Use this when all builder inputs are ready to be materialized.
|
|
46
|
+
*/
|
|
27
47
|
Build() {
|
|
28
48
|
return this.build();
|
|
29
49
|
}
|
|
@@ -41,6 +61,10 @@ export class RedisCorrelationStoreOptions {
|
|
|
41
61
|
set EntryTtl(value) {
|
|
42
62
|
this.EntryTtlSeconds = value;
|
|
43
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Exposes the public validate operation.
|
|
66
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
67
|
+
*/
|
|
44
68
|
validate() {
|
|
45
69
|
if (!this.ConnectionString.trim()) {
|
|
46
70
|
throw new Error("Redis ConnectionString must be provided.");
|
|
@@ -52,6 +76,10 @@ export class RedisCorrelationStoreOptions {
|
|
|
52
76
|
throw new RangeError("Redis EntryTtl must be greater than zero.");
|
|
53
77
|
}
|
|
54
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Exposes the public Validate operation.
|
|
81
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
82
|
+
*/
|
|
55
83
|
Validate() {
|
|
56
84
|
this.validate();
|
|
57
85
|
}
|
|
@@ -60,12 +88,24 @@ export class CorrelationStoreConfiguration {
|
|
|
60
88
|
constructor() {
|
|
61
89
|
this.Kind = "InMemory";
|
|
62
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Creates an in-memory correlation store configuration.
|
|
93
|
+
* Use this when tracking state can stay local to the process.
|
|
94
|
+
*/
|
|
63
95
|
static inMemory() {
|
|
64
96
|
return new CorrelationStoreConfiguration();
|
|
65
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates an in-memory correlation store configuration.
|
|
100
|
+
* Use this when tracking state can stay local to the process.
|
|
101
|
+
*/
|
|
66
102
|
static InMemory() {
|
|
67
103
|
return CorrelationStoreConfiguration.inMemory();
|
|
68
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Creates a Redis-backed correlation store configuration.
|
|
107
|
+
* Use this when tracking state must survive across processes or cluster nodes.
|
|
108
|
+
*/
|
|
69
109
|
static redisStore(options) {
|
|
70
110
|
if (!(options instanceof RedisCorrelationStoreOptions)) {
|
|
71
111
|
throw new TypeError("Redis correlation store options must be provided.");
|
|
@@ -75,9 +115,17 @@ export class CorrelationStoreConfiguration {
|
|
|
75
115
|
configuration.Redis = options;
|
|
76
116
|
return configuration;
|
|
77
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates a Redis-backed correlation store configuration.
|
|
120
|
+
* Use this when tracking state must survive across processes or cluster nodes.
|
|
121
|
+
*/
|
|
78
122
|
static RedisStore(options) {
|
|
79
123
|
return CorrelationStoreConfiguration.redisStore(options);
|
|
80
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Exposes the public validate operation.
|
|
127
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
128
|
+
*/
|
|
81
129
|
validate() {
|
|
82
130
|
if (this.Kind === "Redis") {
|
|
83
131
|
if (!(this.Redis instanceof RedisCorrelationStoreOptions)) {
|
|
@@ -86,6 +134,10 @@ export class CorrelationStoreConfiguration {
|
|
|
86
134
|
this.Redis.validate();
|
|
87
135
|
}
|
|
88
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Exposes the public Validate operation.
|
|
139
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
140
|
+
*/
|
|
89
141
|
Validate() {
|
|
90
142
|
this.validate();
|
|
91
143
|
}
|
|
@@ -97,6 +149,10 @@ export class InMemoryCorrelationStore {
|
|
|
97
149
|
this.sourceOccurrences = new Map();
|
|
98
150
|
this.destinationOccurrences = new Map();
|
|
99
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Sets the current gauge value.
|
|
154
|
+
* Use this when the latest observed value should replace the previous one.
|
|
155
|
+
*/
|
|
100
156
|
set(entry) {
|
|
101
157
|
const normalized = normalizeCorrelationEntry(entry);
|
|
102
158
|
const queue = this.pendingByTrackingId.get(normalized.trackingId) ?? [];
|
|
@@ -104,10 +160,18 @@ export class InMemoryCorrelationStore {
|
|
|
104
160
|
this.pendingByTrackingId.set(normalized.trackingId, queue);
|
|
105
161
|
this.pendingByEventId.set(normalized.eventId ?? "", normalized);
|
|
106
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Exposes the public get operation.
|
|
165
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
166
|
+
*/
|
|
107
167
|
get(trackingId) {
|
|
108
168
|
const queue = this.pendingByTrackingId.get(trackingId);
|
|
109
169
|
return queue && queue.length > 0 ? hydrateCorrelationEntry(queue[0]) : null;
|
|
110
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Exposes the public delete operation.
|
|
173
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
174
|
+
*/
|
|
111
175
|
delete(trackingId) {
|
|
112
176
|
const queue = this.pendingByTrackingId.get(trackingId);
|
|
113
177
|
if (!queue || queue.length === 0) {
|
|
@@ -123,6 +187,10 @@ export class InMemoryCorrelationStore {
|
|
|
123
187
|
}
|
|
124
188
|
this.pendingByTrackingId.set(trackingId, queue);
|
|
125
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Exposes the public all operation.
|
|
192
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
193
|
+
*/
|
|
126
194
|
all() {
|
|
127
195
|
const entries = [];
|
|
128
196
|
for (const queue of this.pendingByTrackingId.values()) {
|
|
@@ -132,21 +200,37 @@ export class InMemoryCorrelationStore {
|
|
|
132
200
|
}
|
|
133
201
|
return entries;
|
|
134
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Exposes the public collectExpired operation.
|
|
205
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
206
|
+
*/
|
|
135
207
|
collectExpired(nowMs = Date.now(), maxCount = 0) {
|
|
136
208
|
void nowMs;
|
|
137
209
|
void maxCount;
|
|
138
210
|
return [];
|
|
139
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Exposes the public incrementSourceOccurrences operation.
|
|
214
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
215
|
+
*/
|
|
140
216
|
incrementSourceOccurrences(trackingId) {
|
|
141
217
|
const next = (this.sourceOccurrences.get(trackingId) ?? 0) + 1;
|
|
142
218
|
this.sourceOccurrences.set(trackingId, next);
|
|
143
219
|
return next;
|
|
144
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Exposes the public incrementDestinationOccurrences operation.
|
|
223
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
224
|
+
*/
|
|
145
225
|
incrementDestinationOccurrences(trackingId) {
|
|
146
226
|
const next = (this.destinationOccurrences.get(trackingId) ?? 0) + 1;
|
|
147
227
|
this.destinationOccurrences.set(trackingId, next);
|
|
148
228
|
return next;
|
|
149
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Exposes the public registerSource operation.
|
|
232
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
233
|
+
*/
|
|
150
234
|
registerSource(trackingId, sourceTimestampUtc) {
|
|
151
235
|
const entry = normalizeCorrelationEntry({
|
|
152
236
|
trackingId,
|
|
@@ -159,6 +243,10 @@ export class InMemoryCorrelationStore {
|
|
|
159
243
|
this.set(entry);
|
|
160
244
|
return entry;
|
|
161
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Exposes the public tryMatchDestination operation.
|
|
248
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
249
|
+
*/
|
|
162
250
|
tryMatchDestination(trackingId, destinationTimestampUtc) {
|
|
163
251
|
const source = this.get(trackingId);
|
|
164
252
|
if (!source) {
|
|
@@ -170,6 +258,10 @@ export class InMemoryCorrelationStore {
|
|
|
170
258
|
destinationTimestampUtc
|
|
171
259
|
});
|
|
172
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Exposes the public tryExpire operation.
|
|
263
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
264
|
+
*/
|
|
173
265
|
tryExpire(eventId) {
|
|
174
266
|
const entry = this.pendingByEventId.get(eventId);
|
|
175
267
|
if (!entry) {
|
|
@@ -510,10 +602,18 @@ export class RedisCorrelationStore {
|
|
|
510
602
|
}
|
|
511
603
|
}
|
|
512
604
|
export class TrackingFieldSelector {
|
|
605
|
+
/**
|
|
606
|
+
* Exposes the public constructor operation.
|
|
607
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
608
|
+
*/
|
|
513
609
|
constructor(location, path) {
|
|
514
610
|
this.kind = normalizeTrackingFieldLocation(location);
|
|
515
611
|
this.path = String(path ?? "");
|
|
516
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Parses a selector or configuration expression.
|
|
615
|
+
* Use this when a tracking selector should be created from its text form.
|
|
616
|
+
*/
|
|
517
617
|
static parse(value) {
|
|
518
618
|
const normalized = (value ?? "").trim();
|
|
519
619
|
const [kindRaw, ...rest] = normalized.split(":");
|
|
@@ -523,9 +623,17 @@ export class TrackingFieldSelector {
|
|
|
523
623
|
}
|
|
524
624
|
return new TrackingFieldSelector(kindRaw.trim(), path);
|
|
525
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Parses a selector or configuration expression.
|
|
628
|
+
* Use this when a tracking selector should be created from its text form.
|
|
629
|
+
*/
|
|
526
630
|
static Parse(value) {
|
|
527
631
|
return TrackingFieldSelector.parse(value);
|
|
528
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* Attempts to parse a selector or configuration expression.
|
|
635
|
+
* Use this when invalid input should be handled without throwing.
|
|
636
|
+
*/
|
|
529
637
|
static tryParse(value) {
|
|
530
638
|
try {
|
|
531
639
|
return {
|
|
@@ -540,6 +648,10 @@ export class TrackingFieldSelector {
|
|
|
540
648
|
};
|
|
541
649
|
}
|
|
542
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Attempts to parse a selector or configuration expression.
|
|
653
|
+
* Use this when invalid input should be handled without throwing.
|
|
654
|
+
*/
|
|
543
655
|
static TryParse(value) {
|
|
544
656
|
return TrackingFieldSelector.tryParse(value);
|
|
545
657
|
}
|
|
@@ -549,6 +661,10 @@ export class TrackingFieldSelector {
|
|
|
549
661
|
get Path() {
|
|
550
662
|
return this.path;
|
|
551
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Extracts a value from the provided payload.
|
|
666
|
+
* Use this when a tracking selector should read data from a transport payload.
|
|
667
|
+
*/
|
|
552
668
|
extract(payload) {
|
|
553
669
|
if (this.kind === "header") {
|
|
554
670
|
const headers = payload.headers ?? {};
|
|
@@ -578,9 +694,17 @@ export class TrackingFieldSelector {
|
|
|
578
694
|
}
|
|
579
695
|
return String(current);
|
|
580
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Exposes the public toString operation.
|
|
699
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
700
|
+
*/
|
|
581
701
|
toString() {
|
|
582
702
|
return `${this.kind}:${this.path}`;
|
|
583
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Exposes the public ToString operation.
|
|
706
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
707
|
+
*/
|
|
584
708
|
ToString() {
|
|
585
709
|
return this.toString();
|
|
586
710
|
}
|
|
@@ -593,6 +717,10 @@ function normalizeTrackingFieldLocation(location) {
|
|
|
593
717
|
throw new Error("Tracking field location must be Header or Json.");
|
|
594
718
|
}
|
|
595
719
|
export class CrossPlatformTrackingRuntime {
|
|
720
|
+
/**
|
|
721
|
+
* Exposes the public constructor operation.
|
|
722
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
723
|
+
*/
|
|
596
724
|
constructor(options) {
|
|
597
725
|
this.trackingIdDisplayByEventId = new Map();
|
|
598
726
|
this.gatherByDisplayByComparisonKey = new Map();
|
|
@@ -810,6 +938,10 @@ export class CrossPlatformTrackingRuntime {
|
|
|
810
938
|
this.gatherByDisplayByComparisonKey.set(comparisonKey, gatherKey);
|
|
811
939
|
return gatherKey;
|
|
812
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Exposes the public isTimeoutCountedAsFailure operation.
|
|
943
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
944
|
+
*/
|
|
813
945
|
isTimeoutCountedAsFailure() {
|
|
814
946
|
return this.timeoutCountsAsFailure;
|
|
815
947
|
}
|
package/dist/esm/local.js
CHANGED
|
@@ -47,6 +47,10 @@ const CI_ENVIRONMENT_VARIABLES = [
|
|
|
47
47
|
"HEROKU_TEST_RUN_ID"
|
|
48
48
|
];
|
|
49
49
|
export class LoadStrikeLocalClient {
|
|
50
|
+
/**
|
|
51
|
+
* Exposes the public constructor operation.
|
|
52
|
+
* Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
|
|
53
|
+
*/
|
|
50
54
|
constructor(options = {}) {
|
|
51
55
|
this.signingKeyCache = new Map();
|
|
52
56
|
assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
|
|
@@ -113,6 +117,17 @@ export class LoadStrikeLocalClient {
|
|
|
113
117
|
const context = asRecord(request.Context);
|
|
114
118
|
const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
|
|
115
119
|
if (nodeType === "agent") {
|
|
120
|
+
const agentExecutionToken = stringOrDefault(pickValue(context, "AgentExecutionToken", "agentExecutionToken"), "").trim();
|
|
121
|
+
if (!agentExecutionToken) {
|
|
122
|
+
throw new Error("Agent node type cannot run directly. Start it from a coordinator session after controller license validation.");
|
|
123
|
+
}
|
|
124
|
+
const agentCommandId = stringOrDefault(pickValue(context, "AgentCommandId", "agentCommandId"), "").trim();
|
|
125
|
+
if (!agentCommandId) {
|
|
126
|
+
throw new Error("Agent execution is missing the coordinator command identifier.");
|
|
127
|
+
}
|
|
128
|
+
const requestedFeatures = collectRequestedFeatures(request);
|
|
129
|
+
const sessionId = stringOrDefault(pickValue(context, "SessionId", "sessionId"), "").trim();
|
|
130
|
+
await this.verifySignedAgentExecutionToken(agentExecutionToken, request, requestedFeatures, sessionId, agentCommandId, resolveTargetScenarioNames(context));
|
|
116
131
|
return {};
|
|
117
132
|
}
|
|
118
133
|
const runnerKey = stringOrDefault(context.RunnerKey, "").trim();
|
|
@@ -219,6 +234,92 @@ export class LoadStrikeLocalClient {
|
|
|
219
234
|
enforceEntitlementClaims(parsed.payload, requestedFeatures);
|
|
220
235
|
enforceRuntimePolicyClaims(parsed.payload, request);
|
|
221
236
|
}
|
|
237
|
+
async createAgentExecutionToken(controllerRunToken, sessionId, commandId, agentIndex, agentCount, targetScenarios) {
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
|
|
240
|
+
try {
|
|
241
|
+
const payload = {
|
|
242
|
+
ControllerRunToken: controllerRunToken,
|
|
243
|
+
SessionId: sessionId,
|
|
244
|
+
CommandId: commandId,
|
|
245
|
+
AgentIndex: agentIndex,
|
|
246
|
+
AgentCount: agentCount,
|
|
247
|
+
TargetScenarios: [...targetScenarios]
|
|
248
|
+
};
|
|
249
|
+
const { response, json } = await this.postLicensingRequest("/api/v1/licenses/agent-token", payload, controller.signal);
|
|
250
|
+
const agentPayload = json;
|
|
251
|
+
const isValid = pickValue(agentPayload, "IsValid", "isValid");
|
|
252
|
+
if (!response.ok || isValid !== true) {
|
|
253
|
+
const details = stringOrDefault(pickValue(agentPayload, "Message", "message"), "No additional details provided.");
|
|
254
|
+
const denialCode = stringOrDefault(pickValue(agentPayload, "DenialCode", "denialCode"), "unknown_denial");
|
|
255
|
+
throw new Error(`Agent execution authorization denied. Reason: ${denialCode}. ${details}`);
|
|
256
|
+
}
|
|
257
|
+
const agentRunToken = stringOrDefault(pickValue(agentPayload, "AgentRunToken", "agentRunToken"), "").trim();
|
|
258
|
+
if (!agentRunToken) {
|
|
259
|
+
throw new Error("Agent execution authorization failed: token is missing.");
|
|
260
|
+
}
|
|
261
|
+
return agentRunToken;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async verifySignedAgentExecutionToken(runToken, request, requestedFeatures, sessionId, commandId, targetScenarios) {
|
|
268
|
+
const parsed = parseJwt(runToken);
|
|
269
|
+
const keyId = stringOrDefault(parsed.header.kid, "default");
|
|
270
|
+
const algorithm = stringOrDefault(parsed.header.alg, "RS256").toUpperCase();
|
|
271
|
+
if (algorithm !== "RS256") {
|
|
272
|
+
throw new Error("Agent execution authorization failed: invalid token signature.");
|
|
273
|
+
}
|
|
274
|
+
const keyRecord = await this.getSigningKey(keyId, algorithm);
|
|
275
|
+
if (!verifyJwtSignature(runToken, keyRecord, algorithm)) {
|
|
276
|
+
throw new Error("Agent execution authorization failed: invalid token signature.");
|
|
277
|
+
}
|
|
278
|
+
enforceJwtLifetime(parsed.payload);
|
|
279
|
+
const tokenIssuer = stringOrDefault(parsed.payload.iss, "").trim();
|
|
280
|
+
if (!keyRecord.issuer || tokenIssuer !== keyRecord.issuer) {
|
|
281
|
+
throw new Error("Agent execution authorization failed: token issuer does not match.");
|
|
282
|
+
}
|
|
283
|
+
const rawAudience = parsed.payload.aud;
|
|
284
|
+
const tokenAudiences = Array.isArray(rawAudience)
|
|
285
|
+
? rawAudience.map((entry) => stringOrDefault(entry, "").trim()).filter((entry) => entry.length > 0)
|
|
286
|
+
: stringOrDefault(rawAudience, "").trim()
|
|
287
|
+
? [stringOrDefault(rawAudience, "").trim()]
|
|
288
|
+
: [];
|
|
289
|
+
if (!keyRecord.audience || !tokenAudiences.includes(keyRecord.audience)) {
|
|
290
|
+
throw new Error("Agent execution authorization failed: token audience does not match.");
|
|
291
|
+
}
|
|
292
|
+
const tokenKind = stringOrDefault(parsed.payload.token_kind, "").trim();
|
|
293
|
+
if (tokenKind !== "agent_execution") {
|
|
294
|
+
throw new Error("Agent execution authorization failed: token kind is invalid.");
|
|
295
|
+
}
|
|
296
|
+
const tokenNodeType = stringOrDefault(parsed.payload.node_type, "").trim().toLowerCase();
|
|
297
|
+
if (tokenNodeType !== "agent") {
|
|
298
|
+
throw new Error("Agent execution authorization failed: token node type is invalid.");
|
|
299
|
+
}
|
|
300
|
+
const tokenSessionId = stringOrDefault(parsed.payload.session_id, "").trim();
|
|
301
|
+
if (tokenSessionId !== sessionId) {
|
|
302
|
+
throw new Error("Agent execution authorization failed: session id does not match.");
|
|
303
|
+
}
|
|
304
|
+
const tokenCommandId = stringOrDefault(parsed.payload.command_id, "").trim();
|
|
305
|
+
if (tokenCommandId !== commandId) {
|
|
306
|
+
throw new Error("Agent execution authorization failed: command id does not match.");
|
|
307
|
+
}
|
|
308
|
+
const tokenTargetScenarios = collectClaimStrings(parsed.payload, "target_scenario")
|
|
309
|
+
.map((value) => value.trim())
|
|
310
|
+
.filter((value) => value.length > 0)
|
|
311
|
+
.sort();
|
|
312
|
+
const expectedTargetScenarios = [...targetScenarios]
|
|
313
|
+
.map((value) => value.trim())
|
|
314
|
+
.filter((value) => value.length > 0)
|
|
315
|
+
.sort();
|
|
316
|
+
if (tokenTargetScenarios.length !== expectedTargetScenarios.length
|
|
317
|
+
|| tokenTargetScenarios.some((value, index) => value !== expectedTargetScenarios[index])) {
|
|
318
|
+
throw new Error("Agent execution authorization failed: target scenarios do not match.");
|
|
319
|
+
}
|
|
320
|
+
enforceEntitlementClaims(parsed.payload, requestedFeatures);
|
|
321
|
+
enforceRuntimePolicyClaims(parsed.payload, request);
|
|
322
|
+
}
|
|
222
323
|
async getSigningKey(keyId, algorithm) {
|
|
223
324
|
const nowMs = Date.now();
|
|
224
325
|
const normalizedKeyId = stringOrDefault(keyId, "default").trim() || "default";
|
|
@@ -400,6 +501,7 @@ function generateSessionId() {
|
|
|
400
501
|
function collectRequestedFeatures(request) {
|
|
401
502
|
const context = asRecord(request.Context);
|
|
402
503
|
const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
|
|
504
|
+
const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
|
|
403
505
|
const features = new Set([
|
|
404
506
|
"core.runtime",
|
|
405
507
|
"reporting.formats.standard"
|
|
@@ -413,7 +515,9 @@ function collectRequestedFeatures(request) {
|
|
|
413
515
|
const hasAdvancedTargeting = normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")).length > 0
|
|
414
516
|
|| normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios")).length > 0
|
|
415
517
|
|| resolveTimeoutSeconds(context, "ClusterCommandTimeoutSeconds", "clusterCommandTimeoutSeconds", "ClusterCommandTimeoutMs", "clusterCommandTimeoutMs", 120) !== 120;
|
|
416
|
-
|
|
518
|
+
// Agent child contexts carry coordinator-assigned target slices. That internal
|
|
519
|
+
// state must not force the advanced targeting entitlement a second time.
|
|
520
|
+
if (hasAdvancedTargeting && nodeType !== "agent") {
|
|
417
521
|
features.add("cluster.targeting_and_tuning.advanced");
|
|
418
522
|
}
|
|
419
523
|
if (countCustomWorkerPlugins(context) > 0) {
|
|
@@ -1850,13 +1954,16 @@ function toContractNodeType(value) {
|
|
|
1850
1954
|
}
|
|
1851
1955
|
}
|
|
1852
1956
|
function countTargetedScenarios(context) {
|
|
1853
|
-
const values =
|
|
1854
|
-
...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
|
|
1855
|
-
...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
|
|
1856
|
-
...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
|
|
1857
|
-
];
|
|
1957
|
+
const values = resolveTargetScenarioNames(context);
|
|
1858
1958
|
return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)).size;
|
|
1859
1959
|
}
|
|
1960
|
+
function resolveTargetScenarioNames(context) {
|
|
1961
|
+
return [...new Set([
|
|
1962
|
+
...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
|
|
1963
|
+
...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
|
|
1964
|
+
...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
|
|
1965
|
+
])];
|
|
1966
|
+
}
|
|
1860
1967
|
function countCustomWorkerPlugins(context) {
|
|
1861
1968
|
const workerPlugins = asList(pickValue(context, "WorkerPlugins", "workerPlugins"));
|
|
1862
1969
|
let count = 0;
|
package/dist/esm/reporting.js
CHANGED
|
@@ -1031,6 +1031,9 @@ function buildDotnetHtmlTabs(nodeStats) {
|
|
|
1031
1031
|
}
|
|
1032
1032
|
return tabs;
|
|
1033
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Exposes the build dotnet txt report operation. Use this when interacting with the SDK through this surface.
|
|
1036
|
+
*/
|
|
1034
1037
|
export function buildDotnetTxtReport(nodeStats) {
|
|
1035
1038
|
const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
|
|
1036
1039
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
@@ -1077,6 +1080,9 @@ export function buildDotnetTxtReport(nodeStats) {
|
|
|
1077
1080
|
}
|
|
1078
1081
|
return reportLines(lines);
|
|
1079
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Exposes the build dotnet csv report operation. Use this when interacting with the SDK through this surface.
|
|
1085
|
+
*/
|
|
1080
1086
|
export function buildDotnetCsvReport(nodeStats) {
|
|
1081
1087
|
const lines = ["ScenarioName,Requests,Ok,Fail,DurationSeconds,Rps"];
|
|
1082
1088
|
for (const scenario of sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"))) {
|
|
@@ -1086,6 +1092,9 @@ export function buildDotnetCsvReport(nodeStats) {
|
|
|
1086
1092
|
}
|
|
1087
1093
|
return reportLines(lines);
|
|
1088
1094
|
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Exposes the build dotnet markdown report operation. Use this when interacting with the SDK through this surface.
|
|
1097
|
+
*/
|
|
1089
1098
|
export function buildDotnetMarkdownReport(nodeStats) {
|
|
1090
1099
|
const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
|
|
1091
1100
|
const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
|
|
@@ -1125,6 +1134,9 @@ export function buildDotnetMarkdownReport(nodeStats) {
|
|
|
1125
1134
|
}
|
|
1126
1135
|
return reportLines(lines);
|
|
1127
1136
|
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Exposes the build dotnet html report operation. Use this when interacting with the SDK through this surface.
|
|
1139
|
+
*/
|
|
1128
1140
|
export function buildDotnetHtmlReport(nodeStats) {
|
|
1129
1141
|
const tabs = buildDotnetHtmlTabs(nodeStats);
|
|
1130
1142
|
const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${tabId}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");
|