@modelprofile.com/flexharness 9.6.1 → 9.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +8 -0
- package/dist_ts/classes.flexharness.js +298 -21
- package/dist_ts/interfaces.d.ts +48 -1
- package/dist_ts/interfaces.js +2 -1
- package/dist_ts/utils.harnessoptions.d.ts +2 -1
- package/dist_ts/utils.harnessoptions.js +30 -2
- package/dist_ts/utils.json.js +10 -1
- package/dist_ts/utils.validation.d.ts +2 -0
- package/dist_ts/utils.validation.js +16 -2
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +375 -15
- package/ts/interfaces.ts +58 -0
- package/ts/utils.harnessoptions.ts +32 -2
- package/ts/utils.json.ts +12 -0
- package/ts/utils.validation.ts +19 -0
package/ts/interfaces.ts
CHANGED
|
@@ -158,6 +158,8 @@ export interface IFlexSession {
|
|
|
158
158
|
sessionId: string;
|
|
159
159
|
sessionGenerationId?: string;
|
|
160
160
|
sessionGenerationSequence?: number;
|
|
161
|
+
/** Opaque immutable configuration revision bound to this exact session generation. */
|
|
162
|
+
configurationRef?: string;
|
|
161
163
|
title?: string;
|
|
162
164
|
createdAt: string;
|
|
163
165
|
updatedAt: string;
|
|
@@ -295,6 +297,7 @@ export const FLEX_SESSION_IMPORT_MAXIMUM_LIMITS = Object.freeze({
|
|
|
295
297
|
export interface IFlexCreateSessionOptions {
|
|
296
298
|
sessionId?: string;
|
|
297
299
|
sessionGenerationId?: string;
|
|
300
|
+
configurationRef?: string;
|
|
298
301
|
title?: string;
|
|
299
302
|
}
|
|
300
303
|
|
|
@@ -529,9 +532,24 @@ export interface IFlexPermissionRequest
|
|
|
529
532
|
sessionId: string;
|
|
530
533
|
runId: string;
|
|
531
534
|
createdAt: string;
|
|
535
|
+
/** A bound policy requires an explicit response and disallows remembered approval. */
|
|
536
|
+
requiresExplicitResponse?: true;
|
|
532
537
|
}
|
|
533
538
|
|
|
534
539
|
export type TFlexPermissionDecision = 'once' | 'always' | 'reject';
|
|
540
|
+
export type TFlexSessionPermissionPolicyDecision = 'inherit' | 'allow' | 'ask' | 'deny';
|
|
541
|
+
|
|
542
|
+
export interface IFlexSessionPermissionPolicyContext<TScope> extends IFlexSessionGeneration {
|
|
543
|
+
readonly scopeId: string;
|
|
544
|
+
readonly scope: TScope;
|
|
545
|
+
readonly storageKey: string;
|
|
546
|
+
readonly sessionId: string;
|
|
547
|
+
readonly runId: string;
|
|
548
|
+
readonly configurationRef: string;
|
|
549
|
+
readonly agent?: string;
|
|
550
|
+
readonly request: Readonly<IFlexPermissionRequest>;
|
|
551
|
+
readonly signal: AbortSignal;
|
|
552
|
+
}
|
|
535
553
|
|
|
536
554
|
export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration {
|
|
537
555
|
scopeId: string;
|
|
@@ -731,6 +749,39 @@ export interface IFlexSessionGeneration {
|
|
|
731
749
|
sessionGenerationSequence: number;
|
|
732
750
|
}
|
|
733
751
|
|
|
752
|
+
export const FLEX_SESSION_CONFIGURATION_REF_MAX_BYTES = 1024;
|
|
753
|
+
|
|
754
|
+
export interface IFlexSessionConfigurationResolverContext<TScope> extends IFlexSessionGeneration {
|
|
755
|
+
readonly scopeId: string;
|
|
756
|
+
readonly scope: TScope;
|
|
757
|
+
readonly storageKey: string;
|
|
758
|
+
readonly sessionId: string;
|
|
759
|
+
readonly configurationRef: string;
|
|
760
|
+
readonly parentSessionId?: string;
|
|
761
|
+
readonly parentSessionGenerationId?: string;
|
|
762
|
+
readonly parentSessionGenerationSequence?: number;
|
|
763
|
+
readonly agent?: string;
|
|
764
|
+
readonly depth: number;
|
|
765
|
+
readonly signal: AbortSignal;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export interface IFlexResolvedSessionConfiguration<TScope> {
|
|
769
|
+
subagents?: readonly IFlexSubagentDefinition[];
|
|
770
|
+
slashCommands?: readonly TFlexSlashCommandRegistration<TScope>[];
|
|
771
|
+
/** Exact model-facing names permitted for this bound session; omitted means unrestricted. */
|
|
772
|
+
allowedTools?: readonly string[];
|
|
773
|
+
/** Evaluated for each request before a remembered grant can be reused. */
|
|
774
|
+
permissionPolicy?: (
|
|
775
|
+
context: IFlexSessionPermissionPolicyContext<TScope>,
|
|
776
|
+
) => TFlexSessionPermissionPolicyDecision | Promise<TFlexSessionPermissionPolicyDecision>;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export interface IFlexSessionConfigurationResolver<TScope> {
|
|
780
|
+
resolveSessionConfiguration(
|
|
781
|
+
context: IFlexSessionConfigurationResolverContext<TScope>,
|
|
782
|
+
): Promise<IFlexResolvedSessionConfiguration<TScope>> | IFlexResolvedSessionConfiguration<TScope>;
|
|
783
|
+
}
|
|
784
|
+
|
|
734
785
|
export interface IFlexSubagentProvenance {
|
|
735
786
|
readonly parentSessionId: string;
|
|
736
787
|
readonly parentSessionGenerationId: string;
|
|
@@ -1107,6 +1158,8 @@ export interface IFlexRedoSessionResult {
|
|
|
1107
1158
|
|
|
1108
1159
|
export type TFlexExternalErrorSource =
|
|
1109
1160
|
| 'modelResolver'
|
|
1161
|
+
| 'sessionConfigurationResolver'
|
|
1162
|
+
| 'permissionPolicy'
|
|
1110
1163
|
| 'toolProvider'
|
|
1111
1164
|
| 'delegatedRunAdmissionProvider'
|
|
1112
1165
|
| 'toolExecution'
|
|
@@ -1237,6 +1290,8 @@ export interface IFlexSubagentDefinition {
|
|
|
1237
1290
|
modelHint?: string;
|
|
1238
1291
|
system?: string;
|
|
1239
1292
|
maxSteps?: number;
|
|
1293
|
+
/** Exact model-facing names permitted for this agent; omitted means unrestricted. */
|
|
1294
|
+
allowedTools?: readonly string[];
|
|
1240
1295
|
}
|
|
1241
1296
|
|
|
1242
1297
|
export interface IFlexProjectManagementBuiltInToolsOptions {
|
|
@@ -1253,6 +1308,7 @@ export interface IFlexBuiltInToolsOptions {
|
|
|
1253
1308
|
export interface IFlexHarnessOptions<TScope> {
|
|
1254
1309
|
scopeResolver: IFlexScopeResolver<TScope>;
|
|
1255
1310
|
modelResolver: IFlexModelResolver<TScope>;
|
|
1311
|
+
sessionConfigurationResolver?: IFlexSessionConfigurationResolver<TScope>;
|
|
1256
1312
|
toolProvider?: IFlexToolProvider<TScope>;
|
|
1257
1313
|
resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
|
|
1258
1314
|
executionContextProvider?: IFlexExecutionContextProvider<TScope>;
|
|
@@ -1388,6 +1444,8 @@ export interface IFlexPermissionResolvedEvent extends IFlexEventBase {
|
|
|
1388
1444
|
readonly runId: string;
|
|
1389
1445
|
readonly request: IFlexPermissionRequest;
|
|
1390
1446
|
readonly decision: TFlexPermissionDecision;
|
|
1447
|
+
/** Present when a bound policy resolved the request without making it pending. */
|
|
1448
|
+
readonly origin?: 'policy';
|
|
1391
1449
|
}
|
|
1392
1450
|
|
|
1393
1451
|
export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
@@ -54,14 +54,41 @@ export const maxSubagentNameBytes = 128;
|
|
|
54
54
|
const maxSubagentDescriptionBytes = 2048;
|
|
55
55
|
const maxSubagentModelHintBytes = 512;
|
|
56
56
|
const maxSubagentSystemBytes = 64 * 1024;
|
|
57
|
+
const maxAllowedToolNames = 128;
|
|
58
|
+
const maxAllowedToolNameBytes = 512;
|
|
59
|
+
const toolNameControlPattern = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
60
|
+
const toolNameUnpairedSurrogatePattern = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u;
|
|
57
61
|
const maxSlashCommandRegistrations = 128;
|
|
58
62
|
const maxSlashCommandDescriptionBytes = 2048;
|
|
59
63
|
const maxSlashCommandTemplateBytes = FLEX_SLASH_COMMAND_MAX_INPUT_BYTES;
|
|
60
64
|
const defaultReversionMaintenanceTimeoutMs = 30_000;
|
|
61
65
|
const maximumReversionMaintenanceTimeoutMs = 30 * 60 * 1000;
|
|
62
66
|
|
|
67
|
+
export function normalizeAllowedTools(
|
|
68
|
+
names: readonly string[] | undefined,
|
|
69
|
+
label = 'allowedTools',
|
|
70
|
+
): readonly string[] | undefined {
|
|
71
|
+
if (names === undefined) return undefined;
|
|
72
|
+
if (!Array.isArray(names) || names.length > maxAllowedToolNames) {
|
|
73
|
+
throw new FlexHarnessValidationError(`${label} must be an array with at most ${maxAllowedToolNames} names.`);
|
|
74
|
+
}
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
const normalized: string[] = [];
|
|
77
|
+
for (let index = 0; index < names.length; index++) {
|
|
78
|
+
const name = names[index];
|
|
79
|
+
validateUtf8String(name, `${label}[${index}]`, maxAllowedToolNameBytes, true);
|
|
80
|
+
if (toolNameControlPattern.test(name) || toolNameUnpairedSurrogatePattern.test(name)) {
|
|
81
|
+
throw new FlexHarnessValidationError(`${label}[${index}] must be a well-formed name without control characters.`);
|
|
82
|
+
}
|
|
83
|
+
if (seen.has(name)) throw new FlexHarnessValidationError(`${label} contains duplicate name "${name}".`);
|
|
84
|
+
seen.add(name);
|
|
85
|
+
normalized.push(name);
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze(normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
export function normalizeSubagents(
|
|
64
|
-
definitions: IFlexSubagentDefinition[] | undefined,
|
|
91
|
+
definitions: readonly IFlexSubagentDefinition[] | undefined,
|
|
65
92
|
): ReadonlyMap<string, Readonly<IFlexSubagentDefinition>> {
|
|
66
93
|
if (definitions === undefined) return new Map();
|
|
67
94
|
if (!Array.isArray(definitions) || definitions.length > maxSubagentDefinitions) {
|
|
@@ -81,7 +108,7 @@ export function normalizeSubagents(
|
|
|
81
108
|
throw new FlexHarnessValidationError(`subagents[${index}] must be a plain object.`);
|
|
82
109
|
}
|
|
83
110
|
const unsupported = Object.keys(definition).find(
|
|
84
|
-
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps'].includes(key),
|
|
111
|
+
(key) => !['name', 'description', 'modelHint', 'system', 'maxSteps', 'allowedTools'].includes(key),
|
|
85
112
|
);
|
|
86
113
|
if (unsupported) {
|
|
87
114
|
throw new FlexHarnessValidationError(`subagents[${index}] does not support "${unsupported}".`);
|
|
@@ -121,6 +148,9 @@ export function normalizeSubagents(
|
|
|
121
148
|
...(definition.modelHint === undefined ? {} : { modelHint: definition.modelHint }),
|
|
122
149
|
...(definition.system === undefined ? {} : { system: definition.system }),
|
|
123
150
|
...(definition.maxSteps === undefined ? {} : { maxSteps: definition.maxSteps }),
|
|
151
|
+
...(definition.allowedTools === undefined ? {} : {
|
|
152
|
+
allowedTools: normalizeAllowedTools(definition.allowedTools, `subagents[${index}].allowedTools`),
|
|
153
|
+
}),
|
|
124
154
|
}));
|
|
125
155
|
}
|
|
126
156
|
return Object.freeze(normalized);
|
package/ts/utils.json.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
TFlexAgentModelMessage,
|
|
21
21
|
TJsonValue,
|
|
22
22
|
} from './interfaces.js';
|
|
23
|
+
import { isValidSessionConfigurationRef } from './utils.validation.js';
|
|
23
24
|
|
|
24
25
|
const DEFAULT_MAX_DEPTH = 12;
|
|
25
26
|
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
@@ -483,6 +484,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
483
484
|
'sessionId',
|
|
484
485
|
'sessionGenerationId',
|
|
485
486
|
'sessionGenerationSequence',
|
|
487
|
+
'configurationRef',
|
|
486
488
|
'title',
|
|
487
489
|
'createdAt',
|
|
488
490
|
'updatedAt',
|
|
@@ -502,6 +504,9 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
502
504
|
if (session.origin !== undefined) validateSessionOrigin(session.origin, `${path}.origin`);
|
|
503
505
|
const sessionId = requireString(session.sessionId, `${path}.sessionId`);
|
|
504
506
|
const generation = validateOptionalSessionGeneration(session, path);
|
|
507
|
+
if (session.configurationRef !== undefined && !isValidSessionConfigurationRef(session.configurationRef)) {
|
|
508
|
+
throw new FlexHarnessStoreFormatError(`${path}.configurationRef is invalid.`);
|
|
509
|
+
}
|
|
505
510
|
requireOptionalString(session.title, `${path}.title`);
|
|
506
511
|
requireString(session.createdAt, `${path}.createdAt`);
|
|
507
512
|
requireString(session.updatedAt, `${path}.updatedAt`);
|
|
@@ -545,6 +550,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
545
550
|
return {
|
|
546
551
|
sessionId,
|
|
547
552
|
...generation,
|
|
553
|
+
...(session.configurationRef === undefined ? {} : { configurationRef: session.configurationRef as string }),
|
|
548
554
|
...(relationshipCount === 0
|
|
549
555
|
? {}
|
|
550
556
|
: {
|
|
@@ -689,6 +695,7 @@ interface IValidatedSessionIdentity {
|
|
|
689
695
|
sessionId: string;
|
|
690
696
|
sessionGenerationId?: string;
|
|
691
697
|
sessionGenerationSequence?: number;
|
|
698
|
+
configurationRef?: string;
|
|
692
699
|
parentSessionId?: string;
|
|
693
700
|
parentRunId?: string;
|
|
694
701
|
parentToolCallId?: string;
|
|
@@ -889,6 +896,11 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
889
896
|
`Snapshot child session "${session.sessionId}" has no live parent "${session.parentSessionId}".`,
|
|
890
897
|
);
|
|
891
898
|
}
|
|
899
|
+
if (session.configurationRef !== parent.configurationRef) {
|
|
900
|
+
throw new FlexHarnessStoreFormatError(
|
|
901
|
+
`Snapshot child session "${session.sessionId}" has a different configurationRef from its parent.`,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
892
904
|
const expectedDepth = (parent.depth ?? 0) + 1;
|
|
893
905
|
if (session.depth !== expectedDepth) {
|
|
894
906
|
throw new FlexHarnessStoreFormatError(
|
package/ts/utils.validation.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
5
5
|
FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES,
|
|
6
6
|
FLEX_SESSION_GENERATION_ID_MAX_BYTES,
|
|
7
|
+
FLEX_SESSION_CONFIGURATION_REF_MAX_BYTES,
|
|
7
8
|
} from './interfaces.js';
|
|
8
9
|
import type {
|
|
9
10
|
IFlexCompactSessionOptions,
|
|
@@ -23,6 +24,24 @@ const maxProjectedErrorNameBytes = 128;
|
|
|
23
24
|
const maxProjectedErrorMessageBytes = 2048;
|
|
24
25
|
const maxProjectedErrorCodeBytes = 128;
|
|
25
26
|
const controlCharacterPattern = /[\u0000-\u001f\u007f]/u;
|
|
27
|
+
const configurationControlCharacterPattern = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
28
|
+
const unpairedSurrogatePattern = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u;
|
|
29
|
+
|
|
30
|
+
export function isValidSessionConfigurationRef(value: unknown): value is string {
|
|
31
|
+
return typeof value === 'string'
|
|
32
|
+
&& Boolean(value.trim())
|
|
33
|
+
&& Buffer.byteLength(value, 'utf8') <= FLEX_SESSION_CONFIGURATION_REF_MAX_BYTES
|
|
34
|
+
&& !configurationControlCharacterPattern.test(value)
|
|
35
|
+
&& !unpairedSurrogatePattern.test(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateSessionConfigurationRef(value: unknown): asserts value is string {
|
|
39
|
+
if (!isValidSessionConfigurationRef(value)) {
|
|
40
|
+
throw new FlexHarnessValidationError(
|
|
41
|
+
`configurationRef must be a non-empty well-formed string without control characters of at most ${FLEX_SESSION_CONFIGURATION_REF_MAX_BYTES} UTF-8 bytes.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
|
|
27
46
|
export function validateIdentifier(value: string, name: string): void {
|
|
28
47
|
if (typeof value !== 'string' || value.length === 0) {
|