@sparkelf/dsh-task-bus 0.1.0 → 0.1.2
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/client.js +673 -238
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +12 -1
- package/lib/index.js +241 -99
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, isAbsolute, join } from "node:path";
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -81,6 +81,38 @@ const open = {
|
|
|
81
81
|
type: "object",
|
|
82
82
|
additionalProperties: true
|
|
83
83
|
};
|
|
84
|
+
/** Where this workspace stores the connector's own enabled state. */
|
|
85
|
+
const CONNECTOR_STATE_PATH = join(process.env.DSH_HOME?.trim() || "/workspace/.dataops/dsh", "task-bus", "connector-state.json");
|
|
86
|
+
const DEFAULT_CONNECTOR_STATE = {
|
|
87
|
+
enabled: false,
|
|
88
|
+
capabilities: { "approval.push": true }
|
|
89
|
+
};
|
|
90
|
+
/** Read the workspace connector state; a missing/corrupt file is a safe disabled state. */
|
|
91
|
+
async function readConnectorState(path = CONNECTOR_STATE_PATH) {
|
|
92
|
+
try {
|
|
93
|
+
const text = await readFile(path, "utf8");
|
|
94
|
+
const value = JSON.parse(text);
|
|
95
|
+
return {
|
|
96
|
+
enabled: value.enabled === true,
|
|
97
|
+
capabilities: {
|
|
98
|
+
...DEFAULT_CONNECTOR_STATE.capabilities,
|
|
99
|
+
...typeof value.capabilities === "object" && value.capabilities !== null ? value.capabilities : {}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
...DEFAULT_CONNECTOR_STATE,
|
|
105
|
+
capabilities: { ...DEFAULT_CONNECTOR_STATE.capabilities }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Persist the connector state atomically; the host and UI read the same file. */
|
|
110
|
+
async function writeConnectorState(next, path = CONNECTOR_STATE_PATH) {
|
|
111
|
+
await mkdir(dirname(path), { recursive: true });
|
|
112
|
+
const temporary = path + ".tmp-" + process.pid.toString();
|
|
113
|
+
await writeFile(temporary, JSON.stringify(next, null, 2), "utf8");
|
|
114
|
+
await rename(temporary, path);
|
|
115
|
+
}
|
|
84
116
|
const renderJson = (_args, value) => [{
|
|
85
117
|
type: "text",
|
|
86
118
|
text: JSON.stringify(value, null, 2)
|
|
@@ -107,16 +139,45 @@ const apply = (ctx) => {
|
|
|
107
139
|
const url = new URL(request.url ?? "/", "http://dsh.internal");
|
|
108
140
|
const route = url.pathname.slice(17).replace(/^\/+/u, "");
|
|
109
141
|
try {
|
|
142
|
+
if (route === "connector") {
|
|
143
|
+
if (request.method === "GET") {
|
|
144
|
+
writeJson(response, 200, await readConnectorState());
|
|
145
|
+
syncTools().catch(() => {
|
|
146
|
+
disposeTools();
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (request.method === "POST") {
|
|
151
|
+
const body = await readJsonBody(request);
|
|
152
|
+
if (typeof body?.enabled !== "boolean") throw new Error("connector state requires enabled:boolean");
|
|
153
|
+
const current = await readConnectorState();
|
|
154
|
+
const capabilities = typeof body.capabilities === "object" && body.capabilities !== null ? {
|
|
155
|
+
...current.capabilities,
|
|
156
|
+
...body.capabilities
|
|
157
|
+
} : current.capabilities;
|
|
158
|
+
await writeConnectorState({
|
|
159
|
+
enabled: body.enabled,
|
|
160
|
+
capabilities
|
|
161
|
+
});
|
|
162
|
+
await syncTools();
|
|
163
|
+
writeJson(response, 200, await readConnectorState());
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
writeJson(response, 405, { error: "connector state supports GET and POST" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
110
169
|
if (request.method === "GET" && route === "health") {
|
|
111
170
|
resolveBase();
|
|
112
171
|
writeJson(response, 200, {
|
|
113
172
|
ok: true,
|
|
114
173
|
source: "dataops_approval"
|
|
115
174
|
});
|
|
175
|
+
syncTools();
|
|
116
176
|
return;
|
|
117
177
|
}
|
|
118
178
|
if (request.method === "GET" && route === "overview") {
|
|
119
179
|
writeJson(response, 200, absoluteApprovalLinks(await client("/overview")));
|
|
180
|
+
syncTools();
|
|
120
181
|
return;
|
|
121
182
|
}
|
|
122
183
|
if (request.method === "GET" && route === "attachment-materialize") {
|
|
@@ -237,107 +298,188 @@ const apply = (ctx) => {
|
|
|
237
298
|
}
|
|
238
299
|
}
|
|
239
300
|
}), "task-bus: browser API");
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Tool definitions are static; only their registration is permission-dependent.
|
|
303
|
+
*
|
|
304
|
+
* This is the plugin-side disclosure boundary: the model never sees a tool the
|
|
305
|
+
* current DataOps principal cannot use. Authorization itself remains at the
|
|
306
|
+
* DataOps Task Center API; this layer only adjusts what the connector contributes.
|
|
307
|
+
*/
|
|
308
|
+
const toolDefinitions = [
|
|
309
|
+
{
|
|
310
|
+
permission: "workflow:task:read",
|
|
311
|
+
definition: tool("task_center_list", "List DataOps Task Center cards visible to the current workspace user, including approval tasks and generic Task Bus tasks. Approval cards are read-only projections of DataOps tasks.", {
|
|
312
|
+
status: {
|
|
313
|
+
type: "string",
|
|
314
|
+
enum: [
|
|
315
|
+
"all",
|
|
316
|
+
"pending",
|
|
317
|
+
"done"
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
scope: {
|
|
321
|
+
type: "string",
|
|
322
|
+
enum: [
|
|
323
|
+
"mine",
|
|
324
|
+
"created",
|
|
325
|
+
"all"
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
limit: { type: "number" }
|
|
329
|
+
}, async (args) => {
|
|
330
|
+
const status = typeof args.status === "string" ? args.status : "all";
|
|
331
|
+
const limit = typeof args.limit === "number" ? Math.max(1, Math.min(50, args.limit)) : 20;
|
|
332
|
+
const result = absoluteApprovalLinks(await client("/overview"));
|
|
333
|
+
const items = (result.items ?? []).filter((item) => {
|
|
334
|
+
if (status === "pending") return item.status !== "completed" && item.status !== "cancelled";
|
|
335
|
+
if (status === "done") return item.status === "completed" || item.status === "cancelled";
|
|
336
|
+
return true;
|
|
337
|
+
}).slice(0, limit);
|
|
338
|
+
return {
|
|
339
|
+
...result,
|
|
340
|
+
items,
|
|
341
|
+
total: items.length
|
|
342
|
+
};
|
|
343
|
+
})
|
|
248
344
|
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
345
|
+
{
|
|
346
|
+
permission: "workflow:task:read",
|
|
347
|
+
definition: tool("task_center_get", "Read one Task Center card by its stable id. Use approval:<id> for a DataOps approval task or task:<uuid> for a generic Task Bus task.", { id: {
|
|
348
|
+
type: "string",
|
|
349
|
+
required: true
|
|
350
|
+
} }, async (args) => {
|
|
351
|
+
const id = String(args.id ?? "");
|
|
352
|
+
if (id.startsWith("approval:")) return client(`/items/${encodeURIComponent(id)}`);
|
|
353
|
+
if (id.startsWith("task:")) return client(`/generic/${encodeURIComponent(id.slice(5))}`);
|
|
354
|
+
throw new Error("Task Center id must start with approval: or task:");
|
|
355
|
+
})
|
|
256
356
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
type: "string",
|
|
291
|
-
required: true
|
|
357
|
+
{
|
|
358
|
+
permission: "workflow:task:create",
|
|
359
|
+
definition: tool("task_center_create", [
|
|
360
|
+
"Create a generic Task Center card ONLY when the user explicitly asks you to create a task/card in this turn.",
|
|
361
|
+
"Never call this tool proactively, speculatively, as a side effect of another request, or merely because work could be tracked.",
|
|
362
|
+
"If the user did not explicitly ask for a new card, do not call it; ask for confirmation instead.",
|
|
363
|
+
"This never creates or mutates a DataOps approval task."
|
|
364
|
+
].join(" "), {
|
|
365
|
+
title: {
|
|
366
|
+
type: "string",
|
|
367
|
+
required: true
|
|
368
|
+
},
|
|
369
|
+
description: { type: "string" },
|
|
370
|
+
source_type: { type: "string" },
|
|
371
|
+
source_id: { type: "string" },
|
|
372
|
+
assignee_user_id: { type: "string" },
|
|
373
|
+
priority: { type: "string" },
|
|
374
|
+
user_requested: {
|
|
375
|
+
type: "boolean",
|
|
376
|
+
required: true,
|
|
377
|
+
description: "Must be true only when the user explicitly requested creation of this card in the current turn."
|
|
378
|
+
}
|
|
379
|
+
}, async (args) => {
|
|
380
|
+
if (args.user_requested !== true) throw new Error("task_center_create requires an explicit user request; do not create Task Center cards proactively");
|
|
381
|
+
return client("/generic", "POST", {
|
|
382
|
+
title: String(args.title ?? ""),
|
|
383
|
+
description: typeof args.description === "string" ? args.description : void 0,
|
|
384
|
+
sourceType: typeof args.source_type === "string" ? args.source_type : "agent",
|
|
385
|
+
sourceId: typeof args.source_id === "string" ? args.source_id : void 0,
|
|
386
|
+
assigneeUserId: typeof args.assignee_user_id === "string" ? args.assignee_user_id : void 0,
|
|
387
|
+
priority: typeof args.priority === "string" ? args.priority : void 0
|
|
388
|
+
});
|
|
389
|
+
})
|
|
292
390
|
},
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
391
|
+
{
|
|
392
|
+
permission: "workflow:task:execute",
|
|
393
|
+
definition: tool("task_center_transition", "Advance a generic Task Bus task. Approval tasks must be handled through the existing DataOps approval UI and APIs; this tool refuses approval ids.", {
|
|
394
|
+
id: {
|
|
395
|
+
type: "string",
|
|
396
|
+
required: true
|
|
397
|
+
},
|
|
398
|
+
action: {
|
|
399
|
+
type: "string",
|
|
400
|
+
required: true,
|
|
401
|
+
enum: [
|
|
402
|
+
"accept",
|
|
403
|
+
"start",
|
|
404
|
+
"submit",
|
|
405
|
+
"complete",
|
|
406
|
+
"cancel",
|
|
407
|
+
"block",
|
|
408
|
+
"unblock"
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
comment: { type: "string" }
|
|
412
|
+
}, async (args) => {
|
|
413
|
+
const id = String(args.id ?? "");
|
|
414
|
+
if (!id.startsWith("task:")) throw new Error("Only generic task:<uuid> cards can be transitioned here");
|
|
415
|
+
return client(`/generic/${encodeURIComponent(id.slice(5))}/transition`, "POST", {
|
|
416
|
+
action: String(args.action ?? ""),
|
|
417
|
+
comment: typeof args.comment === "string" ? args.comment : void 0
|
|
418
|
+
});
|
|
419
|
+
})
|
|
302
420
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
421
|
+
];
|
|
422
|
+
const toolDisposers = /* @__PURE__ */ new Map();
|
|
423
|
+
let syncGeneration = 0;
|
|
424
|
+
const disposeTools = () => {
|
|
425
|
+
for (const dispose of toolDisposers.values()) dispose();
|
|
426
|
+
toolDisposers.clear();
|
|
427
|
+
};
|
|
428
|
+
/** Read the current principal's DataOps permission keys without caching them per tool call. */
|
|
429
|
+
const loadPermissionKeys = async () => {
|
|
430
|
+
try {
|
|
431
|
+
const token = (await host.credentials.resolve(credentialRef(DATAOPS_CREDENTIAL_REF)))?.value?.trim();
|
|
432
|
+
if (!token) return null;
|
|
433
|
+
const response = await fetch(resolveBase() + "/api/ai/review/context", { headers: {
|
|
434
|
+
authorization: `Bearer ${token}`,
|
|
435
|
+
accept: "application/json"
|
|
436
|
+
} });
|
|
437
|
+
if (!response.ok) return null;
|
|
438
|
+
const body = await response.json();
|
|
439
|
+
const values = Array.isArray(body.permissions) ? body.permissions : Array.isArray(body.permissionKeys) ? body.permissionKeys : null;
|
|
440
|
+
return values === null ? null : new Set(values.filter((item) => typeof item === "string"));
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Reconcile connector state, principal permissions and the registered tool set.
|
|
447
|
+
*
|
|
448
|
+
* Called at plugin start, after every browser API request, and after a connector
|
|
449
|
+
* enable/disable change. The tool execution path never repeats this work.
|
|
450
|
+
*/
|
|
451
|
+
const syncTools = async () => {
|
|
452
|
+
const generation = ++syncGeneration;
|
|
453
|
+
const state = await readConnectorState();
|
|
454
|
+
if (!state.enabled || state.capabilities["approval.push"] === false) {
|
|
455
|
+
disposeTools();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const permissions = await loadPermissionKeys();
|
|
459
|
+
if (permissions === null) {
|
|
460
|
+
disposeTools();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (generation !== syncGeneration) return;
|
|
464
|
+
for (const item of toolDefinitions) {
|
|
465
|
+
const existing = toolDisposers.get(item.definition.name);
|
|
466
|
+
if (permissions.has(item.permission)) {
|
|
467
|
+
if (existing === void 0) toolDisposers.set(item.definition.name, host.tools.register(item.definition));
|
|
468
|
+
} else if (existing !== void 0) {
|
|
469
|
+
existing();
|
|
470
|
+
toolDisposers.delete(item.definition.name);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
ctx.effect(() => {
|
|
475
|
+
syncTools().catch(() => {
|
|
476
|
+
disposeTools();
|
|
339
477
|
});
|
|
340
|
-
|
|
478
|
+
return () => {
|
|
479
|
+
syncGeneration += 1;
|
|
480
|
+
disposeTools();
|
|
481
|
+
};
|
|
482
|
+
}, "task-bus: permission-aware tool disclosure");
|
|
341
483
|
};
|
|
342
484
|
//#endregion
|
|
343
|
-
export { TASK_BUS_ROUTE, apply, inject, name };
|
|
485
|
+
export { CONNECTOR_STATE_PATH, TASK_BUS_ROUTE, apply, inject, name, readConnectorState, writeConnectorState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-task-bus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DataOps Task Center and Task Bus for the official DSH Web shell.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"cordis.patch.yml",
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
+
},
|
|
40
44
|
"engines": {
|
|
41
45
|
"node": "^22.19.0 || >=24.0.0"
|
|
42
46
|
},
|
|
@@ -81,9 +85,5 @@
|
|
|
81
85
|
"@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
|
|
82
86
|
"@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
|
|
83
87
|
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2"
|
|
84
|
-
},
|
|
85
|
-
"scripts": {
|
|
86
|
-
"build": "tsdown",
|
|
87
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
88
88
|
}
|
|
89
|
-
}
|
|
89
|
+
}
|