import { type TControllerResource, type TControllerResourceAttachmentTarget, type TControllerSessionId } from '../dist_ts_interfaces/index.js'; import type { SmartDataAuthStore } from './classes.authstore.js'; import type { IControllerResourceDocument } from './interfaces.projects.js'; import { type IFlexBrowserResourceDescriptor } from './interfaces.flexipc.js'; export interface IControllerResourceRuntimeHost { startResource(resourceArg: IControllerResourceDocument, signalArg: AbortSignal): Promise; stopResource(resourceArg: IControllerResourceDocument, signalArg: AbortSignal): Promise; isRunning(resourceArg: IControllerResourceDocument): boolean; isAvailable(): boolean; renameResource?(resourceArg: IControllerResourceDocument): Promise; reconcileAttachment(resourceArg: IControllerResourceDocument, signalArg: AbortSignal): Promise; retireResource(resourceArg: IControllerResourceDocument, signalArg: AbortSignal): Promise; } export interface IControllerResourceCoordinatorOptions { store: Pick; terminalHost: IControllerResourceRuntimeHost; browserHost: IControllerResourceRuntimeHost; beforeResourceRetirement?(resourceArg: IControllerResourceDocument, signalArg: AbortSignal): Promise; isFlexHarnessAvailable(): boolean; resolveSessionAuthority(projectIdArg: string, sessionIdArg: TControllerSessionId): TControllerResourceSessionAuthoritySnapshot; resolveRetiredSessionIdentity(projectIdArg: string, sessionIdArg: TControllerSessionId, sessionIdentityIdArg: string, signalArg: AbortSignal): Promise; } /** What an MCP caller is allowed to act for, derived from its credential and never from input. */ export type TControllerResourceSubject = { kind: 'session'; sessionId: TControllerSessionId; sessionIdentityId: string; } /** `agentSessionId` is the conversation fence; absent for a plain shell terminal. */ | { kind: 'terminal'; resourceId: string; agentSessionId?: string; } | { kind: 'runtime'; }; export type TControllerResourceSessionAuthoritySnapshot = Readonly<{ state: 'managed'; sessionIdentityId: string; }> | Readonly<{ state: 'definitively_unmanaged' | 'uncertain'; sessionIdentityId?: never; }>; export declare class ControllerResourceUnavailableError extends Error { readonly code = "resource_unavailable"; constructor(messageArg: string); } export declare class ControllerProjectHasResourcesError extends Error { readonly resourceIds: string[]; readonly code = "project_has_resources"; constructor(resourceIds: string[]); } export declare class ControllerResourceCoordinator { private readonly options; private readonly projectStates; private readonly deletingSessions; private readonly retiringResources; constructor(options: IControllerResourceCoordinatorOptions); /** Admission closes before the first retirement write and stays closed on failure. */ isResourceRetiring(projectIdArg: string, resourceIdArg: string): boolean; listResources(projectIdArg: string, signalArg?: AbortSignal): Promise; reconcileProjectResources(projectIdArg: string, signalArg: AbortSignal): Promise; requireResourceKind(projectIdArg: string, resourceIdArg: string, kindArg: IControllerResourceDocument['kind']): Promise; createResource(inputArg: Parameters[0], signalArg: AbortSignal): Promise; renameResource(projectIdArg: string, resourceIdArg: string, titleArg: string): Promise; /** * Adds, removes or replaces one membership in the resource's attachment set. * * `add` is idempotent: attaching a subject that is already attached leaves the set as it is. * `replace` is what a Move uses, so the resource is never momentarily unattached. A remove with * no entry clears the whole set. */ changeAttachment(inputArg: { projectId: string; resourceId: string; op: 'add' | 'remove' | 'replace'; target: TControllerResourceAttachmentTarget | null; replaces?: TControllerResourceAttachmentTarget; expectedAttachmentRevision: number; signal: AbortSignal; }): Promise; /** * Refuses the attachment that would grow the set past the controller's own limit. The limit * sits below the browser runtime's capability cap, so a resource can never reach a size the * runtime rejects from inside a lifecycle path, where the refusal has nowhere to go. */ private assertAttachmentSetFits; /** Resolves an attach target into the exact entry that will be persisted. */ private resolveAttachmentEntry; /** * Resolves a target that is being removed. A removal must not re-prove the subject's authority: * the entry is being taken out precisely because the subject may no longer be usable. */ private resolveRemovableEntry; retireResource(projectIdArg: string, resourceIdArg: string, signalArg: AbortSignal): Promise; startResource(projectIdArg: string, resourceIdArg: string, signalArg: AbortSignal): Promise; stopResource(projectIdArg: string, resourceIdArg: string, signalArg: AbortSignal): Promise; recover(signalArg: AbortSignal): Promise; withSessionDeletion(projectIdArg: string, sessionIdArg: TControllerSessionId, signalArg: AbortSignal, deleteArg: () => Promise): Promise; reconcileSessionRetirement(projectIdArg: string, sessionIdArg: TControllerSessionId, signalArg: AbortSignal): Promise; withProjectRemovalGuard(projectIdArg: string, removeArg: () => Promise): Promise; resolveFlexBrowserResources(projectIdArg: string, sessionIdArg: string): Promise; requireFlexBrowserResource(inputArg: { projectId: string; sessionId: string; resourceId: string; attachmentRevision: number; }): Promise; /** * Only a browser may take a terminal subject, which makes an attachment cycle impossible by * construction instead of by cycle detection. */ private requireAttachableTerminalTarget; /** * A terminal subject is proven from local state only, so it is never `uncertain`: the terminal * either exists, is active and still runs the recorded conversation, or the attachment is stale. */ private resolveExactTerminalTargetAuthority; /** * The MCP browser gate. Identical in strength to {@link requireFlexBrowserResource}: exact * subject identity, exact attachment revision, and no attachment in flight. The subject comes * from the caller's credential, never from the request. */ requireSubjectBrowserResource(inputArg: { projectId: string; resourceId: string; attachmentRevision: number; subject: TControllerResourceSubject; }): Promise; private reconcilePendingAttachment; /** * Drops every entry whose subject is no longer exactly authorized, one entry at a time. An * unauthorized entry never invalidates the rest of the set: the other attached subjects keep * their access. */ private reconcileResourceAuthorities; private resolveExactEntryAuthority; private resolveExactAttachmentAuthority; /** * A deleted conversation loses its own membership only. Resources it shared with other chats * stay attached to those, which is the point of the set. */ private reconcileSessionRetirementInProject; /** * A retiring terminal stops being an attachment subject before it stops existing, so its * dependents are detached first and never observe a dangling subject. */ private detachResourcesAttachedToTerminal; private assertRetiredAttachmentAuthority; private restorePriorAndCancel; private toPublicResource; /** * Brings agent-backed terminals back after a controller restart or upgrade. Deliberately not * part of recover(): reconcileAttachment also runs during every attachment transition, and a * respawn must happen exactly once, at startup. * * Failures are collected rather than thrown — one unresolvable agent binary must not prevent the * controller from starting — and are bounded by the persisted failure counter, so a permanently * broken chat degrades once instead of retrying on every boot forever. */ restartAgentTerminals(signalArg: AbortSignal): Promise>; private hostFor; private projectState; private runProjectMutation; private sessionKey; }