clay-server 5.2.0-beta.4 → 5.2.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/account-auth-socket.js +1 -0
- package/lib/builtin-mates.js +17 -14
- package/lib/canonical-clay-record-authority.js +15 -0
- package/lib/canonical-clay-scheduling-authority.js +19 -0
- package/lib/claude-hook-installer.js +28 -0
- package/lib/clay-mate-chat-receipts.js +26 -0
- package/lib/clay-mate-chat.js +87 -0
- package/lib/clay-mate-knowledge-input.js +26 -0
- package/lib/clay-mate-knowledge.js +95 -0
- package/lib/clay-mate-lifecycle.js +95 -0
- package/lib/clay-product-control-input.js +33 -0
- package/lib/clay-product-control-tools.js +51 -0
- package/lib/clay-product-control.js +237 -0
- package/lib/clay-project-lifecycle.js +154 -0
- package/lib/clay-project-record-input.js +44 -0
- package/lib/clay-project-records.js +83 -0
- package/lib/clay-scheduling-input.js +21 -0
- package/lib/clay-scheduling-receipts.js +26 -0
- package/lib/clay-scheduling.js +119 -0
- package/lib/clay-screen-controls.js +96 -0
- package/lib/clay-workspace-controls.js +83 -0
- package/lib/clay-workspace-input.js +17 -0
- package/lib/clay-workspace-receipts.js +26 -0
- package/lib/daemon-project-access.js +11 -0
- package/lib/daemon-project-lifecycle-callbacks.js +15 -0
- package/lib/daemon-project-lifecycle.js +262 -0
- package/lib/daemon-projects.js +1 -1
- package/lib/daemon.js +73 -221
- package/lib/issues-service.js +29 -15
- package/lib/knowledge-db-import.js +5 -4
- package/lib/knowledge-db-service.js +9 -6
- package/lib/knowledge-document-index.js +2 -1
- package/lib/knowledge-product-operations.js +46 -0
- package/lib/mate-creation-mcp-server.js +2 -0
- package/lib/mate-knowledge-canonical-access.js +10 -0
- package/lib/mate-knowledge-service.js +7 -4
- package/lib/mate-lifecycle-service.js +94 -0
- package/lib/mate-message-delivery.js +43 -0
- package/lib/mate-ready-creation.js +9 -1
- package/lib/mate-schedule-input.js +29 -0
- package/lib/mate-schedule-lifecycle.js +151 -0
- package/lib/mate-scheduling.js +103 -0
- package/lib/mates.js +4 -3
- package/lib/notes-checklist.js +50 -0
- package/lib/notes.js +49 -3
- package/lib/os-users.js +20 -5
- package/lib/project-admin-overview.js +51 -0
- package/lib/project-ask-user.js +4 -2
- package/lib/project-delegated-follow-up.js +2 -1
- package/lib/project-directory-claim.js +65 -0
- package/lib/project-file-operations.js +61 -0
- package/lib/project-filesystem.js +13 -87
- package/lib/project-full-access.js +23 -2
- package/lib/project-knowledge.js +32 -18
- package/lib/project-lifecycle-operations.js +98 -0
- package/lib/project-lifecycle-process-witness.js +32 -0
- package/lib/project-lifecycle-runtime.js +50 -0
- package/lib/project-linux-access.js +22 -0
- package/lib/project-logs-canonical-clay.js +27 -0
- package/lib/project-logs-clay-query.js +16 -0
- package/lib/project-logs-schema.js +4 -2
- package/lib/project-logs-service.js +8 -7
- package/lib/project-logs.js +1 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-mate-chat-control.js +55 -0
- package/lib/project-mate-creation-proposal.js +18 -3
- package/lib/project-models.js +14 -31
- package/lib/project-notifications.js +74 -16
- package/lib/project-ownership.js +77 -0
- package/lib/project-pair-result-delivery.js +14 -3
- package/lib/project-pending-message-queue.js +1 -0
- package/lib/project-product-control.js +87 -0
- package/lib/project-product-session-queue.js +46 -0
- package/lib/project-push-access.js +28 -0
- package/lib/project-scheduled-message-queue.js +30 -0
- package/lib/project-scheduled-messages.js +63 -64
- package/lib/project-scheduled-task-control.js +83 -0
- package/lib/project-scheduled-tasks.js +13 -2
- package/lib/project-scheduling-access.js +10 -0
- package/lib/project-session-actions.js +65 -0
- package/lib/project-session-delete.js +8 -1
- package/lib/project-session-github.js +47 -12
- package/lib/project-session-history-actions.js +165 -0
- package/lib/project-session-runtime-actions.js +63 -0
- package/lib/project-session-vendor.js +54 -0
- package/lib/project-sessions.js +64 -241
- package/lib/project-user-message.js +30 -1
- package/lib/project-workspace-operations.js +126 -0
- package/lib/project-workspace-query.js +9 -2
- package/lib/project.js +95 -3
- package/lib/public/css/clay-tool-activity.css +51 -0
- package/lib/public/css/home-mate-creation.css +5 -0
- package/lib/public/css/overlays.css +4 -2
- package/lib/public/css/project-admin-overview.css +13 -0
- package/lib/public/css/session-folders.css +11 -0
- package/lib/public/css/session-github.css +3 -0
- package/lib/public/css/sticky-notes.css +61 -1
- package/lib/public/index.html +1 -1
- package/lib/public/modules/admin-settings-projects.js +6 -2
- package/lib/public/modules/app-connection.js +8 -1
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +18 -3
- package/lib/public/modules/app-projects.js +5 -0
- package/lib/public/modules/clay-owner-navigation.js +27 -0
- package/lib/public/modules/clay-tool-activity.js +140 -0
- package/lib/public/modules/clay-tool-presentation.js +338 -0
- package/lib/public/modules/command-palette.js +1 -1
- package/lib/public/modules/default-ai.js +11 -3
- package/lib/public/modules/home-debate-planning.js +5 -5
- package/lib/public/modules/home-mate-chat.js +8 -6
- package/lib/public/modules/home-mate-creation.js +73 -1
- package/lib/public/modules/personal-settings-dialog.js +7 -1
- package/lib/public/modules/project-access-recovery.js +17 -0
- package/lib/public/modules/project-admin-overview.js +21 -0
- package/lib/public/modules/session-folder-layout.js +1 -1
- package/lib/public/modules/session-folder-view-menu.js +9 -0
- package/lib/public/modules/session-github.js +65 -8
- package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
- package/lib/public/modules/sticky-note-markdown.js +72 -6
- package/lib/public/modules/sticky-notes-actions.js +30 -0
- package/lib/public/modules/sticky-notes-browser.js +9 -2
- package/lib/public/modules/sticky-notes-card.js +23 -7
- package/lib/public/modules/sticky-notes-checklist.js +71 -0
- package/lib/public/modules/sticky-notes-editor.js +108 -44
- package/lib/public/modules/sticky-notes-shared.js +50 -2
- package/lib/public/modules/sticky-notes-state.js +66 -0
- package/lib/public/modules/sticky-notes.js +34 -51
- package/lib/public/modules/thinking-lifecycle.js +33 -3
- package/lib/public/modules/thinking-view.js +23 -1
- package/lib/public/modules/tools.js +34 -6
- package/lib/public/modules/user-settings-appearance.js +10 -3
- package/lib/public/modules/user-settings-editors.js +1 -1
- package/lib/public/modules/user-settings-layout.js +11 -2
- package/lib/public/modules/user-settings-preferences.js +6 -2
- package/lib/public/modules/user-settings-profile.js +12 -2
- package/lib/public/modules/user-settings.js +3 -2
- package/lib/public/style.css +2 -0
- package/lib/scheduled-task-records.js +3 -1
- package/lib/scheduler.js +2 -0
- package/lib/sdk-bridge.js +220 -21
- package/lib/sdk-message-processor.js +2 -0
- package/lib/sdk-response-recovery.js +155 -0
- package/lib/server-admin.js +63 -16
- package/lib/server-dm.js +1 -1
- package/lib/server-global-ws.js +10 -20
- package/lib/server-home-chat-events.js +7 -3
- package/lib/server-home-chat.js +4 -16
- package/lib/server-home-mate-creation.js +1 -0
- package/lib/server-home-mcp-consent.js +3 -3
- package/lib/server-mates.js +25 -67
- package/lib/server-owner-ui.js +77 -0
- package/lib/server-path-access.js +38 -0
- package/lib/server-project-removal.js +22 -0
- package/lib/server.js +99 -29
- package/lib/session-config-transition.js +45 -0
- package/lib/session-folders.js +7 -2
- package/lib/session-github.js +9 -2
- package/lib/session-visibility.js +5 -2
- package/lib/sessions.js +10 -0
- package/lib/terminal-manager.js +52 -0
- package/lib/users-permissions.js +2 -8
- package/lib/users.js +4 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +8 -2
- package/lib/yoke/adapters/claude.js +47 -8
- package/lib/yoke/adapters/codex.js +52 -111
- package/lib/yoke/claude-worker-startup.js +28 -0
- package/lib/yoke/codex-idle-recovery.js +81 -1
- package/lib/yoke/codex-recovery-admission.js +164 -0
- package/lib/yoke/session-tool-transport.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var builtinMates = require("./builtin-mates");
|
|
3
|
+
var accessModule = require("./workspace-query-access");
|
|
4
|
+
var refs = require("./workspace-query-service");
|
|
5
|
+
var provenance = require("./session-provenance");
|
|
6
|
+
var personal = require("./mcp-session-access");
|
|
7
|
+
|
|
8
|
+
var input = require("./clay-product-control-input");
|
|
9
|
+
var ACTIONS = input.ACTIONS;
|
|
10
|
+
var KEYS = input.KEYS;
|
|
11
|
+
var deleteScope = require("./project-session-delete").resolveDeletionScope;
|
|
12
|
+
function revision(session) {
|
|
13
|
+
return JSON.stringify([session.ownerId, session.sessionOriginId, session.cliSessionId, session.title, session.vendor,
|
|
14
|
+
session.model, session.effort, session.permissionMode, session.mode, session.runtimeMode, session.hidden,
|
|
15
|
+
session.isProcessing, session._queryStarting, session.taskStopRequested, session._pendingMessageDrainPaused,
|
|
16
|
+
session._sdkQueryGeneration, session.history && session.history.length, session.messageUUIDs,
|
|
17
|
+
session.sessionProvenance, session.pendingPush, Object.keys(session.pendingPermissions || {}), Object.keys(session.pendingAskUser || {})]);
|
|
18
|
+
}
|
|
19
|
+
function string(value, max) { return typeof value === "string" && value.trim() && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value); }
|
|
20
|
+
function attachClayProductControl(ctx) {
|
|
21
|
+
var access = accessModule.attachWorkspaceQueryAccess(ctx);
|
|
22
|
+
var states = new WeakMap();
|
|
23
|
+
var busy = new WeakSet();
|
|
24
|
+
function bind(source) {
|
|
25
|
+
if (!source || !source.session) return null;
|
|
26
|
+
var session = source.session, generation = Number(session._sdkQueryGeneration || 0);
|
|
27
|
+
var sourceProject = ctx.getProjects().get(source.projectSlug);
|
|
28
|
+
function authorize() {
|
|
29
|
+
var project = ctx.getProjects().get(source.projectSlug);
|
|
30
|
+
if (!project || project !== sourceProject) throw new Error("Source project was deleted or replaced.");
|
|
31
|
+
var status = project.getStatus(), manager = project.getSessionManager();
|
|
32
|
+
var mate = ctx.resolveMate(source.projectOwnerId, source.mateId);
|
|
33
|
+
var definition = mate && builtinMates.getBuiltinByKey(mate.builtinKey);
|
|
34
|
+
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.");
|
|
35
|
+
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.");
|
|
36
|
+
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.");
|
|
37
|
+
if (ctx.isMultiUser() && !personal.canUsePersonalConnections(session, manager.sessions)) throw new Error("Foreign input cannot authorize personal product controls.");
|
|
38
|
+
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.");
|
|
39
|
+
if (ctx.isMultiUser() && (!source.projectOwnerId || session.ownerId !== source.projectOwnerId || !ctx.findUser(source.projectOwnerId))) throw new Error("The source owner is no longer available.");
|
|
40
|
+
if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== source.projectOwnerId) throw new Error("Source ownership changed.");
|
|
41
|
+
return { userId: ctx.isMultiUser() ? source.projectOwnerId : null, singleUserOwnerId: source.projectOwnerId || null };
|
|
42
|
+
}
|
|
43
|
+
try { authorize(); } catch (error) { return null; }
|
|
44
|
+
var state = states.get(session);
|
|
45
|
+
if (!state || state.generation !== generation) { state = { generation: generation, targets: new Map(), requests: new Map() }; states.set(session, state); }
|
|
46
|
+
function resolve(slug, ref) {
|
|
47
|
+
var principal = authorize(), project = ctx.getProjects().get(slug);
|
|
48
|
+
var status = project && project.getStatus();
|
|
49
|
+
if (!status || status.isMate || !access.evaluate(principal, status).accessible) throw new Error("Target project is unavailable or private.");
|
|
50
|
+
var sm = project.getSessionManager(), target = null;
|
|
51
|
+
sm.sessions.forEach(function (candidate) { if (refs.sessionRef(slug, candidate) === ref) target = candidate; });
|
|
52
|
+
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.");
|
|
53
|
+
if (target.spawn || provenance.isWorker(target)) throw new Error("Worker sessions are operated by their project Driver.");
|
|
54
|
+
return { project: project, session: target, principal: principal };
|
|
55
|
+
}
|
|
56
|
+
function scope(project, target) {
|
|
57
|
+
var sm = project.getSessionManager();
|
|
58
|
+
var plan = deleteScope(sm.sessions, [target], function (item) { return (item.ownerId || null) === (target.ownerId || null); });
|
|
59
|
+
if (!plan.ok) throw new Error(plan.error);
|
|
60
|
+
return plan.ids.map(function (id) { var item = sm.sessions.get(id); return { item: item, revision: revision(item) }; });
|
|
61
|
+
}
|
|
62
|
+
function check(target, queueRequired, terminalClosing) {
|
|
63
|
+
if (Date.now() - target.created > 300000) throw new Error("Target inspection expired.");
|
|
64
|
+
var live = resolve(target.slug, target.ref);
|
|
65
|
+
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.");
|
|
66
|
+
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.");
|
|
67
|
+
if (queueRequired && live.project.inspectProductSessionQueue(live.principal, live.session).revision !== target.queueRevision) throw new Error("The queue revision changed. Inspect and review again.");
|
|
68
|
+
var current = scope(live.project, live.session);
|
|
69
|
+
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.");
|
|
70
|
+
return live;
|
|
71
|
+
}
|
|
72
|
+
function capabilities() {
|
|
73
|
+
authorize();
|
|
74
|
+
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 is server-known connection/conversation only. Owner notification read and reviewed exact dismissal use the existing queue. No arbitrary messages, settings or contextual UI.", 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.",
|
|
75
|
+
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.",
|
|
76
|
+
unsupported: { 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." },
|
|
77
|
+
excluded: ["serverSettings", "userSettings", "workerCreation", "pairOrchestration"] };
|
|
78
|
+
}
|
|
79
|
+
function inspect(args) {
|
|
80
|
+
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.");
|
|
81
|
+
var live = resolve(args.projectSlug, args.sessionRef), target = live.session;
|
|
82
|
+
if (state.targets.size >= 100) state.targets.delete(state.targets.keys().next().value);
|
|
83
|
+
var queue = typeof live.project.inspectProductSessionQueue === "function" ? live.project.inspectProductSessionQueue(live.principal, target) : null;
|
|
84
|
+
var token = "control:" + crypto.randomUUID();
|
|
85
|
+
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() });
|
|
86
|
+
var sm = live.project.getSessionManager(), vendor = target.vendor || sm.defaultVendor || "claude";
|
|
87
|
+
var caps = (sm.capabilitiesByVendor || {})[vendor] || {};
|
|
88
|
+
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,
|
|
89
|
+
providerCapabilities: { 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 };
|
|
90
|
+
}
|
|
91
|
+
async function run(args, target) {
|
|
92
|
+
var queueAction = args.action.indexOf("queue_") === 0;
|
|
93
|
+
var live = check(target, queueAction);
|
|
94
|
+
if (Date.now() - target.created > 300000) throw new Error("Target inspection expired.");
|
|
95
|
+
if (args.action !== "stop") {
|
|
96
|
+
var question = input.description(args) + "\nProject: " + target.slug + "\nSession: " + (live.session.title || "New Session") + " (" + target.ref + ")\nWhy: " + args.why;
|
|
97
|
+
if (queueAction) question += "\nQueue revision: " + target.queueRevision + ". Queue changes never resume a stopped Driver.";
|
|
98
|
+
if (args.action === "delete") question += "\nPermanently deletes " + target.scope.length + " sessions: this Driver and all its owned Worker history. Inspect again if anything changes.";
|
|
99
|
+
var result = await sourceProject.requestProductControlReview(session, question, function () { authorize(); check(target, queueAction); });
|
|
100
|
+
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 };
|
|
101
|
+
live = check(target, queueAction);
|
|
102
|
+
}
|
|
103
|
+
if (!queueAction && live.session.taskStopRequested && args.action !== "stop" && args.action !== "rename" && args.action !== "delete") throw new Error("The target was stopped. Resume it explicitly through its Driver.");
|
|
104
|
+
if (typeof live.project.applyProductSessionAction !== "function") throw new Error("Target project controls are unavailable.");
|
|
105
|
+
if (busy.has(live.session)) throw new Error("Another control action is applying to this target. Inspect again after it settles.");
|
|
106
|
+
busy.add(live.session);
|
|
107
|
+
var result;
|
|
108
|
+
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 }); }
|
|
109
|
+
finally { busy.delete(live.session); }
|
|
110
|
+
return Object.assign({ status: "completed", action: args.action, projectSlug: target.slug, sessionRef: target.ref, why: args.why }, result);
|
|
111
|
+
}
|
|
112
|
+
function act(args) {
|
|
113
|
+
authorize();
|
|
114
|
+
input.validate(args);
|
|
115
|
+
args = Object.assign({}, args);
|
|
116
|
+
if (args.ids) args.ids = args.ids.slice();
|
|
117
|
+
var target = state.targets.get(args.targetToken);
|
|
118
|
+
if (!target) throw new Error("Inspect the exact target first.");
|
|
119
|
+
var key = JSON.stringify(KEYS.map(function (name) { return args[name] === undefined ? null : args[name]; }));
|
|
120
|
+
var old = state.requests.get(args.requestId);
|
|
121
|
+
if (old) { if (old.key !== key) throw new Error("requestId already names a different exact action."); var project = ctx.getProjects().get(target.slug);
|
|
122
|
+
if (project !== target.project || !access.evaluate(authorize(), project.getStatus()).accessible) throw new Error("Target access changed.");
|
|
123
|
+
var current = project.getSessionManager().sessions.get(target.session.localId);
|
|
124
|
+
if (current && (current !== target.session || (current.ownerId || null) !== target.ownerId)) throw new Error("Target was replaced.");
|
|
125
|
+
return old.promise; }
|
|
126
|
+
if (state.requests.size >= 100) throw new Error("Too many control requests in this query.");
|
|
127
|
+
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 || {}); });
|
|
128
|
+
state.requests.set(args.requestId, { key: key, promise: promise });
|
|
129
|
+
return promise;
|
|
130
|
+
}
|
|
131
|
+
if (!state.lifecycle) state.lifecycle = { targets: new Map(), requests: new Map() };
|
|
132
|
+
var lifecycle = require("./clay-project-lifecycle").attachClayProjectLifecycle({
|
|
133
|
+
state: state.lifecycle, service: ctx.projectLifecycle, authorize: authorize, getProjects: ctx.getProjects, findUser: ctx.findUser,
|
|
134
|
+
permissions: ctx.lifecyclePermissions, canUsePath: ctx.canUsePath, getAccess: ctx.onGetProjectAccess,
|
|
135
|
+
sourceKey: source.projectSlug + "\n" + (session.sessionOriginId || session.localId),
|
|
136
|
+
creationRevision: function (anchor) { return ctx.projectLifecycle.creationRevision(anchor); },
|
|
137
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); },
|
|
138
|
+
});
|
|
139
|
+
if (!state.mates) state.mates = { targets: new Map(), requests: new Map() };
|
|
140
|
+
var mateControls = require('./clay-mate-lifecycle').attachClayMateLifecycle({ state: state.mates, authorize: authorize,
|
|
141
|
+
getService: ctx.getMateLifecycle, getProjects: ctx.getProjects,
|
|
142
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); } });
|
|
143
|
+
if (!state.knowledge) state.knowledge = { targets: new Map(), requests: new Map() };
|
|
144
|
+
var knowledgeControls = require('./clay-mate-knowledge').attachClayMateKnowledge({ state: state.knowledge, authorize: authorize,
|
|
145
|
+
getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
|
|
146
|
+
sourceKey: source.projectSlug + '\n' + (session.sessionOriginId || session.localId),
|
|
147
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); } });
|
|
148
|
+
if (!state.records) state.records = { targets: new Map(), requests: new Map() };
|
|
149
|
+
var recordControls = require('./clay-project-records').attachClayProjectRecords({ state: state.records, authorize: authorize,
|
|
150
|
+
services: function () { return ctx.getRecordServices && ctx.getRecordServices(); },
|
|
151
|
+
resolve: function (slug) {
|
|
152
|
+
var principal = authorize(), project = ctx.getProjects().get(slug), status = project && project.getStatus();
|
|
153
|
+
var decision = status && access.evaluate(principal, status);
|
|
154
|
+
if (!status || status.isMate || !decision.owned || !decision.accessible) throw new Error('Selected records require an owned ordinary project with live access.');
|
|
155
|
+
if (status.isWorktree) {
|
|
156
|
+
var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus();
|
|
157
|
+
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.');
|
|
158
|
+
}
|
|
159
|
+
return { principal: principal, project: project, status: status };
|
|
160
|
+
},
|
|
161
|
+
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 }; },
|
|
162
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); } });
|
|
163
|
+
if (!state.scheduling) state.scheduling = { targets: new Map(), requests: new Map(), interview: false };
|
|
164
|
+
var scheduleControls = require('./clay-scheduling').attachClayScheduling({ state: state.scheduling, authorize: authorize, session: session,
|
|
165
|
+
multiUser: ctx.isMultiUser, getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
|
|
166
|
+
mateSlug: function (id) {
|
|
167
|
+
var found;
|
|
168
|
+
ctx.getProjects().forEach(function (project, slug) { var status = project.getStatus(); if (status.isMate && status.mateId === id) found = slug; });
|
|
169
|
+
if (!found) throw new Error('The Mate scheduling runtime is unavailable.'); return found;
|
|
170
|
+
},
|
|
171
|
+
authorizeProject: function (principal, status) {
|
|
172
|
+
var decision = access.evaluate(principal, status);
|
|
173
|
+
if (!decision.owned || !decision.accessible) throw new Error('Scheduling requires live ordinary-project ownership and access.');
|
|
174
|
+
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.'); }
|
|
175
|
+
},
|
|
176
|
+
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 }; },
|
|
177
|
+
sourceKey: source.projectSlug + '\n' + (session.sessionOriginId || session.localId),
|
|
178
|
+
save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
|
|
179
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); } });
|
|
180
|
+
if (!state.mateChat) state.mateChat = { targets: new Map(), requests: new Map() };
|
|
181
|
+
var mateChatControls = require('./clay-mate-chat').attachClayMateChat({
|
|
182
|
+
state: state.mateChat, authorize: authorize, session: session, multiUser: ctx.isMultiUser,
|
|
183
|
+
sourceIdentity: function () {
|
|
184
|
+
var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
|
|
185
|
+
if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('The canonical source Mate directory changed.');
|
|
186
|
+
return sourceProject.getMateChatControl().validate(session);
|
|
187
|
+
},
|
|
188
|
+
getProjects: ctx.getProjects, resolveMate: ctx.resolveMate, getMateDir: ctx.getMateDir,
|
|
189
|
+
save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
|
|
190
|
+
actor: function () { authorize(); return { mateId: source.mateId, sourceProjectSlug: source.projectSlug, sourceSessionRef: refs.sessionRef(source.projectSlug, session) }; },
|
|
191
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); },
|
|
192
|
+
});
|
|
193
|
+
if (!state.workspace) state.workspace = { targets: new Map(), requests: new Map() };
|
|
194
|
+
var workspaceControls = require('./clay-workspace-controls').attachWorkspaceControls({
|
|
195
|
+
state: state.workspace, session: session, authorize: authorize, getProjects: ctx.getProjects,
|
|
196
|
+
sourceIdentity: function () {
|
|
197
|
+
var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
|
|
198
|
+
if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('Canonical source directory changed.');
|
|
199
|
+
return sourceProject.getMateChatControl().validate(session);
|
|
200
|
+
},
|
|
201
|
+
authorizeProject: function (principal, status) {
|
|
202
|
+
var decision = access.evaluate(principal, status);
|
|
203
|
+
if (!decision.owned || !decision.accessible) throw new Error('Workspace controls require current ordinary-project ownership and access.');
|
|
204
|
+
if (status.isWorktree) {
|
|
205
|
+
var parent = ctx.getProjects().get(status.parentSlug), parentStatus = parent && parent.getStatus();
|
|
206
|
+
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.');
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
save: function () { return sourceProject.getSessionManager().saveSessionFile(session); },
|
|
210
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); },
|
|
211
|
+
});
|
|
212
|
+
if (!state.screen) state.screen = { targets: new Map(), requests: new Map() };
|
|
213
|
+
var screenControls = require('./clay-screen-controls').attachScreenControls({
|
|
214
|
+
state: state.screen, session: session, authorize: authorize, getProjects: ctx.getProjects,
|
|
215
|
+
getService: ctx.getOwnerUi, getNotifications: ctx.getNotifications,
|
|
216
|
+
sourceIdentity: function () {
|
|
217
|
+
var status = sourceProject.getStatus(), owner = source.projectOwnerId || 'default';
|
|
218
|
+
if (require('path').resolve(status.path) !== require('path').resolve(ctx.getMateDir(owner, source.mateId))) throw new Error('Canonical source directory changed.');
|
|
219
|
+
return sourceProject.getMateChatControl().validate(session);
|
|
220
|
+
},
|
|
221
|
+
authorizeProject: function (principal, status) {
|
|
222
|
+
if (status.isMate) {
|
|
223
|
+
var mate = ctx.resolveMate(source.projectOwnerId, status.mateId);
|
|
224
|
+
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.');
|
|
225
|
+
} else {
|
|
226
|
+
var decision = access.evaluate(principal, status);
|
|
227
|
+
if (!decision.owned || !decision.accessible || ctx.canUsePath && !ctx.canUsePath(principal.userId, status.path)) throw new Error('Navigation requires current project ownership and access.');
|
|
228
|
+
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.'); }
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
review: function (question, check) { return sourceProject.requestProductControlReview(session, question, check); }
|
|
232
|
+
});
|
|
233
|
+
return Object.assign({ capabilities: capabilities, inspect: inspect, act: act, inspectProject: lifecycle.inspectProject, actProject: lifecycle.actProject }, mateControls, knowledgeControls, recordControls, scheduleControls, mateChatControls, workspaceControls, screenControls);
|
|
234
|
+
}
|
|
235
|
+
return { bind: bind };
|
|
236
|
+
}
|
|
237
|
+
module.exports = { attachClayProductControl: attachClayProductControl };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Query-bound review of typed project intents; no transport impersonation.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var ACTIONS = ['create', 'register', 'rename', 'remove', 'visibility', 'allowed_users', 'session_visibility', 'environment', 'owner', 'reconcile', 'recover_registration'];
|
|
6
|
+
function hash(value) { return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
7
|
+
function attachClayProjectLifecycle(ctx) {
|
|
8
|
+
var targets = ctx.state.targets, requests = ctx.state.requests;
|
|
9
|
+
var service = ctx.service;
|
|
10
|
+
function principal() {
|
|
11
|
+
var actor = ctx.authorize();
|
|
12
|
+
if (!service) throw new Error('Project lifecycle is unavailable in this runtime.');
|
|
13
|
+
return actor;
|
|
14
|
+
}
|
|
15
|
+
function user() { var actor = principal(); return actor.userId ? ctx.findUser(actor.userId) : null; }
|
|
16
|
+
function permission(action) {
|
|
17
|
+
var account = user();
|
|
18
|
+
if (account) {
|
|
19
|
+
var perms = ctx.permissions(account);
|
|
20
|
+
if (action === 'environment' && !perms.projectSettings) throw new Error('Project settings access is not permitted.');
|
|
21
|
+
if ((action === 'create' || action === 'register') && !perms.createProject || action === 'remove' && !perms.deleteProject) throw new Error('Your project lifecycle permission is no longer available.');
|
|
22
|
+
}
|
|
23
|
+
return account;
|
|
24
|
+
}
|
|
25
|
+
function owned(slug) {
|
|
26
|
+
var actor = principal(), runtime = ctx.getProjects().get(slug), status = runtime && runtime.getStatus();
|
|
27
|
+
var record = ctx.getAccess(slug);
|
|
28
|
+
if (!status || status.isMate || /^mate-/.test(slug) || slug.indexOf('--') >= 0 || !record || record.error || record.isMate || record.ownershipRecovery || (actor.userId ? record.ownerId !== actor.userId : !!record.ownerId && record.ownerId !== actor.singleUserOwnerId)) throw new Error('Project controls require live ownership of an ordinary parent project.');
|
|
29
|
+
return runtime;
|
|
30
|
+
}
|
|
31
|
+
function validate(args) {
|
|
32
|
+
if (!args || Object.keys(args).some(function (key) { return ['action', 'projectSlug', 'name', 'path', 'value'].indexOf(key) < 0; }) || ACTIONS.indexOf(args.action) < 0) throw new Error('Choose a typed project lifecycle action.');
|
|
33
|
+
var allowed = ['action'];
|
|
34
|
+
if (args.action === 'create') {
|
|
35
|
+
allowed.push('name'); if (typeof args.name !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(args.name)) throw new Error('Provide a valid project name.');
|
|
36
|
+
} else if (args.action === 'register') {
|
|
37
|
+
allowed.push('path'); if (typeof args.path !== 'string' || args.path.length > 4096 || !path.isAbsolute(args.path) || /[\u0000-\u001f]/.test(args.path)) throw new Error('Provide an absolute existing directory path.');
|
|
38
|
+
} else {
|
|
39
|
+
allowed.push('projectSlug'); if (typeof args.projectSlug !== 'string' || !args.projectSlug || args.projectSlug.length > 200) throw new Error('Provide an exact projectSlug.');
|
|
40
|
+
if (args.action !== 'remove') allowed.push('value');
|
|
41
|
+
}
|
|
42
|
+
if (Object.keys(args).some(function (key) { return allowed.indexOf(key) < 0; })) throw new Error('Fields do not match the project action.');
|
|
43
|
+
if (args.action === 'allowed_users') {
|
|
44
|
+
if (!Array.isArray(args.value) || args.value.length > 100 || args.value.some(function (id, i) { return typeof id !== 'string' || !ctx.findUser(id) || args.value.indexOf(id) !== i; })) throw new Error('Choose unique existing account IDs.');
|
|
45
|
+
} else if (args.action !== 'create' && args.action !== 'register' && args.action !== 'remove' && args.action !== 'reconcile' && args.action !== 'recover_registration') {
|
|
46
|
+
if (typeof args.value !== 'string' || args.value.length > (args.action === 'environment' ? 10000 : 100) || /\u0000/.test(args.value)) throw new Error('Provide a bounded exact value.');
|
|
47
|
+
if (args.action === 'environment') { var error = require('./runtime-env').validateEnvString(args.value); if (error) throw new Error(error); }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function runtimeScope(slug) {
|
|
51
|
+
var scope = [];
|
|
52
|
+
ctx.getProjects().forEach(function (project, key) {
|
|
53
|
+
if (key !== slug && key.indexOf(slug + '--') !== 0) return;
|
|
54
|
+
var sessions = [];
|
|
55
|
+
project.getSessionManager().sessions.forEach(function (session) {
|
|
56
|
+
sessions.push([session, JSON.stringify([session.localId, session.sessionOriginId, session.ownerId, session.title, session.vendor, session.model, session.effort, session.permissionMode, session.sessionVisibility, session.isProcessing, session._queryStarting, session.taskStopRequested, session._sdkQueryGeneration, session.history && session.history.length, session.messageUUIDs, Object.keys(session.pendingPermissions || {}), Object.keys(session.pendingAskUser || {})])]);
|
|
57
|
+
});
|
|
58
|
+
scope.push({ slug: key, project: project, sessions: sessions });
|
|
59
|
+
});
|
|
60
|
+
return scope;
|
|
61
|
+
}
|
|
62
|
+
function sameScope(previous, current) {
|
|
63
|
+
return previous.length === current.length && previous.every(function (entry, i) {
|
|
64
|
+
var other = current[i]; return entry.slug === other.slug && entry.project === other.project && entry.sessions.length === other.sessions.length && entry.sessions.every(function (session, j) { return session[0] === other.sessions[j][0] && session[1] === other.sessions[j][1]; });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function inspect(args) {
|
|
68
|
+
validate(args);
|
|
69
|
+
args = JSON.parse(JSON.stringify({ action: args.action, projectSlug: args.projectSlug, name: args.name, path: args.path, value: args.value }));
|
|
70
|
+
var account = permission(args.action), runtime = null, revision = null;
|
|
71
|
+
service.assertSupported(args.action);
|
|
72
|
+
var recovery = null, receiptOnly = false;
|
|
73
|
+
if (args.action === 'reconcile' || args.action === 'recover_registration') {
|
|
74
|
+
recovery = service.inspectRecovery(args.projectSlug, args.value, principal()); permission(args.action === 'recover_registration' ? 'rename' : recovery.action); args.value = recovery.operationId; revision = recovery.revision;
|
|
75
|
+
} else if (args.action === 'owner' && service.receiptPreview(hash(args), ctx.sourceKey, account && account.id || null)) {
|
|
76
|
+
receiptOnly = true;
|
|
77
|
+
} else if (args.action === 'register') {
|
|
78
|
+
if (!ctx.canUsePath(account && account.id, args.path)) throw new Error('Directory access is not permitted.');
|
|
79
|
+
revision = JSON.stringify(service.registration(args.path, account));
|
|
80
|
+
args.path = fs.realpathSync(args.path);
|
|
81
|
+
} else if (args.action !== 'create') {
|
|
82
|
+
if (ctx.getProjects().has(args.projectSlug)) { runtime = owned(args.projectSlug); revision = service.stamp(args.projectSlug); }
|
|
83
|
+
else if (args.action !== 'remove') throw new Error('Project is unavailable.');
|
|
84
|
+
} else revision = ctx.creationRevision();
|
|
85
|
+
if (targets.size >= 100) targets.delete(targets.keys().next().value);
|
|
86
|
+
var token = 'project-control:' + crypto.randomUUID();
|
|
87
|
+
targets.set(token, { receiptOnly: receiptOnly, recovery: recovery, creationPlan: args.action === 'create' ? service.creationPlan(args.name) : null, scope: ['remove', 'reconcile', 'recover_registration'].indexOf(args.action) >= 0 ? runtimeScope(args.projectSlug) : null, accountIdentity: JSON.stringify([account && account.id, account && account.linuxUser]), intent: args, revision: revision, runtime: runtime, created: Date.now() });
|
|
88
|
+
var current = runtime && service.inspect(args.projectSlug).project;
|
|
89
|
+
return { recovery: recovery ? { operationId: recovery.operationId, action: recovery.action, phase: recovery.phase, conflict: recovery.conflict, runtimeClosureConfirmed: recovery.runtimeClosureConfirmed } : undefined, receiptOnly: receiptOnly, current: current ? { title: current.title || args.projectSlug, ownerId: current.ownerId, visibility: current.visibility || 'public', allowedUsers: current.allowedUsers || [], sessionVisibilityDefault: current.sessionVisibilityDefault || 'private', envrc: args.action === 'environment' ? current.envrc || '' : undefined } : undefined,
|
|
90
|
+
targetToken: token, action: args.action, projectSlug: args.projectSlug, name: args.name, path: args.path || (args.action === 'create' ? service.creationPlan(args.name).path : undefined), expiresInSeconds: 300,
|
|
91
|
+
details: args.action === 'remove' ? 'Removes the Clay registration and stops its runtimes. Files and stored history remain on disk. Worktree runtimes are also stopped.' : args.action === 'environment' ? 'Only newly created agent processes receive this project environment. Active processes keep their current environment.' : 'The authenticated owner must review the exact change before it is saved.' };
|
|
92
|
+
}
|
|
93
|
+
function check(target) {
|
|
94
|
+
var args = target.intent, account = permission(args.action);
|
|
95
|
+
if (target.receiptOnly) throw new Error('This token only reads the existing exact transfer receipt. Reuse its original requestId.');
|
|
96
|
+
if (args.action === 'reconcile' || args.action === 'recover_registration') {
|
|
97
|
+
if (!sameScope(target.scope, runtimeScope(args.projectSlug))) throw new Error('Recovery runtime scope changed. Inspect again.');
|
|
98
|
+
permission(args.action === 'recover_registration' ? 'rename' : target.recovery.action);
|
|
99
|
+
if (service.inspectRecovery(args.projectSlug, args.value, principal()).revision !== target.revision) throw new Error('The recovery operation changed. Inspect again.');
|
|
100
|
+
if (Date.now() - target.created > 300000) throw new Error('Recovery inspection expired.');
|
|
101
|
+
return account;
|
|
102
|
+
}
|
|
103
|
+
if (args.action === 'remove' && !sameScope(target.scope, runtimeScope(args.projectSlug))) throw new Error('Project or worktree session revisions changed. Inspect removal again.');
|
|
104
|
+
if (JSON.stringify([account && account.id, account && account.linuxUser]) !== target.accountIdentity) throw new Error('The operating account identity changed.');
|
|
105
|
+
if (Date.now() - target.created > 300000) throw new Error('Project inspection expired.');
|
|
106
|
+
if (args.action === 'register') {
|
|
107
|
+
if (!ctx.canUsePath(account && account.id, args.path) || JSON.stringify(service.registration(args.path, account)) !== target.revision) throw new Error('Registration path or access changed.');
|
|
108
|
+
} else if (args.action === 'create') {
|
|
109
|
+
if (ctx.creationRevision(JSON.parse(target.revision).anchor) !== target.revision) throw new Error('Project creation destination or registry changed. Inspect again.');
|
|
110
|
+
} else if (!target.runtime || owned(args.projectSlug) !== target.runtime || service.stamp(args.projectSlug) !== target.revision) throw new Error('The exact project revision was deleted, replaced or changed. Inspect again.');
|
|
111
|
+
return account;
|
|
112
|
+
}
|
|
113
|
+
function correlation(target, requestId) {
|
|
114
|
+
var actor = principal();
|
|
115
|
+
return { sourceKey: ctx.sourceKey, action: target.intent.action, authorize: ctx.authorize, key: hash([actor.userId || actor.singleUserOwnerId, ctx.sourceKey, requestId]), fingerprint: hash(target.intent), ownerId: actor.userId || null };
|
|
116
|
+
}
|
|
117
|
+
async function run(target, args, receipt) {
|
|
118
|
+
check(target);
|
|
119
|
+
var intent = target.intent;
|
|
120
|
+
var labels = { recover_registration: 'Cancel this pending removal and restore the current registration after acknowledged runtime closure. Queued and scheduled execution stays paused until the owner explicitly sends a new message', reconcile: 'Reconcile the exact persisted lifecycle operation. Runtime closure must be acknowledged before removal completes. No stopped work is resumed', owner: 'Transfer this ordinary project to the selected account; session owners stay unchanged', create: 'Create a private project', register: 'Register an existing directory as a private project', rename: 'Rename this project', remove: 'Remove this project registration', visibility: 'Change project visibility', allowed_users: 'Replace project sharing grants', session_visibility: 'Change the default visibility for new sessions', environment: 'Replace this project environment' };
|
|
121
|
+
var heading = target.recovery && intent.action === 'reconcile' && target.recovery.action === 'remove' ? 'Finish removing this Clay registration after confirming runtime closure. Files and stored history stay on disk.' : intent.action === 'recover_registration' ? 'Cancel the pending removal and restore the current registration. Execution stays paused until the owner sends a new message.' : labels[intent.action];
|
|
122
|
+
var question = heading + '\nTarget: ' + (intent.projectSlug || intent.name || intent.path) + '\n';
|
|
123
|
+
if (target.recovery) question += 'Original change: ' + target.recovery.action + '. Runtime closure: ' + (target.recovery.runtimeClosureConfirmed ? 'confirmed' : 'must be confirmed by the service') + '.\n' + (target.recovery.registrationTitle ? 'Current project: ' + target.recovery.registrationTitle + '\n' : '') + (target.recovery.conflict ? 'The registration changed. Removal finalization cannot overwrite it; restoring retains the current registration with execution paused.\n' : 'The saved registration revision must still match exactly.\n');
|
|
124
|
+
if (target.creationPlan) question += 'Directory: ' + target.creationPlan.path + '\nRegistration: ' + target.creationPlan.slug + '\n';
|
|
125
|
+
if (intent.value !== undefined) question += 'Exact value:\n' + (Array.isArray(intent.value) ? intent.value.join(', ') : intent.value) + '\n';
|
|
126
|
+
if (intent.action === 'remove') question += 'Runtime count: ' + target.scope.length + '. Conversation count: ' + target.scope.reduce(function (sum, entry) { return sum + entry.sessions.length; }, 0) + '.\nProject files and stored history remain on disk. All project and worktree runtimes will be stopped.\n';
|
|
127
|
+
if (intent.action === 'environment') question += 'Applies to new agent processes only; active processes keep their environment.\n';
|
|
128
|
+
question += 'Why: ' + args.why;
|
|
129
|
+
if (question.length > 2000) throw new Error('The exact project review exceeds 2000 characters. Shorten the value or reason; no change was applied.');
|
|
130
|
+
var answer = await ctx.review(question, function () { check(target); });
|
|
131
|
+
if (answer.status !== 'submitted' || !answer.answers.control || answer.answers.control.length !== 1 || answer.answers.control[0] !== 'Apply exact change') return { status: 'cancelled', action: intent.action };
|
|
132
|
+
var account = check(target);
|
|
133
|
+
return service.execute(intent, account, receipt, function () { check(target); });
|
|
134
|
+
}
|
|
135
|
+
function act(args) {
|
|
136
|
+
principal();
|
|
137
|
+
if (!args || Object.keys(args).some(function (key) { return ['targetToken', 'requestId', 'why'].indexOf(key) < 0; }) || typeof args.requestId !== 'string' || !/^[\w-]{1,128}$/.test(args.requestId) || typeof args.why !== 'string' || !args.why.trim() || args.why.length > 1000) throw new Error('Use an inspected targetToken, requestId and why. Model confirmation cannot authorize project changes.');
|
|
138
|
+
var target = targets.get(args.targetToken); if (!target) throw new Error('Inspect this exact project intent first.');
|
|
139
|
+
permission(target.intent.action);
|
|
140
|
+
var receipt = correlation(target, args.requestId);
|
|
141
|
+
var saved = service.receipt(receipt.key, receipt.fingerprint, receipt.ownerId);
|
|
142
|
+
if (saved && ['completed', 'superseded'].indexOf(saved.status) >= 0) return saved;
|
|
143
|
+
if (target.intent.action !== 'reconcile' && target.intent.action !== 'recover_registration' && !target.receiptOnly && ctx.getProjects().has(target.intent.projectSlug)) owned(target.intent.projectSlug);
|
|
144
|
+
var old = requests.get(receipt.key);
|
|
145
|
+
if (old) { if (old.fingerprint !== receipt.fingerprint) throw new Error('requestId already names another project action.'); return old.promise; }
|
|
146
|
+
if (saved) return saved;
|
|
147
|
+
if (requests.size >= 100) throw new Error('Too many project control requests in this query.');
|
|
148
|
+
var promise = run(target, args, receipt).catch(function (error) { return Object.assign({ status: 'failed', error: error.message }, error.lifecycleResult || {}); });
|
|
149
|
+
requests.set(receipt.key, { fingerprint: receipt.fingerprint, promise: promise });
|
|
150
|
+
return promise;
|
|
151
|
+
}
|
|
152
|
+
return { inspectProject: inspect, actProject: act };
|
|
153
|
+
}
|
|
154
|
+
module.exports = { attachClayProjectLifecycle: attachClayProjectLifecycle, ACTIONS: ACTIONS };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
var z = require('zod');
|
|
2
|
+
var text = z.string(), revision = z.number().int().positive();
|
|
3
|
+
var issueFields = { title: text.optional(), summary: text.optional(), body: text.optional(), resolutionSummary: text.optional(), type: z.enum(['bug', 'feature', 'plan']).optional(), priority: z.enum(['normal', 'important', 'urgent']).optional(), status: z.enum(['open', 'in_progress', 'resolved', 'closed']).optional(), closeReason: z.enum(['declined', 'duplicate', 'non_code_decision']).optional() };
|
|
4
|
+
var logFields = { title: text.optional(), summary: text.optional(), body: text.optional(), kind: text.optional(), priority: z.enum(['normal', 'important', 'urgent']).optional(), tags: text.optional() };
|
|
5
|
+
var page = { limit: z.number().int().positive().max(200).optional(), cursor: text.optional() };
|
|
6
|
+
var filters = Object.assign({}, page, { query: text.optional(), kind: text.optional(), priority: text.optional(), tag: text.optional(), contextMode: z.enum(['current', 'project', 'all']).optional() });
|
|
7
|
+
var issueFilter = Object.assign({}, page, { query: text.optional(), status: z.enum(['open', 'in_progress', 'resolved', 'closed']).optional(), type: text.optional(), priority: text.optional() });
|
|
8
|
+
var READS = {}, WRITES = {};
|
|
9
|
+
function operation(area, action, method, fields, writes) {
|
|
10
|
+
(writes ? WRITES : READS)[area + ':' + action] = method;
|
|
11
|
+
return z.object(Object.assign({ area: z.literal(area), action: z.literal(action) }, fields)).strict();
|
|
12
|
+
}
|
|
13
|
+
var readSchema = z.union([
|
|
14
|
+
operation('issues', 'list', 'listIssues', issueFilter), operation('issues', 'search', 'searchIssues', issueFilter),
|
|
15
|
+
operation('issues', 'read', 'readIssue', { ref: text }), operation('issues', 'history', 'issueHistory', { ref: text }),
|
|
16
|
+
operation('issues', 'revision', 'readIssueRevision', { ref: text, revision: revision }), operation('issues', 'feedback', 'listIssueFeedback', page),
|
|
17
|
+
operation('logs', 'list', 'listLogs', filters), operation('logs', 'search', 'searchLogs', filters),
|
|
18
|
+
operation('logs', 'read', 'readLog', { ref: text }), operation('logs', 'history', 'logHistory', Object.assign({ ref: text }, page)),
|
|
19
|
+
operation('logs', 'revision', 'readLogRevision', { ref: text, revision: revision }), operation('logs', 'feedback', 'listLogFeedback', filters),
|
|
20
|
+
]);
|
|
21
|
+
var review = { ref: text, expectedRevision: revision, commentId: text, reviewAction: z.enum(['incorporate', 'clarify', 'decline']), response: text.optional() };
|
|
22
|
+
var writeSchema = z.union([
|
|
23
|
+
operation('issues', 'create', 'createIssue', Object.assign({}, issueFields, { title: text, commitSha: text.optional() }), true),
|
|
24
|
+
operation('issues', 'update', 'updateIssue', Object.assign({}, issueFields, { ref: text, expectedRevision: revision, commitSha: text.optional() }), true),
|
|
25
|
+
operation('issues', 'review', 'reviewIssueComment', Object.assign({}, issueFields, review), true),
|
|
26
|
+
operation('issues', 'comment', 'commentIssue', { ref: text, expectedRevision: revision, body: text }, true),
|
|
27
|
+
operation('issues', 'delete', 'removeIssue', { ref: text, expectedRevision: revision }, true),
|
|
28
|
+
operation('logs', 'create', 'createLog', Object.assign({}, logFields, { title: text, kind: text, summary: text }), true),
|
|
29
|
+
operation('logs', 'update', 'updateLog', Object.assign({}, logFields, { ref: text, expectedRevision: revision }), true),
|
|
30
|
+
operation('logs', 'review', 'reviewLogComment', Object.assign({}, logFields, review), true),
|
|
31
|
+
operation('logs', 'comment', 'commentLog', { ref: text, expectedRevision: revision, body: text }, true),
|
|
32
|
+
operation('logs', 'revert', 'revertLog', { ref: text, expectedRevision: revision, revision: revision, reason: text }, true),
|
|
33
|
+
operation('logs', 'link', 'linkLog', { ref: text, expectedRevision: revision, links: text }, true),
|
|
34
|
+
operation('logs', 'delete', 'removeLog', { ref: text, expectedRevision: revision }, true),
|
|
35
|
+
]);
|
|
36
|
+
function parameters(op) {
|
|
37
|
+
var out = Object.assign({}, op); delete out.area; delete out.action;
|
|
38
|
+
if (op.area === 'logs') {
|
|
39
|
+
['tags', 'links'].forEach(function (key) { if (out[key] !== undefined) { out[key] = JSON.parse(out[key]); if (!Array.isArray(out[key])) throw new Error(key + ' must encode a JSON array.'); } });
|
|
40
|
+
}
|
|
41
|
+
if (op.action === 'review') { out.action = out.reviewAction; delete out.reviewAction; }
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
module.exports = { readSchema: readSchema, writeSchema: writeSchema, READS: READS, WRITES: WRITES, parameters: parameters };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var authority = require('./canonical-clay-record-authority');
|
|
3
|
+
var input = require('./clay-project-record-input');
|
|
4
|
+
function fingerprint(value) { return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
5
|
+
function text(value, max) { return typeof value === 'string' && value.trim() && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value); }
|
|
6
|
+
function attachClayProjectRecords(ctx) {
|
|
7
|
+
var state = ctx.state;
|
|
8
|
+
// This closure is created only after the canonical source query authorizes.
|
|
9
|
+
// Every service call rechecks it, including after commit-verification awaits.
|
|
10
|
+
var token = authority.issue(function (slug) {
|
|
11
|
+
var live = ctx.resolve(slug);
|
|
12
|
+
return Object.assign({}, live, { actor: ctx.actor() });
|
|
13
|
+
});
|
|
14
|
+
function resolve(slug, area) {
|
|
15
|
+
var live = authority.resolve(token, slug), services = ctx.services();
|
|
16
|
+
var service = services && services[area];
|
|
17
|
+
var bound = service && service.bindCanonicalClay({ projectSlug: slug, authority: token });
|
|
18
|
+
if (!bound) throw new Error('Selected project record service is unavailable.');
|
|
19
|
+
return { project: live.project, status: live.status, bound: bound };
|
|
20
|
+
}
|
|
21
|
+
function parse(args, write) {
|
|
22
|
+
ctx.authorize();
|
|
23
|
+
if (!args || Object.keys(args).some(function (key) { return key !== 'projectSlug' && key !== 'operation'; }) || !text(args.projectSlug, 200)) throw new Error('Choose an exact ordinary project and typed record operation.');
|
|
24
|
+
return JSON.parse(JSON.stringify((write ? input.writeSchema : input.readSchema).parse(args.operation)));
|
|
25
|
+
}
|
|
26
|
+
function snapshot(live, op) {
|
|
27
|
+
return op.ref ? live.bound[op.area === 'issues' ? 'readIssue' : 'readLog']({ ref: op.ref }) : null;
|
|
28
|
+
}
|
|
29
|
+
function check(target, exact) {
|
|
30
|
+
var live = resolve(target.slug, target.op.area);
|
|
31
|
+
if (live.project !== target.project || fingerprint([live.status.path, live.status.projectKnowledgeId, live.status.changeSetId, live.status.parentSlug, live.status.branch, live.status.baseCommit, live.status.headCommit]) !== target.context) throw new Error('Selected project records context changed.');
|
|
32
|
+
if (exact) {
|
|
33
|
+
if (Date.now() - target.created > 300000) throw new Error('Record inspection expired.');
|
|
34
|
+
if (fingerprint(snapshot(live, target.op)) !== target.snapshot) throw new Error('Record or comment revision changed. Inspect and review again.');
|
|
35
|
+
}
|
|
36
|
+
return live;
|
|
37
|
+
}
|
|
38
|
+
async function read(args) {
|
|
39
|
+
var op = parse(args, false), live = resolve(args.projectSlug, op.area);
|
|
40
|
+
var result = await live.bound[input.READS[op.area + ':' + op.action]](input.parameters(op));
|
|
41
|
+
var after = resolve(args.projectSlug, op.area);
|
|
42
|
+
if (after.project !== live.project || fingerprint(after.status) !== fingerprint(live.status)) throw new Error('Selected project changed during read.');
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function inspect(args) {
|
|
46
|
+
var op = parse(args, true), live = resolve(args.projectSlug, op.area), entry = snapshot(live, op);
|
|
47
|
+
if (entry && (op.area === 'issues' ? entry.revision : entry.revisions) !== op.expectedRevision) throw new Error('Record revision conflict.');
|
|
48
|
+
if (entry && entry.scheduledResult) throw new Error('Scheduled results are server-owned records.');
|
|
49
|
+
if (JSON.stringify(op).length > 1400) throw new Error('The exact record change must fit the existing owner review; use a smaller change.');
|
|
50
|
+
if (state.targets.size >= 100) state.targets.delete(state.targets.keys().next().value);
|
|
51
|
+
var id = 'records-control:' + crypto.randomUUID();
|
|
52
|
+
state.targets.set(id, { slug: args.projectSlug, op: op, project: live.project, context: fingerprint([live.status.path, live.status.projectKnowledgeId, live.status.changeSetId, live.status.parentSlug, live.status.branch, live.status.baseCommit, live.status.headCommit]), snapshot: fingerprint(entry), created: Date.now() });
|
|
53
|
+
return { targetToken: id, projectSlug: args.projectSlug, operation: op, expiresInSeconds: 300 };
|
|
54
|
+
}
|
|
55
|
+
function act(args) {
|
|
56
|
+
ctx.authorize();
|
|
57
|
+
if (!args || Object.keys(args).some(function (key) { return ['targetToken', 'requestId', 'why'].indexOf(key) < 0; }) || !text(args.targetToken, 100) || !text(args.requestId, 128) || !text(args.why, 300)) throw new Error('Use an exact record token, requestId and reason.');
|
|
58
|
+
args = Object.assign({}, args);
|
|
59
|
+
var target = state.targets.get(args.targetToken), key = fingerprint(args), previous = state.requests.get(args.requestId);
|
|
60
|
+
if (!target) throw new Error('Inspect the exact record change first.');
|
|
61
|
+
if (previous) { if (previous.key !== key) throw new Error('requestId already names another record change.'); return previous.promise.then(function (result) { check(target, false); return result; }); }
|
|
62
|
+
if (state.requests.size >= 100) throw new Error('Too many record requests in this query.');
|
|
63
|
+
var persisted = false;
|
|
64
|
+
var promise = Promise.resolve().then(async function () {
|
|
65
|
+
check(target, true);
|
|
66
|
+
var question = 'Apply this exact project record change:\n' + JSON.stringify(target.op) + '\nProject: ' + target.slug + '\nWhy: ' + args.why;
|
|
67
|
+
if (question.length > 2000) throw new Error('The exact change must fit the existing owner review.');
|
|
68
|
+
var response = await ctx.review(question, function () { check(target, true); });
|
|
69
|
+
if (response.status !== 'submitted' || !response.answers.control || response.answers.control.length !== 1 || response.answers.control[0] !== 'Apply exact change') return { status: 'cancelled', persisted: false };
|
|
70
|
+
var live = check(target, true);
|
|
71
|
+
persisted = 'unknown';
|
|
72
|
+
var result = await live.bound[input.WRITES[target.op.area + ':' + target.op.action]](input.parameters(target.op));
|
|
73
|
+
persisted = true; check(target, false);
|
|
74
|
+
if (target.op.area === 'issues' && live.project.notifyIssueUpdate) live.project.notifyIssueUpdate(result, target.op.action === 'delete' ? 'issue_deleted' : undefined);
|
|
75
|
+
if (target.op.area === 'logs' && live.project.notifyLogUpdate) live.project.notifyLogUpdate(result, target.op.action === 'review' ? target.op.reviewAction : target.op.action);
|
|
76
|
+
return { status: 'completed', persisted: true, result: result };
|
|
77
|
+
}).catch(function (error) { return { status: 'failed', persisted: persisted, retrySafe: false, error: error.message }; });
|
|
78
|
+
state.requests.set(args.requestId, { key: key, promise: promise });
|
|
79
|
+
return promise;
|
|
80
|
+
}
|
|
81
|
+
return { readProjectRecords: read, inspectProjectRecordChange: inspect, actProjectRecords: act };
|
|
82
|
+
}
|
|
83
|
+
module.exports = { attachClayProjectRecords: attachClayProjectRecords };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
var z = require('zod');
|
|
2
|
+
var str = z.string();
|
|
3
|
+
var runtime = z.object({ vendor: str, model: str, effort: str.nullable().optional() }).strict();
|
|
4
|
+
var rule = z.object({ version: z.literal(1), timezone: z.literal('server-local'), anchorDate: str, time: str,
|
|
5
|
+
recurrence: z.object({ every: z.number().int(), unit: z.enum(['day', 'week', 'month', 'year']), weekdays: z.array(z.number().int()).optional() }).strict().nullable().optional(),
|
|
6
|
+
recurrenceEnd: z.object({ type: z.enum(['until', 'after']), date: str.optional(), count: z.number().int().optional() }).strict().nullable().optional(),
|
|
7
|
+
interval: z.object({ every: z.number().int(), unit: z.enum(['minute', 'hour']), end: z.object({ type: z.enum(['until', 'after']), time: str.optional(), count: z.number().int().optional() }).strict().nullable().optional() }).strict().nullable().optional(),
|
|
8
|
+
}).strict();
|
|
9
|
+
var mateFields = { message: str.optional(), date: str.optional(), time: str.optional(), scheduleRule: rule.optional() };
|
|
10
|
+
var taskFields = { name: str.optional(), instructions: str.optional(), date: str.optional(), time: str.optional(), cron: str.nullable().optional(), scheduleRule: rule.nullable().optional(), execution: z.object({ driver: runtime, worker: runtime }).strict().optional(), skipIfRunning: z.boolean().optional() };
|
|
11
|
+
var target = z.union([z.object({ kind: z.literal('mate'), mateId: str, sessionRef: str }).strict(), z.object({ kind: z.literal('project'), projectSlug: str, sessionRef: str }).strict()]);
|
|
12
|
+
var read = z.object({ target: target, operation: z.union([z.object({ action: z.literal('list') }).strict(), z.object({ action: z.literal('receipt'), requestId: str }).strict(), z.object({ action: z.literal('read'), id: str }).strict()]) }).strict();
|
|
13
|
+
var operation = z.union([
|
|
14
|
+
z.object({ action: z.literal('create_mate'), data: z.object(Object.assign({}, mateFields, { message: str })).strict() }).strict(),
|
|
15
|
+
z.object({ action: z.literal('update_mate'), id: str, revision: str, data: z.object(mateFields).strict() }).strict(),
|
|
16
|
+
z.object({ action: z.literal('cancel_mate'), id: str, revision: str }).strict(),
|
|
17
|
+
z.object({ action: z.literal('propose'), data: z.object(Object.assign({}, taskFields, { name: str, instructions: str })).strict() }).strict(),
|
|
18
|
+
z.object({ action: z.literal('update'), id: str, revision: z.number().int().positive(), data: z.object(taskFields).strict() }).strict(),
|
|
19
|
+
z.object({ action: z.enum(['cancel', 'pause', 'resume', 'run']), id: str, revision: z.number().int().positive() }).strict(),
|
|
20
|
+
]);
|
|
21
|
+
module.exports = { readSchema: read, inspectSchema: z.object({ target: target, operation: operation }).strict(), targetSchema: target };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Persist before execution; an interrupted application never retries blindly.
|
|
2
|
+
function attachReceipts(ctx) {
|
|
3
|
+
function entries() { return ctx.session.claySchedulingReceipts || []; }
|
|
4
|
+
function save(entry) {
|
|
5
|
+
ctx.authorize();
|
|
6
|
+
var previous = ctx.session.claySchedulingReceipts;
|
|
7
|
+
var all = entries().filter(function (item) {
|
|
8
|
+
var stamp = /_(\d{13})_/.exec(item.id);
|
|
9
|
+
return item.id !== entry.id && (item.status === 'applying' || !stamp || Date.now() - Number(stamp[1]) <= 86400000);
|
|
10
|
+
});
|
|
11
|
+
if (all.length >= 100) throw new Error('Scheduling receipt capacity is full for this 24-hour window; inspect pending attempts and wait for completed receipts to expire.');
|
|
12
|
+
ctx.session.claySchedulingReceipts = all.concat([entry]);
|
|
13
|
+
if (ctx.save() !== true) { ctx.session.claySchedulingReceipts = previous; throw new Error('The scheduling retry receipt could not be saved.'); }
|
|
14
|
+
ctx.authorize();
|
|
15
|
+
}
|
|
16
|
+
function read(id, key) {
|
|
17
|
+
ctx.authorize();
|
|
18
|
+
var match = /^[A-Za-z][A-Za-z0-9-]*_(\d{13})_[A-Za-z0-9_-]{1,80}$/.exec(id || '');
|
|
19
|
+
if (!match || Math.abs(Date.now() - Number(match[1])) > 86400000) throw new Error('Scheduling requestId requires prefix_<13-digit creation time>_<unique suffix> within 24 hours.');
|
|
20
|
+
var entry = entries().find(function (item) { return item.id === id; });
|
|
21
|
+
if (entry && key !== undefined && entry.key !== key) throw new Error('requestId already names another scheduling intent.');
|
|
22
|
+
return entry;
|
|
23
|
+
}
|
|
24
|
+
return { read: read, save: save };
|
|
25
|
+
}
|
|
26
|
+
module.exports = { attachReceipts: attachReceipts };
|