@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.
@@ -79,6 +79,7 @@ import type {
79
79
  IFlexPendingReversionRelease,
80
80
  IFlexRedoSessionResult,
81
81
  IFlexResolvedModel,
82
+ IFlexResolvedSessionConfiguration,
82
83
  IFlexResolvedScope,
83
84
  IFlexResourceToolProviderDescriptor,
84
85
  IFlexRunFinishedEvent,
@@ -86,6 +87,7 @@ import type {
86
87
  IFlexSchedulePromptOptions,
87
88
  IFlexScopeSnapshot,
88
89
  IFlexSession,
90
+ IFlexSessionPermissionPolicyContext,
89
91
  IFlexSessionActivity,
90
92
  IFlexSessionGeneration,
91
93
  IFlexSessionGenerationCohortEntry,
@@ -154,6 +156,7 @@ import {
154
156
  } from './utils.prompt.js';
155
157
  import {
156
158
  maxSubagentNameBytes,
159
+ normalizeAllowedTools,
157
160
  normalizeAgentSessionPolicy,
158
161
  normalizeBuiltInTools,
159
162
  normalizeSlashCommands,
@@ -212,6 +215,7 @@ import {
212
215
  validatePromptOptions,
213
216
  validateSlashCommandExecutionOptions,
214
217
  validateSessionGenerationId,
218
+ validateSessionConfigurationRef,
215
219
  validateUpdateProjectTaskInput,
216
220
  validateUpdateSessionOptions,
217
221
  validateUtf8String,
@@ -396,6 +400,10 @@ interface IActiveRun {
396
400
  reservedUserMessage?: IFlexMessage;
397
401
  reservedAssistantMessage?: IFlexMessage;
398
402
  modelResolution?: IFlexResolvedModel;
403
+ subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
404
+ allowedTools?: readonly string[];
405
+ agentAllowedTools?: readonly string[];
406
+ permissionPolicy?: IFlexResolvedSessionConfiguration<unknown>['permissionPolicy'];
399
407
  delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
400
408
  delegatedRunAdmissionClose?: Promise<void>;
401
409
  completion: Promise<IFlexPromptResult>;
@@ -511,6 +519,22 @@ interface IActiveSlashCommandListing {
511
519
  completion: Promise<unknown>;
512
520
  }
513
521
 
522
+ interface IResolvedSessionConfiguration<TScope> {
523
+ subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
524
+ slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
525
+ allowedTools?: readonly string[];
526
+ permissionPolicy?: IFlexResolvedSessionConfiguration<TScope>['permissionPolicy'];
527
+ }
528
+
529
+ interface ISessionConfigurationOwner<TScope> {
530
+ storageKey: string;
531
+ sessionId: string;
532
+ controller: AbortController;
533
+ completion: Promise<IResolvedSessionConfiguration<TScope>>;
534
+ settled: boolean;
535
+ waiters: number;
536
+ }
537
+
514
538
  interface ISlashCommandInvocationOwner {
515
539
  scopeId: string;
516
540
  storageKey: string;
@@ -743,6 +767,7 @@ function containsStoreCommitUncertainty(error: unknown, seen = new Set<unknown>(
743
767
  export class FlexHarness<TScope = unknown> {
744
768
  private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
745
769
  private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
770
+ private readonly sessionConfigurationResolver: IFlexHarnessOptions<TScope>['sessionConfigurationResolver'];
746
771
  private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
747
772
  private readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
748
773
  private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
@@ -764,6 +789,7 @@ export class FlexHarness<TScope = unknown> {
764
789
  private readonly maxSubagentDepth: number;
765
790
  private readonly maxSubagentCallsPerRun: number;
766
791
  private readonly stateLoads = new Map<string, Promise<IStorageState>>();
792
+ private readonly sessionConfigurations = new Map<string, ISessionConfigurationOwner<TScope>>();
767
793
  private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
768
794
  private readonly scopeRetirements = new Map<string, Promise<void>>();
769
795
  private readonly storageDrains = new Map<string, Promise<void>>();
@@ -810,6 +836,7 @@ export class FlexHarness<TScope = unknown> {
810
836
  }
811
837
  this.scopeResolver = options.scopeResolver;
812
838
  this.modelResolver = options.modelResolver;
839
+ this.sessionConfigurationResolver = options.sessionConfigurationResolver;
813
840
  this.toolProvider = options.toolProvider;
814
841
  this.resourceToolProviderResolver = options.resourceToolProviderResolver;
815
842
  this.executionContextProvider = options.executionContextProvider;
@@ -878,13 +905,14 @@ export class FlexHarness<TScope = unknown> {
878
905
  throw new FlexHarnessValidationError('createSession options must be a plain object.');
879
906
  }
880
907
  const unsupported = Object.keys(options).find(
881
- (key) => !['sessionId', 'sessionGenerationId', 'title'].includes(key),
908
+ (key) => !['sessionId', 'sessionGenerationId', 'configurationRef', 'title'].includes(key),
882
909
  );
883
910
  if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
884
911
  const requestedSessionGenerationId = options.sessionGenerationId;
885
912
  if (requestedSessionGenerationId !== undefined) {
886
913
  validateSessionGenerationId(requestedSessionGenerationId);
887
914
  }
915
+ if (options.configurationRef !== undefined) validateSessionConfigurationRef(options.configurationRef);
888
916
  const resolved = await this.resolveState(scopeId);
889
917
  const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
890
918
  validateIdentifier(sessionId, 'sessionId');
@@ -933,6 +961,7 @@ export class FlexHarness<TScope = unknown> {
933
961
  sessionId,
934
962
  sessionGenerationId: requestedSessionGenerationId ?? createSessionGenerationId(),
935
963
  sessionGenerationSequence: resolved.state.revision + 1,
964
+ ...(options.configurationRef === undefined ? {} : { configurationRef: options.configurationRef }),
936
965
  ...(options.title ? { title: options.title } : {}),
937
966
  createdAt: timestamp,
938
967
  updatedAt: timestamp,
@@ -1205,6 +1234,9 @@ export class FlexHarness<TScope = unknown> {
1205
1234
  sessionId,
1206
1235
  sessionGenerationId: createSessionGenerationId(),
1207
1236
  sessionGenerationSequence: state.revision + 1,
1237
+ ...(parent?.session.configurationRef === undefined
1238
+ ? {}
1239
+ : { configurationRef: parent.session.configurationRef }),
1208
1240
  ...(options.title === undefined ? {} : { title: options.title }),
1209
1241
  createdAt: options.createdAt,
1210
1242
  updatedAt: options.updatedAt,
@@ -1800,6 +1832,12 @@ export class FlexHarness<TScope = unknown> {
1800
1832
  ): Promise<void> {
1801
1833
  const { deletedSessions, descendantRoots } = reservation;
1802
1834
  const reason = this.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
1835
+ const configurationSettlement = this.abortSessionConfigurations(
1836
+ state.storageKey,
1837
+ reason,
1838
+ new Set(deletedSessions.map((deleted) => deleted.sessionId)),
1839
+ );
1840
+ if (configurationSettlement) await configurationSettlement;
1803
1841
  const slashCommandErrors: unknown[] = [];
1804
1842
  for (const deleted of deletedSessions) {
1805
1843
  const listingSettlement = this.abortSlashCommandListings(
@@ -1940,16 +1978,165 @@ export class FlexHarness<TScope = unknown> {
1940
1978
  return publicSnapshot(createBoundedTransferMessage(this.requireMessage(stored, messageId)));
1941
1979
  }
1942
1980
 
1981
+ private async resolveSessionConfiguration(
1982
+ scopeId: string,
1983
+ scope: TScope,
1984
+ state: IStorageState,
1985
+ stored: IStoredSessionState,
1986
+ signal?: AbortSignal,
1987
+ ): Promise<IResolvedSessionConfiguration<TScope>> {
1988
+ const configurationRef = stored.session.configurationRef;
1989
+ if (configurationRef === undefined) {
1990
+ return { subagents: this.subagents, slashCommands: this.slashCommands };
1991
+ }
1992
+ const sessionId = stored.session.sessionId;
1993
+ const generation = requireSessionGeneration(stored.session);
1994
+ const key = JSON.stringify([
1995
+ state.storageKey,
1996
+ sessionId,
1997
+ generation.sessionGenerationId,
1998
+ generation.sessionGenerationSequence,
1999
+ configurationRef,
2000
+ ]);
2001
+ let owner = this.sessionConfigurations.get(key);
2002
+ if (!owner) {
2003
+ const controller = new AbortController();
2004
+ const parent = stored.session.parentSessionId === undefined
2005
+ ? undefined
2006
+ : state.sessions.get(stored.session.parentSessionId);
2007
+ if (stored.session.parentSessionId !== undefined && !parent) {
2008
+ throw this.trustInternalError(new FlexHarnessAbortError('The session parent is no longer available.'));
2009
+ }
2010
+ const parentGeneration = parent === undefined ? undefined : requireSessionGeneration(parent.session);
2011
+ const context = Object.freeze({
2012
+ scopeId,
2013
+ scope,
2014
+ storageKey: state.storageKey,
2015
+ sessionId,
2016
+ ...generation,
2017
+ configurationRef,
2018
+ ...(parent === undefined ? {} : {
2019
+ parentSessionId: parent.session.sessionId,
2020
+ parentSessionGenerationId: parentGeneration!.sessionGenerationId,
2021
+ parentSessionGenerationSequence: parentGeneration!.sessionGenerationSequence,
2022
+ }),
2023
+ ...(stored.session.agent === undefined ? {} : { agent: stored.session.agent }),
2024
+ depth: stored.session.depth ?? 0,
2025
+ signal: controller.signal,
2026
+ });
2027
+ let rejectAbort!: (reason: unknown) => void;
2028
+ const aborted = new Promise<never>((_resolve, reject) => { rejectAbort = reject; });
2029
+ const onAbort = () => rejectAbort(controller.signal.reason);
2030
+ controller.signal.addEventListener('abort', onAbort, { once: true });
2031
+ const resolved = Promise.resolve().then(() => {
2032
+ if (!this.sessionConfigurationResolver) {
2033
+ throw new Error('A sessionConfigurationResolver is required for bound sessions.');
2034
+ }
2035
+ return this.sessionConfigurationResolver.resolveSessionConfiguration(context);
2036
+ });
2037
+ const completion = Promise.race([resolved, aborted]).then((configuration) => {
2038
+ controller.signal.throwIfAborted();
2039
+ if (
2040
+ state.lifecycle !== 'active'
2041
+ || state.sessions.get(sessionId) !== stored
2042
+ || stored.session.configurationRef !== configurationRef
2043
+ || stored.session.sessionGenerationId !== generation.sessionGenerationId
2044
+ || stored.session.sessionGenerationSequence !== generation.sessionGenerationSequence
2045
+ ) {
2046
+ throw this.trustInternalError(new FlexHarnessAbortError(
2047
+ 'The session configuration lost its session generation ownership.',
2048
+ ));
2049
+ }
2050
+ if (
2051
+ !configuration
2052
+ || typeof configuration !== 'object'
2053
+ || Array.isArray(configuration)
2054
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(configuration))
2055
+ || Reflect.ownKeys(configuration).some((name) =>
2056
+ typeof name !== 'string' || !['subagents', 'slashCommands', 'allowedTools', 'permissionPolicy'].includes(name))
2057
+ ) throw new FlexHarnessValidationError('Resolved session configuration is invalid.');
2058
+ if (configuration.permissionPolicy !== undefined && typeof configuration.permissionPolicy !== 'function') {
2059
+ throw new FlexHarnessValidationError('Resolved session permissionPolicy must be a function.');
2060
+ }
2061
+ return Object.freeze({
2062
+ subagents: normalizeSubagents(configuration.subagents),
2063
+ slashCommands: normalizeSlashCommands(configuration.slashCommands),
2064
+ allowedTools: normalizeAllowedTools(configuration.allowedTools),
2065
+ permissionPolicy: configuration.permissionPolicy,
2066
+ });
2067
+ }).catch((error: unknown) => {
2068
+ if (this.sessionConfigurations.get(key)?.completion === completion) {
2069
+ this.sessionConfigurations.delete(key);
2070
+ }
2071
+ if (controller.signal.aborted) throw controller.signal.reason;
2072
+ throw this.projectOperationError(
2073
+ error,
2074
+ 'sessionConfigurationResolver',
2075
+ scopeId,
2076
+ sessionId,
2077
+ 'session-configuration',
2078
+ );
2079
+ }).finally(() => {
2080
+ controller.signal.removeEventListener('abort', onAbort);
2081
+ const current = this.sessionConfigurations.get(key);
2082
+ if (current?.completion === completion) current.settled = true;
2083
+ });
2084
+ owner = { storageKey: state.storageKey, sessionId, controller, completion, settled: false, waiters: 0 };
2085
+ this.sessionConfigurations.set(key, owner);
2086
+ }
2087
+ owner.waiters++;
2088
+ let onConsumerAbort: (() => void) | undefined;
2089
+ const consumerAbort = signal && new Promise<never>((_resolve, reject) => {
2090
+ onConsumerAbort = () => reject(signal.reason ?? this.trustInternalError(
2091
+ new FlexHarnessAbortError('Session configuration resolution was aborted.'),
2092
+ ));
2093
+ signal.addEventListener('abort', onConsumerAbort, { once: true });
2094
+ if (signal.aborted) onConsumerAbort();
2095
+ });
2096
+ try {
2097
+ return await (consumerAbort
2098
+ ? Promise.race([owner.completion, consumerAbort])
2099
+ : owner.completion);
2100
+ } finally {
2101
+ if (signal && onConsumerAbort) signal.removeEventListener('abort', onConsumerAbort);
2102
+ owner.waiters--;
2103
+ if (owner.waiters === 0 && !owner.settled && !owner.controller.signal.aborted) {
2104
+ if (this.sessionConfigurations.get(key) === owner) this.sessionConfigurations.delete(key);
2105
+ owner.controller.abort(this.trustInternalError(new FlexHarnessAbortError(
2106
+ 'Session configuration resolution has no active callers.',
2107
+ )));
2108
+ }
2109
+ }
2110
+ }
2111
+
2112
+ private abortSessionConfigurations(
2113
+ storageKey: string,
2114
+ reason: FlexHarnessAbortError,
2115
+ sessionIds?: ReadonlySet<string>,
2116
+ ): Promise<void> | undefined {
2117
+ const owners = [...this.sessionConfigurations.entries()].filter(([, owner]) =>
2118
+ owner.storageKey === storageKey && (sessionIds === undefined || sessionIds.has(owner.sessionId)));
2119
+ if (owners.length === 0) return undefined;
2120
+ for (const [key, owner] of owners) {
2121
+ this.sessionConfigurations.delete(key);
2122
+ if (!owner.controller.signal.aborted) owner.controller.abort(reason);
2123
+ }
2124
+ return Promise.allSettled(owners.map(([, owner]) => owner.completion)).then(() => undefined);
2125
+ }
2126
+
1943
2127
  public async listSlashCommands(
1944
2128
  scopeId: string,
1945
2129
  sessionId: string,
1946
2130
  ): Promise<IFlexSlashCommandDescriptor[]> {
1947
- const { state } = await this.resolveState(scopeId);
2131
+ const { scope, state } = await this.resolveState(scopeId);
1948
2132
  return this.trackSlashCommandListing(scopeId, state, sessionId, async (signal) => {
1949
2133
  await this.awaitReversionOperation(state.scopeQueue, signal);
1950
2134
  signal.throwIfAborted();
1951
2135
  this.assertStateAcceptingWork(state);
1952
2136
  const stored = this.requireSession(state, sessionId);
2137
+ const configuration = await this.resolveSessionConfiguration(
2138
+ scopeId, scope.scope, state, stored, signal,
2139
+ );
1953
2140
  await this.reconcileContextAvailability(state, stored);
1954
2141
  signal.throwIfAborted();
1955
2142
  const sessionAvailable = this.isSessionAvailableForSlashCommand(state, stored);
@@ -1965,6 +2152,7 @@ export class FlexHarness<TScope = unknown> {
1965
2152
  stored,
1966
2153
  sessionAvailable,
1967
2154
  reversionIdleReason,
2155
+ configuration.slashCommands,
1968
2156
  );
1969
2157
  if (stored.session.origin === undefined) return descriptors;
1970
2158
  return publicSnapshot(descriptors.map((descriptor): IFlexSlashCommandDescriptor => ({
@@ -1980,6 +2168,7 @@ export class FlexHarness<TScope = unknown> {
1980
2168
  stored: IStoredSessionState,
1981
2169
  sessionAvailable: boolean,
1982
2170
  reversionIdleReason?: string,
2171
+ slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>> = this.slashCommands,
1983
2172
  ): IFlexSlashCommandDescriptor[] {
1984
2173
  const sessionAvailability = sessionAvailable
1985
2174
  ? {}
@@ -2058,7 +2247,7 @@ export class FlexHarness<TScope = unknown> {
2058
2247
  workspaceReversion: workspaceReversion(redoUnit),
2059
2248
  },
2060
2249
  ];
2061
- for (const registration of this.slashCommands.values()) {
2250
+ for (const registration of slashCommands.values()) {
2062
2251
  const template = typeof registration.template === 'string';
2063
2252
  const availability = template
2064
2253
  ? this.slashCommandPromptAvailability(
@@ -2087,6 +2276,26 @@ export class FlexHarness<TScope = unknown> {
2087
2276
  sessionId: string,
2088
2277
  untouchedInput: string,
2089
2278
  options: IFlexSlashCommandExecutionOptions = {},
2279
+ ): Promise<TFlexSlashCommandExecutionResult> {
2280
+ return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, false);
2281
+ }
2282
+
2283
+ /** Resolves the exact session configuration before looking up custom commands. */
2284
+ public async executeSessionSlashCommand(
2285
+ scopeId: string,
2286
+ sessionId: string,
2287
+ untouchedInput: string,
2288
+ options: IFlexSlashCommandExecutionOptions = {},
2289
+ ): Promise<TFlexSlashCommandExecutionResult> {
2290
+ return this.executeSlashCommandInternal(scopeId, sessionId, untouchedInput, options, true);
2291
+ }
2292
+
2293
+ private async executeSlashCommandInternal(
2294
+ scopeId: string,
2295
+ sessionId: string,
2296
+ untouchedInput: string,
2297
+ options: IFlexSlashCommandExecutionOptions,
2298
+ sessionAware: boolean,
2090
2299
  ): Promise<TFlexSlashCommandExecutionResult> {
2091
2300
  const currentInvocation = this.slashCommandInvocationContext.getStore();
2092
2301
  if (currentInvocation?.scopeId === scopeId && currentInvocation.sessionId === sessionId) {
@@ -2094,8 +2303,8 @@ export class FlexHarness<TScope = unknown> {
2094
2303
  }
2095
2304
  const parsed = parseSlashCommand(untouchedInput);
2096
2305
  if (parsed.type !== 'parsed') return parsed;
2097
- const registration = this.slashCommands.get(parsed.name);
2098
- if (!isReservedSlashCommandName(parsed.name) && !registration) {
2306
+ let registration = sessionAware ? undefined : this.slashCommands.get(parsed.name);
2307
+ if (!sessionAware && !isReservedSlashCommandName(parsed.name) && !registration) {
2099
2308
  return Object.freeze({
2100
2309
  type: 'unknown',
2101
2310
  input: parsed.input,
@@ -2111,6 +2320,31 @@ export class FlexHarness<TScope = unknown> {
2111
2320
  this.assertStateAcceptingWork(state);
2112
2321
  const stored = this.requireSession(state, sessionId);
2113
2322
  this.assertSessionWritable(stored);
2323
+ if (sessionAware) {
2324
+ const configuration = await this.resolveSessionConfiguration(
2325
+ scopeId, scope.scope, state, stored, options.signal,
2326
+ );
2327
+ this.assertStateAcceptingWork(state);
2328
+ if (state.sessions.get(sessionId) !== stored) {
2329
+ throw this.trustInternalError(new FlexHarnessAbortError(
2330
+ 'The session was deleted during slash command resolution.',
2331
+ ));
2332
+ }
2333
+ registration = configuration.slashCommands.get(parsed.name);
2334
+ if (!isReservedSlashCommandName(parsed.name) && !registration) {
2335
+ return Object.freeze({
2336
+ type: 'unknown',
2337
+ input: parsed.input,
2338
+ name: parsed.name,
2339
+ rawArguments: parsed.rawArguments,
2340
+ arguments: parsed.arguments,
2341
+ });
2342
+ }
2343
+ } else if (stored.session.configurationRef !== undefined) {
2344
+ throw new FlexHarnessValidationError(
2345
+ 'A bound session requires executeSessionSlashCommand().',
2346
+ );
2347
+ }
2114
2348
  const sessionKey = this.slashCommandSessionKey(state.storageKey, sessionId);
2115
2349
  const invocationOwner = this.slashCommandInvocationContext.getStore();
2116
2350
  if (invocationOwner?.sessionKey === sessionKey) {
@@ -3733,6 +3967,7 @@ export class FlexHarness<TScope = unknown> {
3733
3967
 
3734
3968
  private createActiveRun(queued: IQueuedPrompt): IActiveRun {
3735
3969
  return {
3970
+ subagents: this.subagents,
3736
3971
  state: queued.state,
3737
3972
  stored: queued.stored,
3738
3973
  originCompactorContext: this.createCompactorContext(
@@ -4959,6 +5194,22 @@ export class FlexHarness<TScope = unknown> {
4959
5194
  signal: AbortSignal,
4960
5195
  ): Promise<plugins.IAgentGenerationLease> {
4961
5196
  signal.throwIfAborted();
5197
+ const configuration = await this.resolveSessionConfiguration(
5198
+ run.scopeId,
5199
+ run.scope as TScope,
5200
+ run.state,
5201
+ run.stored,
5202
+ signal,
5203
+ );
5204
+ run.subagents = configuration.subagents;
5205
+ run.allowedTools = configuration.allowedTools;
5206
+ run.permissionPolicy = configuration.permissionPolicy as IActiveRun['permissionPolicy'];
5207
+ const agent = run.stored.session.agent;
5208
+ if (agent !== undefined) {
5209
+ const definition = configuration.subagents.get(agent);
5210
+ if (!definition) throw new FlexHarnessValidationError(`Subagent "${agent}" is disabled.`);
5211
+ run.agentAllowedTools = definition.allowedTools;
5212
+ }
4962
5213
  await this.prepareRunReversion(run, signal);
4963
5214
  run.phase = 'running';
4964
5215
  const queued = run.stored.outstandingPromptsById.get(run.queueId);
@@ -5069,9 +5320,16 @@ export class FlexHarness<TScope = unknown> {
5069
5320
  const providedTools = toolHandle?.tools;
5070
5321
  const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
5071
5322
  Object.assign(combinedTools, this.createBuiltInTools(run));
5072
- tools = Object.keys(combinedTools).length > 0
5323
+ const globalNames = run.allowedTools === undefined ? undefined : new Set(run.allowedTools);
5324
+ const agentNames = run.agentAllowedTools === undefined ? undefined : new Set(run.agentAllowedTools);
5325
+ const admittedTools = globalNames === undefined && agentNames === undefined
5326
+ ? combinedTools
5327
+ : Object.fromEntries(Object.entries(combinedTools).filter(([name]) =>
5328
+ (globalNames === undefined || globalNames.has(name))
5329
+ && (agentNames === undefined || agentNames.has(name))));
5330
+ tools = Object.keys(admittedTools).length > 0
5073
5331
  ? wrapToolSet(
5074
- combinedTools as TFlexAgentToolSet,
5332
+ admittedTools as TFlexAgentToolSet,
5075
5333
  this.toolOutputLimits,
5076
5334
  (toolError) => this.projectExternalError(run, toolError, 'toolExecution'),
5077
5335
  )
@@ -5121,7 +5379,7 @@ export class FlexHarness<TScope = unknown> {
5121
5379
 
5122
5380
  private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
5123
5381
  const names = new Set<string>();
5124
- if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5382
+ if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5125
5383
  names.add('delegate');
5126
5384
  }
5127
5385
  if (this.builtInTools.renameSession) names.add('rename_session');
@@ -5144,7 +5402,7 @@ export class FlexHarness<TScope = unknown> {
5144
5402
 
5145
5403
  private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
5146
5404
  const tools: Record<string, unknown> = {};
5147
- if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5405
+ if (run.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
5148
5406
  tools.delegate = this.createSubagentTool(run);
5149
5407
  }
5150
5408
  if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
@@ -5475,7 +5733,7 @@ export class FlexHarness<TScope = unknown> {
5475
5733
  }
5476
5734
 
5477
5735
  private createSubagentTool(run: IActiveRun): unknown {
5478
- const available = [...this.subagents.values()]
5736
+ const available = [...run.subagents.values()]
5479
5737
  .map((definition) => `- ${definition.name}: ${definition.description}`)
5480
5738
  .join('\n');
5481
5739
  return plugins.tool({
@@ -5527,7 +5785,7 @@ export class FlexHarness<TScope = unknown> {
5527
5785
  validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
5528
5786
  }
5529
5787
  validateUtf8String(toolCallId, 'delegate toolCallId', maxTransferIdentifierBytes, true);
5530
- const definition = this.subagents.get(input.subagentType);
5788
+ const definition = run.subagents.get(input.subagentType);
5531
5789
  if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
5532
5790
  throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
5533
5791
  }
@@ -5738,7 +5996,7 @@ export class FlexHarness<TScope = unknown> {
5738
5996
  if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
5739
5997
  throw new FlexHarnessAbortError('The parent run no longer owns this subagent request.');
5740
5998
  }
5741
- if (run.stored.session.agent !== undefined && !this.subagents.has(run.stored.session.agent)) {
5999
+ if (run.stored.session.agent !== undefined && !run.subagents.has(run.stored.session.agent)) {
5742
6000
  throw new FlexHarnessValidationError(`Parent subagent "${run.stored.session.agent}" is disabled.`);
5743
6001
  }
5744
6002
  if ((run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
@@ -5760,8 +6018,11 @@ export class FlexHarness<TScope = unknown> {
5760
6018
  if (
5761
6019
  existing.session.parentSessionId !== run.sessionId
5762
6020
  || existing.session.agent !== definition.name
6021
+ || existing.session.configurationRef !== run.stored.session.configurationRef
5763
6022
  ) {
5764
- throw new FlexHarnessValidationError(`Task session "${sessionId}" is not owned by this parent and agent.`);
6023
+ throw new FlexHarnessValidationError(
6024
+ `Task session "${sessionId}" is not owned by this parent, agent, and configuration.`,
6025
+ );
5765
6026
  }
5766
6027
  if (
5767
6028
  taskId === undefined
@@ -5802,6 +6063,9 @@ export class FlexHarness<TScope = unknown> {
5802
6063
  sessionId,
5803
6064
  sessionGenerationId: createSessionGenerationId(),
5804
6065
  sessionGenerationSequence: state.revision + 1,
6066
+ ...(run.stored.session.configurationRef === undefined
6067
+ ? {}
6068
+ : { configurationRef: run.stored.session.configurationRef }),
5805
6069
  title: `Subagent: ${definition.name}`,
5806
6070
  createdAt: timestamp,
5807
6071
  updatedAt: timestamp,
@@ -7522,8 +7786,10 @@ export class FlexHarness<TScope = unknown> {
7522
7786
  if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
7523
7787
  throw new FlexHarnessPermissionStateError(`Run "${run.runId}" no longer accepts permission requests.`);
7524
7788
  }
7525
- if (input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) return;
7526
- const request: IFlexPermissionRequest = {
7789
+ if (!run.permissionPolicy && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) {
7790
+ return;
7791
+ }
7792
+ const candidate: IFlexPermissionRequest = {
7527
7793
  permissionId: plugins.crypto.randomUUID(),
7528
7794
  scopeId: run.scopeId,
7529
7795
  sessionId: run.sessionId,
@@ -7540,6 +7806,33 @@ export class FlexHarness<TScope = unknown> {
7540
7806
  : { metadata: normalizeJsonValue(input.metadata, this.toolOutputLimits) }),
7541
7807
  createdAt: new Date().toISOString(),
7542
7808
  };
7809
+ const policyDecision = run.permissionPolicy
7810
+ ? await this.decideSessionPermissionPolicy(state, run, candidate)
7811
+ : 'inherit';
7812
+ if (policyDecision === 'allow' || policyDecision === 'deny') {
7813
+ this.emitEvent(run.scopeId, run.stored.session, {
7814
+ type: 'permission.resolved',
7815
+ runId: run.runId,
7816
+ request: publicSnapshot(candidate),
7817
+ decision: policyDecision === 'allow' ? 'once' : 'reject',
7818
+ origin: 'policy',
7819
+ });
7820
+ if (policyDecision === 'deny') {
7821
+ const rejection = this.trustInternalError(
7822
+ new FlexHarnessPermissionRejectedError(candidate.permissionId),
7823
+ );
7824
+ this.abortRunInternally(run, rejection);
7825
+ throw rejection;
7826
+ }
7827
+ return;
7828
+ }
7829
+ if (policyDecision === 'inherit'
7830
+ && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) return;
7831
+ const request: IFlexPermissionRequest = { ...candidate };
7832
+ if (policyDecision === 'ask') {
7833
+ delete request.rememberKey;
7834
+ request.requiresExplicitResponse = true;
7835
+ }
7543
7836
  let resolvePermission!: () => void;
7544
7837
  let rejectPermission!: (error: unknown) => void;
7545
7838
  const promise = new Promise<void>((resolve, reject) => {
@@ -7587,6 +7880,70 @@ export class FlexHarness<TScope = unknown> {
7587
7880
  return promise;
7588
7881
  }
7589
7882
 
7883
+ private async decideSessionPermissionPolicy(
7884
+ state: IStorageState,
7885
+ run: IActiveRun,
7886
+ request: IFlexPermissionRequest,
7887
+ ): Promise<'inherit' | 'allow' | 'ask' | 'deny'> {
7888
+ const policy = run.permissionPolicy!;
7889
+ const signal = run.controller.signal;
7890
+ const context: Readonly<IFlexSessionPermissionPolicyContext<unknown>> = Object.freeze({
7891
+ scopeId: run.scopeId,
7892
+ scope: run.scope,
7893
+ storageKey: state.storageKey,
7894
+ sessionId: run.sessionId,
7895
+ ...requireSessionGeneration(run.stored.session),
7896
+ runId: run.runId,
7897
+ configurationRef: run.stored.session.configurationRef!,
7898
+ ...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
7899
+ request: publicSnapshot(request),
7900
+ signal,
7901
+ });
7902
+ let onAbort!: () => void;
7903
+ const aborted = new Promise<never>((_resolve, reject) => {
7904
+ onAbort = () => reject(signal.reason ?? this.trustInternalError(new FlexHarnessAbortError()));
7905
+ });
7906
+ signal.addEventListener('abort', onAbort, { once: true });
7907
+ if (signal.aborted) onAbort();
7908
+ let decision: unknown;
7909
+ try {
7910
+ decision = await Promise.race([
7911
+ Promise.resolve().then(() => {
7912
+ signal.throwIfAborted();
7913
+ return policy(context);
7914
+ }),
7915
+ aborted,
7916
+ ]);
7917
+ if (!['inherit', 'allow', 'ask', 'deny'].includes(decision as string)) {
7918
+ throw new FlexHarnessValidationError('Session permissionPolicy returned an invalid decision.');
7919
+ }
7920
+ } catch (error) {
7921
+ if (signal.aborted) throw signal.reason;
7922
+ const projected = this.projectExternalError(run, error, 'permissionPolicy');
7923
+ this.abortRunInternally(run, projected);
7924
+ throw projected;
7925
+ } finally {
7926
+ signal.removeEventListener('abort', onAbort);
7927
+ }
7928
+ signal.throwIfAborted();
7929
+ const generation = requireSessionGeneration(run.stored.session);
7930
+ if (
7931
+ state.lifecycle !== 'active'
7932
+ || state.activeRuns.get(run.sessionId) !== run
7933
+ || state.sessions.get(run.sessionId) !== run.stored
7934
+ || run.callbacksClosed
7935
+ || generation.sessionGenerationId !== request.sessionGenerationId
7936
+ || generation.sessionGenerationSequence !== request.sessionGenerationSequence
7937
+ ) {
7938
+ const ownershipError = this.trustInternalError(new FlexHarnessAbortError(
7939
+ 'Permission policy lost its exact run ownership.',
7940
+ ));
7941
+ this.abortRunInternally(run, ownershipError);
7942
+ throw ownershipError;
7943
+ }
7944
+ return decision as 'inherit' | 'allow' | 'ask' | 'deny';
7945
+ }
7946
+
7590
7947
  private async applyPermissionResponse(
7591
7948
  state: IStorageState,
7592
7949
  pending: IPendingPermission,
@@ -10254,6 +10611,8 @@ export class FlexHarness<TScope = unknown> {
10254
10611
  return;
10255
10612
  }
10256
10613
  if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
10614
+ const configurationSettlement = this.abortSessionConfigurations(storageKey, reason);
10615
+ if (configurationSettlement) await configurationSettlement;
10257
10616
  for (const staged of [...state.sessionImports.values()]) {
10258
10617
  if (staged.phase === 'staging') this.releaseStagedSessionImport(staged);
10259
10618
  }
@@ -10444,6 +10803,7 @@ export class FlexHarness<TScope = unknown> {
10444
10803
  this.scopeRetirements.clear();
10445
10804
  this.storageDrains.clear();
10446
10805
  this.storageCompactorLifecycleControllers.clear();
10806
+ this.sessionConfigurations.clear();
10447
10807
  }
10448
10808
 
10449
10809
  private async abortSlashCommandExecutions(