lib/clay-product-control.js
Rama main · réplica de lectura del repositorio original.
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 };