@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/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(
@@ -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) {