Ctrl K
ComunidadGuías…

lib/clay-product-control.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var crypto = require("crypto");
var builtinMates = require("./builtin-mates");
var accessModule = require("./workspace-query-access");
var refs = require("./workspace-query-service");
var provenance = require("./session-provenance");
var personal = require("./mcp-session-access");

var input = require("./clay-product-control-input");
var ACTIONS = input.ACTIONS;
var KEYS = input.KEYS;
var deleteScope = require("./project-session-delete").resolveDeletionScope;
function revision(session) {
  return JSON.stringify([session.ownerId, session.sessionOriginId, session.cliSessionId, session.title, session.vendor,
    session.model, session.effort, session.permissionMode, session.mode, session.runtimeMode, session.hidden,
    session.isProcessing, session._queryStarting, session.taskStopRequested, session._pendingMessageDrainPaused,
    session._sdkQueryGeneration, session.history && session.history.length, session.messageUUIDs,
    session.sessionProvenance, session.pendingPush, Object.keys(session.pendingPermissions || {}), Object.keys(session.pendingAskUser || {})]);
}
function string(value, max) { return typeof value === "string" && value.trim() && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value); }
function attachClayProductControl(ctx) {
  var access = accessModule.attachWorkspaceQueryAccess(ctx);
  var states = new WeakMap();
  var busy = new WeakSet();
  function bind(source) {
    if (!source || !source.session) return null;
    var session = source.session, generation = Number(session._sdkQueryGeneration || 0);
    var sourceProject = ctx.getProjects().get(source.projectSlug);
    function authorize() {
      var project = ctx.getProjects().get(source.projectSlug);
      if (!project || project !== sourceProject) throw new Error("Source project was deleted or replaced.");
      var status = project.getStatus(), manager = project.getSessionManager();
      var mate = ctx.resolveMate(source.projectOwnerId, source.mateId);
      var definition = mate && builtinMates.getBuiltinByKey(mate.builtinKey);
      if (!status || !status.isMate || status.mateId !== source.mateId || status.projectOwnerId !== source.projectOwnerId || !mate || mate.lifecycleDeletion || mate.id !== source.mateId || mate.builtinKey !== "clay" || !definition || !definition.hostAgent || (ctx.isMultiUser() && mate.createdBy !== source.projectOwnerId)) throw new Error("Canonical Clay ownership is no longer valid.");
      if (manager.sessions.get(session.localId) !== session || Number(session._sdkQueryGeneration || 0) !== generation || session.taskStopRequested || session.abortController && session.abortController.signal.aborted || session.spawn || provenance.isWorker(session) || session.homeDebatePlanning || session.debateSetupMode || session._mentionInProgress) throw new Error("This canonical Clay query is stale or stopped.");
      if (session.destroying || session.hidden || session.mateCreationMode || session._mcpForeignInput || session.sessionVisibility === "shared" || session.delegated || session._delegatedBy || session._pairDelegation) throw new Error("Product controls require a private, owner-authored canonical Clay conversation.");
      if (ctx.isMultiUser() && !personal.canUsePersonalConnections(session, manager.sessions)) throw new Error("Foreign input cannot authorize personal product controls.");
      if (!ctx.isMultiUser() && (session.ownerId || (session.history || []).some(function (entry) { return entry.type === "user_message" && entry.from && entry.from !== "default"; }))) throw new Error("Foreign input cannot authorize personal product controls.");
      if (ctx.isMultiUser() && (!source.projectOwnerId || session.ownerId !== source.projectOwnerId || !ctx.findUser(source.projectOwnerId))) throw new Error("The source owner is no longer available.");
      if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== source.projectOwnerId) throw new Error("Source ownership changed.");
      return { userId: ctx.isMultiUser() ? source.projectOwnerId : null, singleUserOwnerId: source.projectOwnerId || null };
    }
    try { authorize(); } catch (error) { return null; }
    var state = states.get(session);
    if (!state || state.generation !== generation) { state = { generation: generation, targets: new Map(), requests: new Map() }; states.set(session, state); }
    var display = require('./clay-control-presentation-binding').bindPresentation({
      state: state, session: session, authorize: authorize, access: access,
      getProjects: ctx.getProjects, getOwnerUi: ctx.getOwnerUi, getMateDir: ctx.getMateDir,
      resolveMate: ctx.resolveMate, canUsePath: ctx.canUsePath, multiUser: ctx.isMultiUser,
      ownerId: source.projectOwnerId, sourceSlug: source.projectSlug, mateId: source.mateId
    });
    function review(question, check, presentation) {
      return display.review(function (text, validate, summary) { return sourceProject.requestProductControlReview(session, text, validate, summary); }, question, check, presentation);
    }
    function resolve(slug, ref) {
      var principal = authorize(), project = ctx.getProjects().get(slug);
      var status = project && project.getStatus();
      if (!status || status.isMate || !access.evaluate(principal, status).accessible) throw new Error("Target project is unavailable or private.");
      var sm = project.getSessionManager(), target = null;
      sm.sessions.forEach(function (candidate) { if (refs.sessionRef(slug, candidate) === ref) target = candidate; });
      if (!target || target.hidden || (ctx.isMultiUser() ? target.ownerId !== principal.userId : !!target.ownerId && target.ownerId !== principal.singleUserOwnerId)) throw new Error("Target session is unavailable or belongs to another owner.");
      if (target.spawn || provenance.isWorker(target)) throw new Error("Worker sessions are operated by their project Driver.");
      return { project: project, session: target, principal: principal };
    }
    function scope(project, target) {
      var sm = project.getSessionManager();
      var plan = deleteScope(sm.sessions, [target], function (item) { return (item.ownerId || null) === (target.ownerId || null); });
      if (!plan.ok) throw new Error(plan.error);
      return plan.ids.map(function (id) { var item = sm.sessions.get(id); return { item: item, revision: revision(item) }; });
    }
    function check(target, queueRequired, terminalClosing) {
      if (Date.now() - target.created > 300000) throw new Error("Target inspection expired.");
      var live = resolve(target.slug, target.ref);
      if (!terminalClosing && (live.project.inspectProductSessionRuntime ? live.project.inspectProductSessionRuntime(live.session) : JSON.stringify([live.session.terminalId, live.session.runtimeTerminalId, live.session.tuiSuspended])) !== target.terminalRevision) throw new Error("Terminal runtime changed. Inspect again.");
      if (live.project !== target.project || live.session !== target.session || revision(live.session) !== target.revision) throw new Error("The exact target revision changed. Inspect again before requesting a new action.");
      if (queueRequired && live.project.inspectProductSessionQueue(live.principal, live.session).revision !== target.queueRevision) throw new Error("The queue revision changed. Inspect and review again.");
      var current = scope(live.project, live.session);
      if (current.length !== target.scope.length || current.some(function (entry, i) { return entry.item !== target.scope[i].item || entry.revision !== target.scope[i].revision; })) throw new Error("Driver or owned Worker revision changed. Inspect again.");
      return live;
    }
    function capabilities() {
      authorize();
      return { screen: { tools: ["read_owner_screen", "inspect_owner_screen_change", "control_owner_screen"], scope: "Exact authenticated owner UI connection and owned conversation navigation; delivery and client acceptance do not prove rendering. screen_context lists server-known connections. split_views reads saved configuration and challenges one selected live project window for visible/cached pane bindings, layout and focus. Typed reviewed split_view changes share human persistence and navigation paths. Owner notification read and reviewed exact dismissal use the existing queue. No arbitrary messages or Settings. Split view create/update/rename/separate/member transfer/folder/Favorite/open/focus are supported in the selected live project window; zero-to-eight session composition and bounded v1 workspace updates for existing tool instances share the human View service. File Follow/Pin/close, placement and layout are supported; new or rebound tool resources remain unavailable through this control. Saved descriptors and client observations are distinct. Account-wide workbench mode remains excluded.", receipts: "Query-local attempts survive catalog rebinding only. Read delivery status; never blindly repeat an uncertain delivery." }, actions: ACTIONS, scheduling: { tools: ["begin_schedule_control_interview", "read_schedule_controls", "inspect_schedule_change", "control_schedules"], mates: "Own and same-owner private Mate conversation list/read/create/update/cancel. No pause/resume/run-now action exists for logical Mate schedules.", projects: "Selected owned ordinary-project Driver list/read, proposed creation requiring final workbench Create task, reviewed update/pause/resume/cancel/run through existing scheduling services. Run is queued, never task completion; Workers remain owned by the project scheduled executor.", receipts: "Dated request IDs; 24-hour receipt window, at most 100 retained receipts on the source conversation. Interrupted writes require inspection, never blind execution." }, projectRecords: { tools: ["read_project_records", "inspect_project_record_change", "control_project_records"], scope: "Selected owned ordinary projects, including worktree context. Exact owner review; query-local bounded retry receipts.", unsupported: ["githubFiling", "workSessionLaunch", "scheduledResultMutation"], routing: "Use the existing reviewed project assignment or follow-up tools for Driver execution. Record changes never run Git commits." }, mateKnowledge: { tools: ["read_mate_knowledge_control", "inspect_mate_knowledge_change", "control_mate_knowledge"], review: "Exact owner-reviewed Markdown and typed database/record operations; serialized intents are limited to 1400 characters within the existing review card.", unsupported: ["hardDatabaseOrRecordDeletion", "legacyJsonlWrites", "promotionOrSharing", "arbitraryFilesystemOrSql", "userKnowledgeCuration"] }, mateLifecycle: { actions: ["list", "inspect", "profile", "delete"], profileFields: ["name", "bio", "workspaceColor", "avatarCatalogId"], deletion: "Idle owner-private Mates only; canonical Clay and primary Mates are protected. Stored transcripts and provider history are retained. Pending runtime cleanup without closure proof requires operator recovery.", excluded: ["knowledge", "directMessages", "schedules", "sharing", "ownershipTransfer", "model"] }, mateChat: { tools: ["read_mate_chat_control", "inspect_mate_chat_change", "control_mate_chat"], actions: ["send", "stop", "model", "vendor", "effort"], scope: "Exact existing same-owner private Mate conversation; vendor selection requires a fresh idle conversation. Delegated messages are untrusted and never owner input. Dated durable retry receipts, no implicit resume or thread migration." }, workspace: { tools: ["read_project_workspace", "read_workspace_receipt", "inspect_workspace_change", "control_project_workspace"], files: "Bounded list/read and exact reviewed writes to existing regular text files within a selected owned ordinary project. Symlinks, hardlinks, managed settings, create/delete/move and arbitrary filesystem operations are excluded.", git: "Status and changed-file diff; exact reviewed selected-path stage/unstage only. No linked Git metadata/submodules, commits, pull/push, PR operations or arbitrary Git arguments.", terminal: "Owner shell list/scrollback and reviewed blank shell creation, rename, resize and acknowledged close. No input, commands, coding-agent/login terminals or Worker control." }, approval: "All changes except Stop require an authenticated owner review of the exact action and target revision.",
        vendorUsage: { tools: ["read_vendor_usage"], scope: "Read-only usage of the owner's own vendor accounts and sessions in one owned project, through the same service as the top bar; the owner's identity only, no refresh, credentials, URLs, commands or settings. Account figures keep each vendor's units; unreadable usage is reported with its reason." },
        taskExecution: "Use propose_project_assignment or propose_project_follow_up to route execution through a project Driver. A stopped session cannot be resumed by this control layer.",
        mcp: { tools: ['read_mcp_connections', 'inspect_mcp_connection_change', 'apply_mcp_connection_change', 'discover_mcp_connections', 'propose_mcp_import'], scope: 'Owner-private agent MCP catalog shared with supported Drivers and Mates. Read connections/tools and bounded Xcode readiness; exact owner-reviewed local/remote add, enable/disable, test, rename/remove, sign-in start, Xcode connect/disconnect and Client MCP toggles. Same authoritative services as human MCP management. Secure credential/token/permission forms and Client setup can be requested on the exact approving interface, with changed-context refusal and explicit client acknowledgement; acceptance does not prove visible rendering. Credentials are never accepted in agent arguments. Extension/bridge installation, extension server definitions, Xcode settings/license and direct session approval policy changes remain human steps. No general Server Settings or User Settings dispatch.', receipts: 'At most 100 per source query, ten-minute expiry, no automatic replay, no durable receipt after restart.' },
        unsupported: { projectOwnershipRecovery: "Human administrator-only diagnosis and reviewed recovery in Project settings or the restricted project Access page. Orphan projects have no verified owner to authorize canonical project controls; canonical Clay cannot inspect or claim them. Server operator identity provisioning remains manual.", personalProviders: "Personal AI provider installation discovery, reviewed supported installation, sign-in and audio transcription API keys are available only in authenticated human settings. No canonical credential, provider installation or sign-in operation is exposed.", worktrees: "Worktree icon edits, exclusion and disk deletion are available only through human product controls. Restoring exclusions and reconciling pending worktree removals have no in-app operation.", archive: "No authoritative session archive service exists.", send: "Use reviewed Driver assignment or follow-up.", runtime: "New TUI presentation and cross-provider thread migration are unavailable. runtime=gui closes a legacy Claude terminal with acknowledged exit. vendor_new_session creates a private blank GUI session and retains the original without execution.", projectLifecycle: ctx.projectLifecycle ? "Create, register, rename, remove registration, project environment and owner access controls use exact human review. OS filesystem sharing changes are unavailable through this service." : "Unavailable in this runtime." },
        excluded: ["serverSettings", "userSettings", "workerCreation", "pairOrchestration"] };
    }
    function inspect(args) {
      if (!args || Object.keys(args).some(function (key) { return ["projectSlug", "sessionRef"].indexOf(key) < 0; }) || !string(args.projectSlug, 200) || !/^session:[\w-]{24}$/.test(args.sessionRef)) throw new Error("An exact projectSlug and opaque sessionRef are required.");
      var live = resolve(args.projectSlug, args.sessionRef), target = live.session;
      if (state.targets.size >= 100) state.targets.delete(state.targets.keys().next().value);
      var queue = typeof live.project.inspectProductSessionQueue === "function" ? live.project.inspectProductSessionQueue(live.principal, target) : null;
      var token = "control:" + crypto.randomUUID();
      state.targets.set(token, { slug: args.projectSlug, ref: args.sessionRef, project: live.project, session: target, ownerId: target.ownerId || null, queueRevision: queue && queue.revision, terminalRevision: live.project.inspectProductSessionRuntime ? live.project.inspectProductSessionRuntime(target) : JSON.stringify([target.terminalId, target.runtimeTerminalId, target.tuiSuspended]), revision: revision(target), scope: scope(live.project, target), created: Date.now() });
      var sm = live.project.getSessionManager(), vendor = target.vendor || sm.defaultVendor || "claude";
      var caps = (sm.capabilitiesByVendor || {})[vendor] || {};
      return { queue: queue, availableVendors: sm.availableVendors || [], runtimeMode: target.runtimeMode || target.mode || "gui", runtimeTransition: "Only legacy Claude terminal to GUI chat is supported; terminal exit must be acknowledged. No execution starts.", vendorTransition: "vendor selects a fresh idle GUI Driver provider; vendor_new_session creates a private blank session and retains an established original without migrating history.", targetToken: token, projectSlug: args.projectSlug, sessionRef: args.sessionRef, title: target.title || "New Session", vendor: vendor, model: target.model || null, effort: target.effort || null, permissionMode: target.permissionMode || "default", processing: !!target.isProcessing, stopped: !!target.taskStopRequested, queuePaused: !!target._pendingMessageDrainPaused,
        providerCapabilities: { forceTermination: require('./session-force-stop-support').describe(sm, target), fork: caps.fork === true, conversationRewind: caps.rollback === true || caps.rewind === true, effort: caps.effort === true, modelCatalogAvailable: !!((sm.modelsByVendor || {})[vendor] || []).length }, deletionCount: scope(live.project, target).length, checkpoints: (target.messageUUIDs || []).map(function (entry) { return { uuid: entry.uuid, type: entry.type }; }), actions: ACTIONS, models: (sm.modelsByVendor || {})[vendor] || [], permissions: require("./session-permission-mode").clientState(target, sm.currentPermissionMode, vendor), expiresInSeconds: 300 };
    }
    async function run(args, target) {
      var queueAction = args.action.indexOf("queue_") === 0;
      var live = check(target, queueAction);
      if (Date.now() - target.created > 300000) throw new Error("Target inspection expired.");
      if (args.action !== "stop") {
        var question = input.description(args) + "\nProject: " + target.slug + "\nSession: " + (live.session.title || "New Session") + " (" + target.ref + ")\nWhy: " + args.why;
        if (queueAction) question += "\nQueue revision: " + target.queueRevision + ". Queue changes never resume a stopped Driver.";
        if (args.action === "force_stop") question += "\nForce stop scope: this Driver and " + (target.scope.length - 1) + " owned Workers. Panes and conversation history remain. Every target must support YOKE force termination before any effect. Query runtime exit is reported separately from unconfirmed external commands or requests.";
        if (args.action === "delete") question += "\nPermanently deletes " + target.scope.length + " sessions: this Driver and all its owned Worker history. Inspect again if anything changes.";
        var result = await review(question, function () { authorize(); check(target, queueAction); });
        if (result.status !== "submitted" || !result.answers.control || result.answers.control.length !== 1 || result.answers.control[0] !== "Apply exact change") return { status: "cancelled", action: args.action };
        live = check(target, queueAction);
      }
      if (!queueAction && live.session.taskStopRequested && args.action !== "stop" && args.action !== "force_stop" && args.action !== "rename" && args.action !== "delete") throw new Error("The target was stopped. Resume it explicitly through its Driver.");
      if (typeof live.project.applyProductSessionAction !== "function") throw new Error("Target project controls are unavailable.");
      if (busy.has(live.session)) throw new Error("Another control action is applying to this target. Inspect again after it settles.");
      if (args.action === "stop" && display.current()) display.current().start("running");
      busy.add(live.session);
      var result;
      try { result = await live.project.applyProductSessionAction(live.principal, live.session, args.action, args.value, function () { check(target, queueAction, args.action === "runtime"); }, { args: args, requestKey: source.projectSlug + "\n" + (session.sessionOriginId || session.localId) + "\n" + args.requestId }); }
      finally { busy.delete(live.session); }
      return Object.assign({ status: "completed", action: args.action, projectSlug: target.slug, sessionRef: target.ref, why: args.why }, result);
    }
    function act(args) {
      authorize();
      input.validate(args);
      args = Object.assign({}, args);
      if (args.ids) args.ids = args.ids.slice();
      var target = state.targets.get(args.targetToken);
      if (!target) throw new Error("Inspect the exact target first.");
      var key = JSON.stringify(KEYS.map(function (name) { return args[name] === undefined ? null : args[name]; }));
      var old = state.requests.get(args.requestId);
      if (old) { if (old.key !== key) throw new Error("requestId already names a different exact action."); var project = ctx.getProjects().get(target.slug);
        if (project !== target.project || !access.evaluate(authorize(), project.getStatus()).accessible) throw new Error("Target access changed.");
        var current = project.getSessionManager().sessions.get(target.session.localId);
        if (current && (current !== target.session || (current.ownerId || null) !== target.ownerId)) throw new Error("Target was replaced.");
        return old.promise; }
      if (state.requests.size >= 100) throw new Error("Too many control requests in this query.");
      var promise = Promise.resolve().then(function () { return run(args, target); }).catch(function (error) { return Object.assign({ status: "failed", action: args.action, error: error.message }, error.productControlResult || {}); });
      state.requests.set(args.requestId, { key: key, promise: promise });
      return promise;
    }
    if (!state.lifecycle) state.lifecycle = { targets: new Map(), requests: new Map() };
    var lifecycle = require("./clay-project-lifecycle").attachClayProjectLifecycle({
      state: state.lifecycle, service: ctx.projectLifecycle, authorize: authorize, getProjects: ctx.getProjects, findUser: ctx.findUser,
      permissions: ctx.lifecyclePermissions, canUsePath: ctx.canUsePath, getAccess: ctx.onGetProjectAccess,
      sourceKey: source.projectSlug + "\n" + (session.sessionOriginId || session.localId),
      creationRevision: function (anchor) { return ctx.projectLifecycle.creationRevision(anchor); },
      review: function (question, check) { return review(question, check); },
    });
    if (!state.mates) state.mates = { targets: new Map(), requests: new Map() };
    var mateControls = require('./clay-mate-lifecycle').attachClayMateLifecycle({ state: state.mates, authorize: authorize,
      getService: ctx.getMateLifecycle, getProjects: ctx.getProjects,
      review: function (question, check) { return review(question, check); } });
    if (!state.knowledge) state.knowledge = { targets: new Map(), requests: new Map() };
    var knowledgeControls = require('./clay-mate-knowledge').attachClayMateKnowledge({ state: state.knowledge, authorize: authorize,
      getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
      sourceKey: source.projectSlug + '\n' + (session.sessionOriginId || session.localId),
      review: function (question, check) { return review(question, check); } });
    function resolveOwnedProject(slug) {
      var principal = authorize(), project = ctx.getProjects().get(slug), status = project && project.getStatus();
      var decision = status && access.evaluate(principal, status);
      if (!status || status.isMate || !decision.owned || !decision.accessible) throw new Error('Selected records require an owned ordinary project with live access.');
      if (status.isWorktree) {
        var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus();
        if (!parentStatus || parentStatus.isMate || !access.evaluate(principal, parentStatus).owned || !access.evaluate(principal, parentStatus).accessible) throw new Error('Selected worktree records require live parent ownership and access.');
      }
      return { principal: principal, project: project, status: status };
    }
    if (!state.records) state.records = { targets: new Map(), requests: new Map() };
    var recordControls = require('./clay-project-records').attachClayProjectRecords({ state: state.records, authorize: authorize,
      services: function () { return ctx.getRecordServices && ctx.getRecordServices(); },
      resolve: resolveOwnedProject,
      actor: function () { authorize(); provenance.ensureOrigin(session); return { type: 'canonicalClay', userId: session.ownerId || null, displayName: 'Clay', sourceProjectSlug: source.projectSlug, mateId: source.mateId, sessionKey: session.cliSessionId || 'local:' + session.localId, sessionOriginId: session.sessionOriginId, vendor: session.vendor || null }; },
      review: function (question, check) { return review(question, check); } });
    var vendorUsageControls = require('./clay-vendor-usage').attachClayVendorUsage({ authorize: authorize, multiUser: ctx.isMultiUser, resolve: resolveOwnedProject });
    if (!state.scheduling) state.scheduling = { targets: new Map(), requests: new Map(), interview: false };
    var scheduleControls = require('./clay-scheduling').attachClayScheduling({ state: state.scheduling, authorize: authorize, session: session,
      multiUser: ctx.isMultiUser, getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
      mateSlug: function (id) {
        var found;
        ctx.getProjects().forEach(function (project, slug) { var status = project.getStatus(); if (status.isMate && status.mateId === id) found = slug; });
        if (!found) throw new Error('The Mate scheduling runtime is unavailable.'); return found;
      },
      authorizeProject: function (principal, status) {
        var decision = access.evaluate(principal, status);
        if (!decision.owned || !decision.accessible) throw new Error('Scheduling requires live ordinary-project ownership and access.');
        if (status.isWorktree) { var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus(); if (!parentStatus || parentStatus.isMate || !access.evaluate(principal, parentStatus).owned || !access.evaluate(principal, parentStatus).accessible) throw new Error('Scheduling requires live parent-project ownership and access.'); }
      },
      actor: function () { authorize(); provenance.ensureOrigin(session); return { type: 'canonicalClay', userId: session.ownerId || null, sourceProjectSlug: source.projectSlug, mateId: source.mateId, sessionOriginId: session.sessionOriginId, vendor: session.vendor || null }; },
      sourceKey: source.projectSlug + '\n' + (session.sessionOriginId || session.localId),
      save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
      review: function (question, check) { return review(question, check); } });
    if (!state.mateChat) state.mateChat = { targets: new Map(), requests: new Map() };
    var mateChatControls = require('./clay-mate-chat').attachClayMateChat({
      state: state.mateChat, authorize: authorize, session: session, multiUser: ctx.isMultiUser, activity: display.current,
      sourceIdentity: function () {
        var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
        if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('The canonical source Mate directory changed.');
        return sourceProject.getMateChatControl().validate(session);
      },
      getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
      save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
      actor: function () { authorize(); return { mateId: source.mateId, sourceProjectSlug: source.projectSlug, sourceSessionRef: refs.sessionRef(source.projectSlug, session) }; },
      review: function (question, check) { return review(question, check); },
    });
    if (!state.workspace) state.workspace = { targets: new Map(), requests: new Map() };
    var workspaceControls = require('./clay-workspace-controls').attachWorkspaceControls({
      state: state.workspace, session: session, authorize: authorize, getProjects: ctx.getProjects,
      sourceIdentity: function () {
        var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
        if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('Canonical source directory changed.');
        return sourceProject.getMateChatControl().validate(session);
      },
      authorizeProject: function (principal, status) {
        var decision = access.evaluate(principal, status);
        if (!decision.owned || !decision.accessible) throw new Error('Workspace controls require current ordinary-project ownership and access.');
        if (status.isWorktree) {
          var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus();
          if (!parentStatus || parentStatus.isMate || !access.evaluate(principal, parentStatus).owned || !access.evaluate(principal, parentStatus).accessible) throw new Error('Worktree controls require current parent ownership and access.');
        }
      },
      save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
      review: function (question, check) { return review(question, check); },
    });
    if (!state.screen) state.screen = { targets: new Map(), requests: new Map() };
    var screenControls = require('./clay-screen-controls').attachScreenControls({
      state: state.screen, session: session, authorize: authorize, getProjects: ctx.getProjects, activity: display.current,
      getService: ctx.getOwnerUi, getNotifications: ctx.getNotifications,
      sourceIdentity: function () {
        var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
        if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('Canonical source directory changed.');
        var mate = ctx.resolveMate(source.projectOwnerId, source.mateId);
        var stat = require('fs').statSync(status.path);
        return JSON.stringify([sourceProject.getMateChatControl().validate(session), mate && [mate.id, mate.createdBy, mate.createdAt, mate.builtinKey], require('fs').realpathSync(status.path), stat.dev, stat.ino]);
      },
      authorizeProject: function (principal, status) {
        if (status.isMate) {
          var mate = ctx.resolveMate(source.projectOwnerId, status.mateId);
          if (!mate || mate.lifecycleDeletion || status.projectOwnerId !== source.projectOwnerId || ctx.isMultiUser() && mate.createdBy !== source.projectOwnerId || require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(source.projectOwnerId || 'default', status.mateId))) throw new Error('Private Mate navigation access denied.');
        } else {
          var decision = access.evaluate(principal, status);
          if (!decision.owned || !decision.accessible || ctx.canUsePath && !ctx.canUsePath(principal.userId, status.path)) throw new Error('Navigation requires current project ownership and access.');
          if (status.isWorktree) { var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus(); if (!parentStatus || !access.evaluate(principal, parentStatus).owned || !access.evaluate(principal, parentStatus).accessible) throw new Error('Worktree parent access changed.'); }
        }
      },
      review: function (question, check, presentation) { return review(question, check, presentation); }
    });
    return display.decorate(Object.assign({ _presentation: display, capabilities: capabilities, inspect: inspect, act: act, inspectProject: lifecycle.inspectProject, actProject: lifecycle.actProject }, mateControls, knowledgeControls, recordControls, scheduleControls, mateChatControls, workspaceControls, screenControls, vendorUsageControls));
  }
  return { bind: bind };
}
module.exports = { attachClayProductControl: attachClayProductControl };