task-handlers.mjs
179 lines 4.6 KB
Raw
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 10 days ago
1 /**
2 * Shared Task list/get handlers — CLI = MCP = Hub REST parity (Phase 2G-b).
3 *
4 * @see docs/TASK-STORE-CONTRACT-2G.md §3
5 */
6
7 import {
8 listTasks,
9 getTask,
10 taskForClient,
11 taskGetEffectiveScope,
12 TASK_ID_RE,
13 MAX_TASK_SUMMARIES,
14 TASK_STATUSES,
15 TASK_KINDS,
16 } from './task-store.mjs';
17 import { resolveFlowScopeQuery } from '../flow/flow-scope.mjs';
18 import { resolveHandlerVisibleScopes } from '../flow/flow-handlers.mjs';
19
20 /**
21 * @typedef {import('../flow/flow-scope.mjs').FlowScope} FlowScope
22 */
23
24 /**
25 * @param {{
26 * dataDir: string,
27 * vaultId: string,
28 * userId?: string,
29 * role?: string,
30 * cliScopes?: FlowScope[],
31 * visibleScopes?: Set<FlowScope>,
32 * ambiguous?: boolean,
33 * scope?: string,
34 * workspace_id?: string,
35 * workspaceId?: string,
36 * status?: string,
37 * kind?: string,
38 * limit?: number,
39 * starterDir?: string,
40 * }} input
41 * @returns {{ ok: true, payload: object } | { ok: false, status: number, error: string, code: string }}
42 */
43 export function handleTaskListRequest(input) {
44 const resolved = resolveHandlerVisibleScopes(input);
45 if (resolved.ambiguous) {
46 return {
47 ok: false,
48 status: 400,
49 error: 'Ambiguous task scope',
50 code: 'TASK_SCOPE_AMBIGUOUS',
51 };
52 }
53
54 const scopeQuery = resolveFlowScopeQuery(resolved.visibleScopes, input.scope);
55 if (!scopeQuery.ok) {
56 const code = scopeQuery.code === 'FLOW_SCOPE_DENIED' ? 'TASK_SCOPE_DENIED' : scopeQuery.code;
57 const error =
58 scopeQuery.code === 'FLOW_SCOPE_DENIED' ? 'Task scope not authorized' : scopeQuery.error;
59 return { ok: false, status: scopeQuery.status, error, code };
60 }
61
62 const status = typeof input.status === 'string' ? input.status.trim() : '';
63 if (status && !TASK_STATUSES.includes(/** @type {typeof TASK_STATUSES[number]} */ (status))) {
64 return {
65 ok: false,
66 status: 400,
67 error: 'Invalid status',
68 code: 'BAD_REQUEST',
69 };
70 }
71
72 const kind = typeof input.kind === 'string' ? input.kind.trim() : '';
73 if (kind && !TASK_KINDS.includes(/** @type {typeof TASK_KINDS[number]} */ (kind))) {
74 return {
75 ok: false,
76 status: 400,
77 error: 'Invalid kind',
78 code: 'BAD_REQUEST',
79 };
80 }
81
82 let limit = input.limit;
83 if (limit !== undefined && limit !== null) {
84 if (!Number.isInteger(limit) || limit < 1 || limit > MAX_TASK_SUMMARIES) {
85 return {
86 ok: false,
87 status: 400,
88 error: `limit must be an integer between 1 and ${MAX_TASK_SUMMARIES}`,
89 code: 'BAD_REQUEST',
90 };
91 }
92 }
93
94 const workspaceId =
95 (typeof input.workspaceId === 'string' ? input.workspaceId : input.workspace_id) ?? undefined;
96
97 const payload = listTasks(input.dataDir, input.vaultId, {
98 visibleScopes: resolved.visibleScopes,
99 filterScopes: scopeQuery.filterScopes,
100 effectiveScope: scopeQuery.effectiveScope,
101 workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : undefined,
102 status: status || undefined,
103 kind: kind || undefined,
104 limit,
105 starterDir: input.starterDir,
106 });
107
108 return { ok: true, payload };
109 }
110
111 /**
112 * @param {{
113 * dataDir: string,
114 * vaultId: string,
115 * taskId: string,
116 * userId?: string,
117 * role?: string,
118 * cliScopes?: FlowScope[],
119 * visibleScopes?: Set<FlowScope>,
120 * ambiguous?: boolean,
121 * starterDir?: string,
122 * }} input
123 * @returns {{ ok: true, payload: object } | { ok: false, status: number, error: string, code: string }}
124 */
125 export function handleTaskGetRequest(input) {
126 const taskId = typeof input.taskId === 'string' ? input.taskId.trim() : '';
127 if (!taskId || !TASK_ID_RE.test(taskId)) {
128 return {
129 ok: false,
130 status: 400,
131 error: 'Invalid task id',
132 code: 'BAD_REQUEST',
133 };
134 }
135
136 const resolved = resolveHandlerVisibleScopes(input);
137 if (resolved.ambiguous) {
138 return {
139 ok: false,
140 status: 400,
141 error: 'Ambiguous task scope',
142 code: 'TASK_SCOPE_AMBIGUOUS',
143 };
144 }
145
146 const stored = getTask(input.dataDir, input.vaultId, taskId, {
147 visibleScopes: resolved.visibleScopes,
148 starterDir: input.starterDir,
149 });
150
151 if (!stored) {
152 return {
153 ok: false,
154 status: 404,
155 error: 'unknown_task',
156 code: 'unknown_task',
157 };
158 }
159
160 return {
161 ok: true,
162 payload: {
163 schema: 'knowtation.task_get/v0',
164 vault_id: input.vaultId,
165 effective_scope: taskGetEffectiveScope(resolved.visibleScopes),
166 task: taskForClient(stored),
167 },
168 };
169 }
170
171 /**
172 * Serialize payload for byte-identical parity (stable key order via JSON.stringify).
173 *
174 * @param {object} payload
175 * @returns {string}
176 */
177 export function serializeTaskPayload(payload) {
178 return JSON.stringify(payload);
179 }
File History 1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 10 days ago