@sparkelf/dsh-dataops-managed 0.3.20 → 0.3.21
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 +3974 -255
- package/lib/client.js.map +1 -1
- package/lib/index.js +1415 -24
- package/lib/types/auth-state.d.ts +37 -0
- package/lib/types/client/NotificationBell.d.ts +19 -0
- package/lib/types/client/PermissionCenterIcon.d.ts +9 -0
- package/lib/types/client/PermissionCenterSection.d.ts +15 -0
- package/lib/types/client/ReloginPrompt.d.ts +29 -0
- package/lib/types/client/ReviewSettingsCard.d.ts +12 -0
- package/lib/types/client/SkillPlazaSection.d.ts +3 -5
- package/lib/types/client/WanxiangBrand.d.ts +7 -1
- package/lib/types/client/WorkspaceUpgradePrompt.d.ts +21 -0
- package/lib/types/client/combobox.d.ts +17 -0
- package/lib/types/client/index.d.ts +17 -2
- package/lib/types/client/locales.d.ts +103 -0
- package/lib/types/client/review-client.d.ts +155 -0
- package/lib/types/client/review-demo.d.ts +13 -0
- package/lib/types/client/review-notification-store.d.ts +23 -0
- package/lib/types/client/review-types.d.ts +114 -0
- package/lib/types/client/settings-gate.d.ts +21 -0
- package/lib/types/client/skill-plaza-client.d.ts +21 -0
- package/lib/types/client/workspace-upgrade-client.d.ts +29 -0
- package/lib/types/index.d.ts +36 -0
- package/lib/types/review-api.d.ts +32 -0
- package/lib/types/skill-plaza.d.ts +31 -0
- package/lib/types/tools.d.ts +21 -0
- package/lib/types/workspace-upgrade-state.d.ts +66 -0
- package/lib/types/workspace-upgrade.d.ts +83 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import * as McpClient from "@sparkelf/dsh-plugin-mcp-credentials";
|
|
5
5
|
import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
|
|
6
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
//#region src/auth-state.ts
|
|
11
|
+
let state = "unknown";
|
|
12
|
+
/**
|
|
13
|
+
* Record that a DataOps call was accepted.
|
|
14
|
+
*
|
|
15
|
+
* Called on any non-401 answer, including ones that carry a refusal for another reason: a 403
|
|
16
|
+
* says the session is fine and the permission is not, so it must not raise the sign-in prompt.
|
|
17
|
+
*/
|
|
18
|
+
const noteAccepted = () => {
|
|
19
|
+
state = "valid";
|
|
20
|
+
};
|
|
21
|
+
/** Record that DataOps refused the credential, which means the session is over. */
|
|
22
|
+
const noteExpired = () => {
|
|
23
|
+
state = "expired";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Read the current observation.
|
|
27
|
+
*
|
|
28
|
+
* @returns `unknown` before any call has completed, then what the last one said.
|
|
29
|
+
*/
|
|
30
|
+
const currentAuthState = () => state;
|
|
31
|
+
//#endregion
|
|
7
32
|
//#region src/model-settings.ts
|
|
8
33
|
/** Profile entry id whose Config holds the model providers. */
|
|
9
34
|
const MODEL_SETTINGS_NAMESPACE = "llm-pi-ai";
|
|
@@ -191,7 +216,11 @@ var ModelSync = class {
|
|
|
191
216
|
method: "POST",
|
|
192
217
|
headers: { authorization: `Bearer ${token}` }
|
|
193
218
|
});
|
|
194
|
-
if (response.status === 401)
|
|
219
|
+
if (response.status === 401) {
|
|
220
|
+
noteExpired();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
noteAccepted();
|
|
195
224
|
if (!response.ok) throw new Error(`Shared model read failed with HTTP ${String(response.status)}`);
|
|
196
225
|
const body = asRecord$3(await response.json());
|
|
197
226
|
if (body === null) return void 0;
|
|
@@ -258,7 +287,11 @@ var ModelSync = class {
|
|
|
258
287
|
const token = await this.token();
|
|
259
288
|
if (token === void 0) return null;
|
|
260
289
|
const response = await fetch(new URL("/api/ai/shared-models/publication", this.apiBase()), { headers: { authorization: `Bearer ${token}` } });
|
|
261
|
-
if (response.status === 401)
|
|
290
|
+
if (response.status === 401) {
|
|
291
|
+
noteExpired();
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
noteAccepted();
|
|
262
295
|
if (!response.ok) throw new Error(`Shared model status read failed with HTTP ${String(response.status)}`);
|
|
263
296
|
const body = asRecord$3(await response.json());
|
|
264
297
|
return {
|
|
@@ -605,7 +638,11 @@ var SkillPlaza = class {
|
|
|
605
638
|
...init.headers
|
|
606
639
|
}
|
|
607
640
|
});
|
|
608
|
-
if (response.status === 401)
|
|
641
|
+
if (response.status === 401) {
|
|
642
|
+
noteExpired();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
noteAccepted();
|
|
609
646
|
if (!response.ok) throw new Error(`Skill plaza request failed with HTTP ${String(response.status)}`);
|
|
610
647
|
return response.status === 204 ? null : await response.json();
|
|
611
648
|
}
|
|
@@ -615,13 +652,30 @@ var SkillPlaza = class {
|
|
|
615
652
|
* @returns Entries, newest first; empty when the workspace is not authorized.
|
|
616
653
|
*/
|
|
617
654
|
async list(options = {}) {
|
|
655
|
+
return (await this.listPage(options)).skills;
|
|
656
|
+
}
|
|
657
|
+
/** List with the server's tab counts, which the plaza shows without re-counting a page. */
|
|
658
|
+
async listPage(options = {}) {
|
|
618
659
|
const params = new URLSearchParams();
|
|
619
660
|
if (options.tag !== void 0 && options.tag !== "") params.set("tag", options.tag);
|
|
620
661
|
if (options.query !== void 0 && options.query !== "") params.set("q", options.query);
|
|
662
|
+
if (options.status !== void 0 && options.status !== "") params.set("status", options.status);
|
|
663
|
+
if (options.scope !== void 0 && options.scope !== "") params.set("scope", options.scope);
|
|
664
|
+
if (options.departmentCode !== void 0 && options.departmentCode !== "") params.set("departmentCode", options.departmentCode);
|
|
621
665
|
const suffix = params.size === 0 ? "" : `?${params.toString()}`;
|
|
622
666
|
const body = await this.call(`/api/ai/skill-plaza/skills${suffix}`);
|
|
623
|
-
|
|
624
|
-
|
|
667
|
+
const record = asRecord$1(body);
|
|
668
|
+
const rows = Array.isArray(body) ? body : Array.isArray(record?.items) ? record.items : [];
|
|
669
|
+
const rawCounts = asRecord$1(record?.counts);
|
|
670
|
+
return {
|
|
671
|
+
skills: rows.flatMap((row) => this.toPlazaSkill(row)),
|
|
672
|
+
...rawCounts === null ? {} : { counts: {
|
|
673
|
+
pending: Number(rawCounts.pending ?? 0),
|
|
674
|
+
approved: Number(rawCounts.approved ?? 0),
|
|
675
|
+
rejected: Number(rawCounts.rejected ?? 0),
|
|
676
|
+
total: Number(rawCounts.total ?? rows.length)
|
|
677
|
+
} }
|
|
678
|
+
};
|
|
625
679
|
}
|
|
626
680
|
/**
|
|
627
681
|
* Every tag in use, so the browser offers existing labels.
|
|
@@ -680,11 +734,583 @@ var SkillPlaza = class {
|
|
|
680
734
|
instruction: typeof row.instruction === "string" ? row.instruction : "",
|
|
681
735
|
tags: Array.isArray(row.tags) ? row.tags.filter((tag) => typeof tag === "string") : [],
|
|
682
736
|
sharedByMe: row.sharedByMe === true,
|
|
683
|
-
updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : ""
|
|
737
|
+
updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
|
|
738
|
+
reviewStatus: row.reviewStatus === "pending" || row.reviewStatus === "rejected" ? row.reviewStatus : "approved",
|
|
739
|
+
visibility: row.visibility === "department" || row.visibility === "private" ? row.visibility : "global",
|
|
740
|
+
departmentCode: typeof row.departmentCode === "string" ? row.departmentCode : null,
|
|
741
|
+
departmentName: typeof row.departmentName === "string" ? row.departmentName : void 0,
|
|
742
|
+
departmentPath: Array.isArray(row.departmentPath) ? row.departmentPath.flatMap((entry) => {
|
|
743
|
+
const node = asRecord$1(entry);
|
|
744
|
+
return node !== null && typeof node.name === "string" ? [{
|
|
745
|
+
code: typeof node.code === "string" ? node.code : "",
|
|
746
|
+
name: node.name
|
|
747
|
+
}] : [];
|
|
748
|
+
}) : void 0,
|
|
749
|
+
ownerName: typeof row.ownerName === "string" ? row.ownerName : void 0,
|
|
750
|
+
riskReport: row.riskReport ?? null,
|
|
751
|
+
reviewNote: typeof row.reviewNote === "string" ? row.reviewNote : null,
|
|
752
|
+
submittedAt: typeof row.submittedAt === "string" ? row.submittedAt : void 0
|
|
684
753
|
}];
|
|
685
754
|
}
|
|
686
755
|
};
|
|
687
756
|
//#endregion
|
|
757
|
+
//#region src/review-api.ts
|
|
758
|
+
/** Cookie-free same-origin bridge to the review endpoints. */
|
|
759
|
+
var ReviewApi = class {
|
|
760
|
+
ctx;
|
|
761
|
+
config;
|
|
762
|
+
constructor(ctx, config) {
|
|
763
|
+
this.ctx = ctx;
|
|
764
|
+
this.config = config;
|
|
765
|
+
}
|
|
766
|
+
apiBase() {
|
|
767
|
+
return new URL(this.config.baseUrl).origin;
|
|
768
|
+
}
|
|
769
|
+
async token() {
|
|
770
|
+
return (await this.ctx.credentials.resolve(credentialRef(this.config.credentialRef)))?.value;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Call one DataOps endpoint with the workspace JWT.
|
|
774
|
+
* @param path - API path below the DataOps origin.
|
|
775
|
+
* @param init - Method, body and extra headers.
|
|
776
|
+
* @returns The upstream status and parsed body; 503 when this workspace has no session.
|
|
777
|
+
*/
|
|
778
|
+
async request(path, init = {}) {
|
|
779
|
+
const token = await this.token();
|
|
780
|
+
if (token === void 0) return {
|
|
781
|
+
status: 503,
|
|
782
|
+
body: { error: "DataOps is not connected" }
|
|
783
|
+
};
|
|
784
|
+
const response = await fetch(new URL(path, this.apiBase()), {
|
|
785
|
+
...init,
|
|
786
|
+
headers: {
|
|
787
|
+
authorization: `Bearer ${token}`,
|
|
788
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
789
|
+
...init.headers
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
if (response.status === 401) {
|
|
793
|
+
noteExpired();
|
|
794
|
+
return {
|
|
795
|
+
status: 401,
|
|
796
|
+
body: { error: "DataOps session expired" }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
noteAccepted();
|
|
800
|
+
const text = await response.text();
|
|
801
|
+
let body = null;
|
|
802
|
+
if (text !== "") try {
|
|
803
|
+
body = JSON.parse(text);
|
|
804
|
+
} catch {
|
|
805
|
+
body = { error: text };
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
status: response.status,
|
|
809
|
+
body
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/tools.ts
|
|
815
|
+
const open = {
|
|
816
|
+
type: "object",
|
|
817
|
+
additionalProperties: true
|
|
818
|
+
};
|
|
819
|
+
const render = (_args, value) => [{
|
|
820
|
+
type: "text",
|
|
821
|
+
text: JSON.stringify(value, null, 2)
|
|
822
|
+
}];
|
|
823
|
+
const roleSet = (scope) => new Set(scope.roleCodes ?? []);
|
|
824
|
+
const isSuper = (scope) => roleSet(scope).has("超级管理员") || scope.primaryRole === "超级管理员";
|
|
825
|
+
const isAdmin = (scope) => isSuper(scope) || roleSet(scope).has("管理员") || scope.primaryRole === "管理员";
|
|
826
|
+
const isDeptAdmin = (scope) => roleSet(scope).has("部门管理员") || scope.primaryRole === "部门管理员";
|
|
827
|
+
const canReview = (scope) => scope.review?.canReview === true || isAdmin(scope) || isDeptAdmin(scope);
|
|
828
|
+
const forbidden = (message) => /* @__PURE__ */ new Error("Forbidden (403): " + message);
|
|
829
|
+
/** Permission codes from the context, tolerating either field name during rollout. */
|
|
830
|
+
const permissionsOf = (scope) => new Set(scope.permissions ?? scope.permissionKeys ?? []);
|
|
831
|
+
/**
|
|
832
|
+
* Whether the caller holds one `skill:*` capability.
|
|
833
|
+
*
|
|
834
|
+
* The context is the authority once it carries a permission list; before task-70 ships,
|
|
835
|
+
* the role-name fallback keeps the existing four-role behavior. A present-but-empty list is
|
|
836
|
+
* a denial, not a reason to fall back: an authenticated user with no keys must not inherit
|
|
837
|
+
* administrator powers from their role label alone.
|
|
838
|
+
*/
|
|
839
|
+
const hasSkillPermission = (scope, key) => {
|
|
840
|
+
const permissions = permissionsOf(scope);
|
|
841
|
+
if (scope.permissions !== void 0 || scope.permissionKeys !== void 0) return permissions.has(key);
|
|
842
|
+
if (isAdmin(scope)) return true;
|
|
843
|
+
if (isDeptAdmin(scope)) return key !== "skill:review:global";
|
|
844
|
+
return key === "skill:read" || key === "skill:install" || key === "skill:submit";
|
|
845
|
+
};
|
|
846
|
+
const requireSkillPermission = (scope, key) => {
|
|
847
|
+
if (!hasSkillPermission(scope, key)) throw forbidden("missing permission " + key);
|
|
848
|
+
};
|
|
849
|
+
/** Fetch and validate the caller's DataOps scope. */
|
|
850
|
+
async function loadScope(api) {
|
|
851
|
+
const result = await api.request("/api/ai/review/context");
|
|
852
|
+
if (result.status === 401) throw new Error("Unauthorized (401): DataOps session expired");
|
|
853
|
+
if (result.status === 403) throw forbidden("no review context");
|
|
854
|
+
if (result.status !== 200) throw new Error("review context failed: HTTP " + result.status.toString());
|
|
855
|
+
const body = result.body;
|
|
856
|
+
if (body === null || typeof body.roleCodes !== "object" || !Array.isArray(body.roleCodes)) throw new Error("review context is missing roleCodes");
|
|
857
|
+
return body;
|
|
858
|
+
}
|
|
859
|
+
/** Parse one plaza response body into rows without trusting its shape. */
|
|
860
|
+
function rowsOf(body) {
|
|
861
|
+
if (Array.isArray(body)) return body.filter((row) => typeof row === "object" && row !== null);
|
|
862
|
+
if (typeof body === "object" && body !== null && Array.isArray(body.skills)) return body.skills.filter((row) => typeof row === "object" && row !== null);
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
/** Register the skill-centre and review tools. */
|
|
866
|
+
function registerAiManagementTools(ctx, reviewApi) {
|
|
867
|
+
const disposers = [];
|
|
868
|
+
const register = (tool) => {
|
|
869
|
+
disposers.push(ctx.tools.register(tool));
|
|
870
|
+
};
|
|
871
|
+
const approve = async (tool, exec, reason) => {
|
|
872
|
+
if (exec.agent === void 0) throw new Error(tool + " requires an initiating agent");
|
|
873
|
+
const outcome = await ctx.approval.request({
|
|
874
|
+
agent: exec.agent,
|
|
875
|
+
toolName: tool,
|
|
876
|
+
callId: exec.callId,
|
|
877
|
+
signal: exec.signal,
|
|
878
|
+
reason
|
|
879
|
+
});
|
|
880
|
+
if (outcome !== "allowed-once") throw new Error(tool + " approval was " + outcome);
|
|
881
|
+
};
|
|
882
|
+
const audit = (tool, exec, detail) => {
|
|
883
|
+
try {
|
|
884
|
+
const agent = exec.agent;
|
|
885
|
+
ctx.logger.info("[audit] " + tool + " " + JSON.stringify({
|
|
886
|
+
sessionId: agent?.session?.id ?? null,
|
|
887
|
+
callId: exec.callId,
|
|
888
|
+
...detail
|
|
889
|
+
}));
|
|
890
|
+
} catch {}
|
|
891
|
+
};
|
|
892
|
+
const call = async (path, init = {}) => {
|
|
893
|
+
const result = await reviewApi.request(path, init);
|
|
894
|
+
if (result.status === 401) throw new Error("Unauthorized (401): DataOps session expired");
|
|
895
|
+
const detail = result.body;
|
|
896
|
+
const reason = typeof detail?.error === "string" ? detail.error : typeof detail?.message === "string" ? detail.message : "";
|
|
897
|
+
if (result.status === 403) throw forbidden(reason || "the current role may not perform this action");
|
|
898
|
+
if (result.status === 409) throw new Error("Conflict (409): " + (reason || "the skill still has installations"));
|
|
899
|
+
if (result.status < 200 || result.status >= 300) throw new Error("DataOps " + path + " failed: HTTP " + result.status.toString() + (reason ? ": " + reason : ""));
|
|
900
|
+
return result.body;
|
|
901
|
+
};
|
|
902
|
+
const listSkills = async (scope, includeLocal) => {
|
|
903
|
+
requireSkillPermission(scope, "skill:read");
|
|
904
|
+
const mine = rowsOf(await call("/api/ai/skill-plaza/skills?scope=mine"));
|
|
905
|
+
const publicRows = rowsOf(await call("/api/ai/skill-plaza/skills"));
|
|
906
|
+
const admin = isAdmin(scope) ? rowsOf(await call("/api/ai/skill-plaza/admin/skills")) : [];
|
|
907
|
+
const byId = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const row of [
|
|
909
|
+
...mine,
|
|
910
|
+
...publicRows,
|
|
911
|
+
...admin
|
|
912
|
+
]) if (typeof row.id === "number") byId.set(row.id, {
|
|
913
|
+
...byId.get(row.id),
|
|
914
|
+
...row
|
|
915
|
+
});
|
|
916
|
+
const rows = [...byId.values()];
|
|
917
|
+
return {
|
|
918
|
+
catalog: {
|
|
919
|
+
local: includeLocal ? (await readLocalSkills(ctx)).map((skill) => ({
|
|
920
|
+
name: skill.name,
|
|
921
|
+
description: skill.description ?? null,
|
|
922
|
+
source: "local"
|
|
923
|
+
})) : [],
|
|
924
|
+
pending: rows.filter((row) => row.reviewStatus === "pending"),
|
|
925
|
+
approved: rows.filter((row) => row.reviewStatus === "approved"),
|
|
926
|
+
rejected: rows.filter((row) => row.reviewStatus === "rejected")
|
|
927
|
+
},
|
|
928
|
+
canReview: canReview(scope),
|
|
929
|
+
isAdmin: isAdmin(scope),
|
|
930
|
+
isDepartmentAdmin: isDeptAdmin(scope),
|
|
931
|
+
departmentCode: scope.departmentCode ?? null,
|
|
932
|
+
skills: rows
|
|
933
|
+
};
|
|
934
|
+
};
|
|
935
|
+
register(defineTool({
|
|
936
|
+
name: "skill_list",
|
|
937
|
+
description: "List skills in the DataOps skill centre: local workspace skills plus the caller's pending/rejected submissions and the approved plaza entries they may install. Scope follows the caller role (department admins see their department; ordinary users see their own and approved entries). Read-only.",
|
|
938
|
+
parameters: { include_local: {
|
|
939
|
+
type: "boolean",
|
|
940
|
+
description: "Include local workspace skills (default true)."
|
|
941
|
+
} },
|
|
942
|
+
output: {
|
|
943
|
+
schema: open,
|
|
944
|
+
render
|
|
945
|
+
},
|
|
946
|
+
execute: async (args) => {
|
|
947
|
+
const scope = await loadScope(reviewApi);
|
|
948
|
+
return listSkills(scope, args.include_local !== false);
|
|
949
|
+
}
|
|
950
|
+
}));
|
|
951
|
+
register(defineTool({
|
|
952
|
+
name: "skill_get",
|
|
953
|
+
description: "Read one skill-centre entry by id: description, instruction, review status, review note, risk report and visibility. Only entries visible to the caller can be read.",
|
|
954
|
+
parameters: { skill_id: {
|
|
955
|
+
type: "number",
|
|
956
|
+
required: true,
|
|
957
|
+
description: "Skill/plaza id from skill_list."
|
|
958
|
+
} },
|
|
959
|
+
output: {
|
|
960
|
+
schema: open,
|
|
961
|
+
render
|
|
962
|
+
},
|
|
963
|
+
execute: async (args) => {
|
|
964
|
+
const scope = await loadScope(reviewApi);
|
|
965
|
+
const found = (await listSkills(scope, false)).skills.find((row) => row.id === args.skill_id);
|
|
966
|
+
if (found === void 0) throw new Error("Skill not found or not visible: " + args.skill_id.toString());
|
|
967
|
+
return found;
|
|
968
|
+
}
|
|
969
|
+
}));
|
|
970
|
+
register(defineTool({
|
|
971
|
+
name: "skill_submit",
|
|
972
|
+
description: "Submit the caller's own skill for plaza review. When the workspace review switch is off DataOps approves immediately; otherwise the entry becomes pending. Requires user approval; audited.",
|
|
973
|
+
parameters: { skill_id: {
|
|
974
|
+
type: "number",
|
|
975
|
+
required: true,
|
|
976
|
+
description: "Own skill/plaza id."
|
|
977
|
+
} },
|
|
978
|
+
output: {
|
|
979
|
+
schema: open,
|
|
980
|
+
render
|
|
981
|
+
},
|
|
982
|
+
execute: async (args, exec) => {
|
|
983
|
+
const scope = await loadScope(reviewApi);
|
|
984
|
+
requireSkillPermission(scope, "skill:submit");
|
|
985
|
+
await approve("skill_submit", exec, "Submit skill " + args.skill_id.toString() + " for review");
|
|
986
|
+
const value = await call("/api/ai/skill-plaza/skills/" + args.skill_id.toString() + "/submit", {
|
|
987
|
+
method: "POST",
|
|
988
|
+
body: "{}"
|
|
989
|
+
});
|
|
990
|
+
audit("skill_submit", exec, { skillId: args.skill_id });
|
|
991
|
+
return value;
|
|
992
|
+
}
|
|
993
|
+
}));
|
|
994
|
+
register(defineTool({
|
|
995
|
+
name: "skill_install",
|
|
996
|
+
description: "Install one approved, visible plaza skill into the caller's workspace. Requires user approval; audited. The secret-free result reports the local skill key.",
|
|
997
|
+
parameters: { skill_id: {
|
|
998
|
+
type: "number",
|
|
999
|
+
required: true,
|
|
1000
|
+
description: "Plaza skill id."
|
|
1001
|
+
} },
|
|
1002
|
+
output: {
|
|
1003
|
+
schema: open,
|
|
1004
|
+
render
|
|
1005
|
+
},
|
|
1006
|
+
execute: async (args, exec) => {
|
|
1007
|
+
const scope = await loadScope(reviewApi);
|
|
1008
|
+
requireSkillPermission(scope, "skill:install");
|
|
1009
|
+
await approve("skill_install", exec, "Install skill " + args.skill_id.toString());
|
|
1010
|
+
const value = await call("/api/ai/skill-plaza/install", {
|
|
1011
|
+
method: "POST",
|
|
1012
|
+
body: JSON.stringify({ skillId: args.skill_id })
|
|
1013
|
+
});
|
|
1014
|
+
audit("skill_install", exec, { skillId: args.skill_id });
|
|
1015
|
+
return value;
|
|
1016
|
+
}
|
|
1017
|
+
}));
|
|
1018
|
+
register(defineTool({
|
|
1019
|
+
name: "skill_uninstall",
|
|
1020
|
+
description: "Uninstall one skill from the caller's own workspace only: deletes the current user's installation record. The plaza entry, its review status, visibility and enabled flag are unchanged and no “unpublished” notification is sent. Requires user approval; audited.",
|
|
1021
|
+
parameters: { skill_id: {
|
|
1022
|
+
type: "number",
|
|
1023
|
+
required: true,
|
|
1024
|
+
description: "Plaza skill id."
|
|
1025
|
+
} },
|
|
1026
|
+
output: {
|
|
1027
|
+
schema: open,
|
|
1028
|
+
render
|
|
1029
|
+
},
|
|
1030
|
+
execute: async (args, exec) => {
|
|
1031
|
+
const scope = await loadScope(reviewApi);
|
|
1032
|
+
requireSkillPermission(scope, "skill:install");
|
|
1033
|
+
await approve("skill_uninstall", exec, "Uninstall skill " + args.skill_id.toString() + " from the current workspace");
|
|
1034
|
+
const result = await call("/api/ai/skill-plaza/skills/" + args.skill_id.toString() + "/install", { method: "DELETE" });
|
|
1035
|
+
audit("skill_uninstall", exec, {
|
|
1036
|
+
skillId: args.skill_id,
|
|
1037
|
+
scope: "installation-only",
|
|
1038
|
+
removed: result.removed ?? null
|
|
1039
|
+
});
|
|
1040
|
+
return {
|
|
1041
|
+
uninstalled: result.uninstalled === true,
|
|
1042
|
+
removed: typeof result.removed === "number" ? result.removed : 0
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}));
|
|
1046
|
+
const setEnabled = (enabled) => defineTool({
|
|
1047
|
+
name: enabled ? "skill_enable" : "skill_disable",
|
|
1048
|
+
description: (enabled ? "Enable" : "Disable") + " one plaza skill for distribution. Administrator only. Requires user approval; audited.",
|
|
1049
|
+
parameters: { skill_id: {
|
|
1050
|
+
type: "number",
|
|
1051
|
+
required: true,
|
|
1052
|
+
description: "Plaza skill id."
|
|
1053
|
+
} },
|
|
1054
|
+
output: {
|
|
1055
|
+
schema: open,
|
|
1056
|
+
render
|
|
1057
|
+
},
|
|
1058
|
+
execute: async (args, exec) => {
|
|
1059
|
+
const scope = await loadScope(reviewApi);
|
|
1060
|
+
requireSkillPermission(scope, "skill:manage");
|
|
1061
|
+
await approve(enabled ? "skill_enable" : "skill_disable", exec, (enabled ? "Enable" : "Disable") + " skill " + args.skill_id.toString());
|
|
1062
|
+
await call("/api/ai/skill-plaza/admin/skills/" + args.skill_id.toString() + "/status", {
|
|
1063
|
+
method: "PATCH",
|
|
1064
|
+
body: JSON.stringify({ enabled })
|
|
1065
|
+
});
|
|
1066
|
+
audit(enabled ? "skill_enable" : "skill_disable", exec, {
|
|
1067
|
+
skillId: args.skill_id,
|
|
1068
|
+
enabled
|
|
1069
|
+
});
|
|
1070
|
+
return {
|
|
1071
|
+
skillId: args.skill_id,
|
|
1072
|
+
enabled
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
register(setEnabled(true));
|
|
1077
|
+
register(setEnabled(false));
|
|
1078
|
+
register(defineTool({
|
|
1079
|
+
name: "skill_unshare",
|
|
1080
|
+
description: "Stop sharing one plaza skill (soft unpublish): the entry and its installations remain, review status/visibility are kept, and DataOps sends an “unpublished” notification. Owners may unshare their own entries; 超级管理员/管理员 any entry; 部门管理员 their department. Requires user approval; audited.",
|
|
1081
|
+
parameters: { skill_id: {
|
|
1082
|
+
type: "number",
|
|
1083
|
+
required: true,
|
|
1084
|
+
description: "Plaza skill id."
|
|
1085
|
+
} },
|
|
1086
|
+
output: {
|
|
1087
|
+
schema: open,
|
|
1088
|
+
render
|
|
1089
|
+
},
|
|
1090
|
+
execute: async (args, exec) => {
|
|
1091
|
+
const scope = await loadScope(reviewApi);
|
|
1092
|
+
requireSkillPermission(scope, "skill:manage");
|
|
1093
|
+
if (!isAdmin(scope) && !isDeptAdmin(scope)) {
|
|
1094
|
+
const found = (await listSkills(scope, false)).skills.find((row) => row.id === args.skill_id);
|
|
1095
|
+
if (found === void 0 || found.sharedByMe !== true) throw forbidden("only the owner or an administrator may unshare this skill");
|
|
1096
|
+
}
|
|
1097
|
+
await approve("skill_unshare", exec, "Unshare skill " + args.skill_id.toString());
|
|
1098
|
+
await call("/api/ai/skill-plaza/skills/" + args.skill_id.toString(), { method: "DELETE" });
|
|
1099
|
+
audit("skill_unshare", exec, {
|
|
1100
|
+
skillId: args.skill_id,
|
|
1101
|
+
effect: "soft-unpublish"
|
|
1102
|
+
});
|
|
1103
|
+
return {
|
|
1104
|
+
unshared: args.skill_id,
|
|
1105
|
+
effect: "plaza-sharing-disabled"
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}));
|
|
1109
|
+
register(defineTool({
|
|
1110
|
+
name: "skill_delete",
|
|
1111
|
+
description: "Permanently delete one plaza skill. This is not unshare: the record is removed, DataOps sends a “deleted” notification (not “unpublished”), and the audit entry records the permanent action. Requires confirm=true as a second confirmation; owners may delete their own entries, 超级管理员/管理员 any entry, 部门管理员 their department. If installations still exist the backend returns 409 and the error is passed through. Requires user approval; audited.",
|
|
1112
|
+
parameters: {
|
|
1113
|
+
skill_id: {
|
|
1114
|
+
type: "number",
|
|
1115
|
+
required: true,
|
|
1116
|
+
description: "Plaza skill id."
|
|
1117
|
+
},
|
|
1118
|
+
confirm: {
|
|
1119
|
+
type: "boolean",
|
|
1120
|
+
required: true,
|
|
1121
|
+
description: "Must be true: explicitly confirms permanent deletion."
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
output: {
|
|
1125
|
+
schema: open,
|
|
1126
|
+
render
|
|
1127
|
+
},
|
|
1128
|
+
execute: async (args, exec) => {
|
|
1129
|
+
if (args.confirm !== true) throw new Error("skill_delete requires confirm=true (permanent deletion)");
|
|
1130
|
+
const scope = await loadScope(reviewApi);
|
|
1131
|
+
requireSkillPermission(scope, "skill:manage");
|
|
1132
|
+
if (!isAdmin(scope) && !isDeptAdmin(scope)) {
|
|
1133
|
+
const found = (await listSkills(scope, false)).skills.find((row) => row.id === args.skill_id);
|
|
1134
|
+
if (found === void 0 || found.sharedByMe !== true) throw forbidden("only the owner or an administrator may delete this skill");
|
|
1135
|
+
}
|
|
1136
|
+
await approve("skill_delete", exec, "Permanently delete skill " + args.skill_id.toString());
|
|
1137
|
+
await call("/api/ai/skill-plaza/skills/" + args.skill_id.toString() + "/permanent", { method: "DELETE" });
|
|
1138
|
+
audit("skill_delete", exec, {
|
|
1139
|
+
skillId: args.skill_id,
|
|
1140
|
+
permanent: true
|
|
1141
|
+
});
|
|
1142
|
+
return {
|
|
1143
|
+
deleted: args.skill_id,
|
|
1144
|
+
permanent: true
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}));
|
|
1148
|
+
register(defineTool({
|
|
1149
|
+
name: "review_scan",
|
|
1150
|
+
description: "Run the DataOps AI risk scan for skill and/or workflow ids. Reviewers only (超级管理员/管理员/部门管理员 with review capability); ordinary users receive 403. Read-only.",
|
|
1151
|
+
parameters: {
|
|
1152
|
+
skills: {
|
|
1153
|
+
type: "array",
|
|
1154
|
+
items: { type: "number" },
|
|
1155
|
+
description: "Skill ids to scan."
|
|
1156
|
+
},
|
|
1157
|
+
workflows: {
|
|
1158
|
+
type: "array",
|
|
1159
|
+
items: { type: "string" },
|
|
1160
|
+
description: "Workflow ids to scan."
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
output: {
|
|
1164
|
+
schema: open,
|
|
1165
|
+
render
|
|
1166
|
+
},
|
|
1167
|
+
execute: async (args) => {
|
|
1168
|
+
const scope = await loadScope(reviewApi);
|
|
1169
|
+
requireSkillPermission(scope, "skill:read");
|
|
1170
|
+
if (!canReview(scope)) throw forbidden("the current role cannot review");
|
|
1171
|
+
return await call("/api/ai/review/scan", {
|
|
1172
|
+
method: "POST",
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
skills: args.skills ?? [],
|
|
1175
|
+
workflows: args.workflows ?? []
|
|
1176
|
+
})
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}));
|
|
1180
|
+
register(defineTool({
|
|
1181
|
+
name: "review_decide",
|
|
1182
|
+
description: "Approve or reject skills/workflows. Requires authorization.confirmed=true (the administrator explicitly confirmed), reviewer role and department scope; requires a second user approval at tool level and writes an audit entry. Ordinary users receive 403.",
|
|
1183
|
+
parameters: {
|
|
1184
|
+
skills: {
|
|
1185
|
+
type: "array",
|
|
1186
|
+
items: { type: "number" },
|
|
1187
|
+
description: "Skill ids."
|
|
1188
|
+
},
|
|
1189
|
+
workflows: {
|
|
1190
|
+
type: "array",
|
|
1191
|
+
items: { type: "string" },
|
|
1192
|
+
description: "Workflow ids."
|
|
1193
|
+
},
|
|
1194
|
+
decision: {
|
|
1195
|
+
type: "string",
|
|
1196
|
+
required: true,
|
|
1197
|
+
description: "approve or reject."
|
|
1198
|
+
},
|
|
1199
|
+
note: {
|
|
1200
|
+
type: "string",
|
|
1201
|
+
description: "Review note (max 2000 chars)."
|
|
1202
|
+
},
|
|
1203
|
+
confirmed: {
|
|
1204
|
+
type: "boolean",
|
|
1205
|
+
required: true,
|
|
1206
|
+
description: "Must be true: the administrator confirmed this decision."
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
output: {
|
|
1210
|
+
schema: open,
|
|
1211
|
+
render
|
|
1212
|
+
},
|
|
1213
|
+
execute: async (args, exec) => {
|
|
1214
|
+
const scope = await loadScope(reviewApi);
|
|
1215
|
+
requireSkillPermission(scope, "skill:review:decide");
|
|
1216
|
+
if (!canReview(scope)) throw forbidden("the current role cannot review");
|
|
1217
|
+
if (args.confirmed !== true) throw new Error("authorization.confirmed=true is required for review_decide");
|
|
1218
|
+
if (args.decision !== "approve" && args.decision !== "reject") throw new Error("decision must be approve or reject");
|
|
1219
|
+
if ((args.skills ?? []).length === 0 && (args.workflows ?? []).length === 0) throw new Error("at least one skill or workflow id is required");
|
|
1220
|
+
if (!isAdmin(scope) && isDeptAdmin(scope)) {
|
|
1221
|
+
const rows = (await listSkills(scope, false)).skills;
|
|
1222
|
+
const allowed = new Set(scope.dataScope?.departmentCodes ?? (scope.departmentCode === void 0 ? [] : [scope.departmentCode]));
|
|
1223
|
+
for (const id of args.skills ?? []) {
|
|
1224
|
+
const row = rows.find((item) => item.id === id);
|
|
1225
|
+
if (row !== void 0 && (row.departmentCode === null || row.departmentCode === void 0)) requireSkillPermission(scope, "skill:review:global");
|
|
1226
|
+
if (row !== void 0 && row.departmentCode !== null && row.departmentCode !== void 0 && !allowed.has(String(row.departmentCode))) throw forbidden("department administrators may only review their own department");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
await approve("review_decide", exec, "Review " + args.decision + " for " + (args.skills ?? []).length.toString() + " skill(s) / " + (args.workflows ?? []).length.toString() + " workflow(s)");
|
|
1230
|
+
const value = await call("/api/ai/review/decide", {
|
|
1231
|
+
method: "POST",
|
|
1232
|
+
body: JSON.stringify({
|
|
1233
|
+
skills: args.skills ?? [],
|
|
1234
|
+
workflows: args.workflows ?? [],
|
|
1235
|
+
decision: args.decision,
|
|
1236
|
+
note: args.note ?? "",
|
|
1237
|
+
authorization: { confirmed: true }
|
|
1238
|
+
})
|
|
1239
|
+
});
|
|
1240
|
+
audit("review_decide", exec, {
|
|
1241
|
+
decision: args.decision,
|
|
1242
|
+
skills: args.skills ?? [],
|
|
1243
|
+
workflows: args.workflows ?? [],
|
|
1244
|
+
confirmed: true
|
|
1245
|
+
});
|
|
1246
|
+
return value;
|
|
1247
|
+
}
|
|
1248
|
+
}));
|
|
1249
|
+
register(defineTool({
|
|
1250
|
+
name: "review_notifications_list",
|
|
1251
|
+
description: "List the caller's review notifications and unread count. Read-only and always scoped to the current DataOps user.",
|
|
1252
|
+
parameters: {
|
|
1253
|
+
unread_only: {
|
|
1254
|
+
type: "boolean",
|
|
1255
|
+
description: "Only unread notifications."
|
|
1256
|
+
},
|
|
1257
|
+
limit: {
|
|
1258
|
+
type: "number",
|
|
1259
|
+
description: "Maximum rows (default 20)."
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
output: {
|
|
1263
|
+
schema: open,
|
|
1264
|
+
render
|
|
1265
|
+
},
|
|
1266
|
+
execute: async (args) => {
|
|
1267
|
+
await loadScope(reviewApi);
|
|
1268
|
+
const query = new URLSearchParams({
|
|
1269
|
+
unreadOnly: args.unread_only === true ? "true" : "false",
|
|
1270
|
+
limit: String(args.limit ?? 20)
|
|
1271
|
+
});
|
|
1272
|
+
return await call("/api/ai/review-notifications?" + query.toString());
|
|
1273
|
+
}
|
|
1274
|
+
}));
|
|
1275
|
+
register(defineTool({
|
|
1276
|
+
name: "review_notifications_read",
|
|
1277
|
+
description: "Mark the caller's review notifications as read, either selected ids or all. Scoped to the current DataOps user; no secret is involved.",
|
|
1278
|
+
parameters: {
|
|
1279
|
+
ids: {
|
|
1280
|
+
type: "array",
|
|
1281
|
+
items: { type: "string" },
|
|
1282
|
+
description: "Notification ids to mark read."
|
|
1283
|
+
},
|
|
1284
|
+
all: {
|
|
1285
|
+
type: "boolean",
|
|
1286
|
+
description: "Mark every notification read."
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
output: {
|
|
1290
|
+
schema: open,
|
|
1291
|
+
render
|
|
1292
|
+
},
|
|
1293
|
+
execute: async (args, exec) => {
|
|
1294
|
+
await loadScope(reviewApi);
|
|
1295
|
+
const value = await call("/api/ai/review-notifications/read", {
|
|
1296
|
+
method: "POST",
|
|
1297
|
+
body: JSON.stringify({
|
|
1298
|
+
ids: args.ids ?? [],
|
|
1299
|
+
all: args.all === true
|
|
1300
|
+
})
|
|
1301
|
+
});
|
|
1302
|
+
audit("review_notifications_read", exec, {
|
|
1303
|
+
count: args.ids?.length ?? 0,
|
|
1304
|
+
all: args.all === true
|
|
1305
|
+
});
|
|
1306
|
+
return value;
|
|
1307
|
+
}
|
|
1308
|
+
}));
|
|
1309
|
+
return () => {
|
|
1310
|
+
for (const dispose of disposers) dispose();
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
//#endregion
|
|
688
1314
|
//#region src/workspace-limits.ts
|
|
689
1315
|
/** Narrow a value to a plain object. */
|
|
690
1316
|
function asRecord(value) {
|
|
@@ -749,7 +1375,11 @@ var WorkspaceLimits = class {
|
|
|
749
1375
|
const token = await this.token();
|
|
750
1376
|
if (token === void 0) return void 0;
|
|
751
1377
|
const response = await fetch(new URL("/api/ai/attachment-settings", this.apiBase()), { headers: { authorization: `Bearer ${token}` } });
|
|
752
|
-
if (response.status === 401)
|
|
1378
|
+
if (response.status === 401) {
|
|
1379
|
+
noteExpired();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
noteAccepted();
|
|
753
1383
|
if (!response.ok) throw new WorkspaceLimitsRejection(response.status, await rejectionMessage(response, "Workspace limits read failed"));
|
|
754
1384
|
const body = asRecord(await response.json());
|
|
755
1385
|
const settings = body === null ? null : asRecord(body.settings);
|
|
@@ -782,7 +1412,11 @@ var WorkspaceLimits = class {
|
|
|
782
1412
|
},
|
|
783
1413
|
body: JSON.stringify({ workspaceSize: limits })
|
|
784
1414
|
});
|
|
785
|
-
if (response.status === 401)
|
|
1415
|
+
if (response.status === 401) {
|
|
1416
|
+
noteExpired();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
noteAccepted();
|
|
786
1420
|
if (!response.ok) throw new WorkspaceLimitsRejection(response.status, await rejectionMessage(response, "Workspace limits write failed"));
|
|
787
1421
|
return this.status();
|
|
788
1422
|
}
|
|
@@ -797,6 +1431,302 @@ function setWorkspaceLimits(next) {
|
|
|
797
1431
|
function currentWorkspaceLimits() {
|
|
798
1432
|
return limitsRef;
|
|
799
1433
|
}
|
|
1434
|
+
/** Directory used by this plugin below the DSH home. */
|
|
1435
|
+
const WORKSPACE_UPGRADE_STATE_DIR = "dataops-managed";
|
|
1436
|
+
/** Name of the hand-off file. */
|
|
1437
|
+
const WORKSPACE_UPGRADE_STATE_FILE = "workspace-upgrade.json";
|
|
1438
|
+
/**
|
|
1439
|
+
* Resolve the persistent DSH home used by this workspace.
|
|
1440
|
+
*
|
|
1441
|
+
* The companion currently starts with `DSH_HOME=/workspace/.dataops/dsh`; the fallback
|
|
1442
|
+
* keeps tests and a manually launched host deterministic without reaching outside the
|
|
1443
|
+
* mounted workspace.
|
|
1444
|
+
* @param dshHome - override for tests.
|
|
1445
|
+
* @returns the state directory path.
|
|
1446
|
+
*/
|
|
1447
|
+
function workspaceUpgradeStatePath(dshHome = process.env.DSH_HOME ?? "/workspace/.dataops/dsh") {
|
|
1448
|
+
return join(dshHome, WORKSPACE_UPGRADE_STATE_DIR, WORKSPACE_UPGRADE_STATE_FILE);
|
|
1449
|
+
}
|
|
1450
|
+
/** Validate one persisted session entry. */
|
|
1451
|
+
function asSession(value) {
|
|
1452
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1453
|
+
const record = value;
|
|
1454
|
+
if (typeof record.sessionId !== "string" || record.sessionId.trim() === "") return null;
|
|
1455
|
+
return {
|
|
1456
|
+
sessionId: record.sessionId.trim(),
|
|
1457
|
+
...typeof record.title === "string" && record.title.trim() !== "" ? { title: record.title.trim() } : {},
|
|
1458
|
+
...typeof record.cwd === "string" && record.cwd.trim() !== "" ? { cwd: record.cwd.trim() } : {}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
/** Validate a parsed state document. */
|
|
1462
|
+
function asWorkspaceUpgradeState(value) {
|
|
1463
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1464
|
+
const record = value;
|
|
1465
|
+
if (record.version !== 1) return null;
|
|
1466
|
+
if (typeof record.upgradeId !== "string" || record.upgradeId.trim() === "") return null;
|
|
1467
|
+
if (typeof record.targetImage !== "string" || record.targetImage.trim() === "") return null;
|
|
1468
|
+
if (!Array.isArray(record.sessions)) return null;
|
|
1469
|
+
const sessions = record.sessions.map(asSession).filter((session) => session !== null);
|
|
1470
|
+
return {
|
|
1471
|
+
version: 1,
|
|
1472
|
+
upgradeId: record.upgradeId.trim(),
|
|
1473
|
+
targetImage: record.targetImage.trim(),
|
|
1474
|
+
sessions,
|
|
1475
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Read the pending hand-off record.
|
|
1480
|
+
* @param file - state file path.
|
|
1481
|
+
* @returns the record, or null when none exists or it cannot be parsed.
|
|
1482
|
+
*/
|
|
1483
|
+
async function readWorkspaceUpgradeState(file = workspaceUpgradeStatePath()) {
|
|
1484
|
+
try {
|
|
1485
|
+
return asWorkspaceUpgradeState(JSON.parse(await readFile(file, "utf8")));
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Persist the hand-off record atomically.
|
|
1492
|
+
*
|
|
1493
|
+
* A partially written JSON file would be indistinguishable from "no upgrade pending",
|
|
1494
|
+
* which would silently skip recovery. The temporary file is renamed into place only
|
|
1495
|
+
* after the content is complete.
|
|
1496
|
+
* @param state - record to persist.
|
|
1497
|
+
* @param file - state file path.
|
|
1498
|
+
*/
|
|
1499
|
+
async function writeWorkspaceUpgradeState(state, file = workspaceUpgradeStatePath()) {
|
|
1500
|
+
await mkdir(dirname(file), { recursive: true });
|
|
1501
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
1502
|
+
await writeFile(temporary, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
1503
|
+
await rename(temporary, file);
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Remove the hand-off record after recovery has been attempted.
|
|
1507
|
+
* @param file - state file path.
|
|
1508
|
+
*/
|
|
1509
|
+
async function clearWorkspaceUpgradeState(file = workspaceUpgradeStatePath()) {
|
|
1510
|
+
await rm(file, { force: true });
|
|
1511
|
+
}
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/workspace-upgrade.ts
|
|
1514
|
+
/**
|
|
1515
|
+
* Workspace image upgrade orchestration.
|
|
1516
|
+
*
|
|
1517
|
+
* DataOps owns the Docker image comparison and the container restart. This plugin owns
|
|
1518
|
+
* the two facts DSH alone can know: which sessions are running right now, and how to
|
|
1519
|
+
* bring those sessions back after the container has been recreated. A small JSON file
|
|
1520
|
+
* on the shared workspace volume carries that hand-off across the restart.
|
|
1521
|
+
*
|
|
1522
|
+
* @module workspace-upgrade
|
|
1523
|
+
*/
|
|
1524
|
+
/** A refusal or transport failure from the DataOps API. */
|
|
1525
|
+
var WorkspaceUpgradeError = class extends Error {
|
|
1526
|
+
status;
|
|
1527
|
+
constructor(status, message) {
|
|
1528
|
+
super(message);
|
|
1529
|
+
this.status = status;
|
|
1530
|
+
this.name = "WorkspaceUpgradeError";
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
/** Create the user-message value `agent.followup` accepts. */
|
|
1534
|
+
function userMessage(text) {
|
|
1535
|
+
return {
|
|
1536
|
+
id: randomUUID(),
|
|
1537
|
+
role: "user",
|
|
1538
|
+
content: [{
|
|
1539
|
+
type: "text",
|
|
1540
|
+
text
|
|
1541
|
+
}],
|
|
1542
|
+
source: { kind: "user" }
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/** Extract a useful message from a DataOps error body. */
|
|
1546
|
+
async function responseMessage(response, fallback) {
|
|
1547
|
+
try {
|
|
1548
|
+
const message = (await response.json()).message;
|
|
1549
|
+
if (typeof message === "string" && message.trim() !== "") return message;
|
|
1550
|
+
if (Array.isArray(message) && typeof message[0] === "string") return message[0];
|
|
1551
|
+
} catch {}
|
|
1552
|
+
return `${fallback} (HTTP ${String(response.status)})`;
|
|
1553
|
+
}
|
|
1554
|
+
/** Owns DataOps preview/restart calls and the DSH session hand-off. */
|
|
1555
|
+
var WorkspaceUpgrade = class {
|
|
1556
|
+
ctx;
|
|
1557
|
+
config;
|
|
1558
|
+
statePath;
|
|
1559
|
+
constructor(ctx, config, statePath = workspaceUpgradeStatePath()) {
|
|
1560
|
+
this.ctx = ctx;
|
|
1561
|
+
this.config = config;
|
|
1562
|
+
this.statePath = statePath;
|
|
1563
|
+
}
|
|
1564
|
+
/** Resolve the injected DSH agents service, if the profile provides one. */
|
|
1565
|
+
agents() {
|
|
1566
|
+
return this.ctx.agents;
|
|
1567
|
+
}
|
|
1568
|
+
/** Read the current DataOps credential. */
|
|
1569
|
+
async token() {
|
|
1570
|
+
return (await this.ctx.credentials.resolve(credentialRef(this.config.credentialRef)))?.value;
|
|
1571
|
+
}
|
|
1572
|
+
/** DataOps origin used by this workspace. */
|
|
1573
|
+
apiBase() {
|
|
1574
|
+
return new URL(this.config.baseUrl).origin;
|
|
1575
|
+
}
|
|
1576
|
+
/** Call one DataOps API path with the workspace JWT. */
|
|
1577
|
+
async request(path, init) {
|
|
1578
|
+
const token = await this.token();
|
|
1579
|
+
if (token === void 0) throw new WorkspaceUpgradeError(401, "workspace is not signed in to DataOps");
|
|
1580
|
+
let response;
|
|
1581
|
+
try {
|
|
1582
|
+
response = await fetch(new URL(path, this.apiBase()), {
|
|
1583
|
+
...init,
|
|
1584
|
+
headers: {
|
|
1585
|
+
authorization: `Bearer ${token}`,
|
|
1586
|
+
...init?.body === void 0 ? {} : { "content-type": "application/json" },
|
|
1587
|
+
...init?.headers ?? {}
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
throw new WorkspaceUpgradeError(502, error instanceof Error ? error.message : String(error));
|
|
1592
|
+
}
|
|
1593
|
+
if (response.status === 401) {
|
|
1594
|
+
noteExpired();
|
|
1595
|
+
throw new WorkspaceUpgradeError(401, "DataOps session has ended");
|
|
1596
|
+
}
|
|
1597
|
+
noteAccepted();
|
|
1598
|
+
return response;
|
|
1599
|
+
}
|
|
1600
|
+
/** Read the DataOps restart preview plus this host's running-session snapshot. */
|
|
1601
|
+
async preview() {
|
|
1602
|
+
const response = await this.request("/api/ai/workspace/restart-preview");
|
|
1603
|
+
if (!response.ok) throw new WorkspaceUpgradeError(response.status, await responseMessage(response, "workspace preview failed"));
|
|
1604
|
+
const body = await response.json();
|
|
1605
|
+
const imageStatus = body.workspace?.imageStatus;
|
|
1606
|
+
if (imageStatus !== "CURRENT" && imageStatus !== "UPDATE_AVAILABLE" && imageStatus !== "NOT_RUNNING" && imageStatus !== "UNKNOWN") throw new WorkspaceUpgradeError(502, "workspace preview returned an unrecognized image status");
|
|
1607
|
+
return {
|
|
1608
|
+
imageStatus,
|
|
1609
|
+
configuredImage: typeof body.workspace?.configuredImage === "string" ? body.workspace.configuredImage : "",
|
|
1610
|
+
activeSessions: this.runningSessions()
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
/** Snapshot sessions that are executing a turn right now. */
|
|
1614
|
+
runningSessions() {
|
|
1615
|
+
const agents = this.agents();
|
|
1616
|
+
if (agents === void 0) return [];
|
|
1617
|
+
return agents.list().filter((agent) => agent.status === "running").map((agent) => ({
|
|
1618
|
+
sessionId: agent.session.id,
|
|
1619
|
+
...agent.session.header?.title ? { title: agent.session.header.title } : {},
|
|
1620
|
+
...agent.session.header?.cwd ? { cwd: agent.session.header.cwd } : {}
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
/** Restart DataOps' workspace container; force is required when sessions are snapshot. */
|
|
1624
|
+
async restart() {
|
|
1625
|
+
const response = await this.request("/api/ai/workspace/restart", {
|
|
1626
|
+
method: "POST",
|
|
1627
|
+
body: JSON.stringify({ force: true })
|
|
1628
|
+
});
|
|
1629
|
+
if (!response.ok) throw new WorkspaceUpgradeError(response.status, await responseMessage(response, "workspace restart failed"));
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Persist the running-session snapshot and ask DataOps to recreate the container.
|
|
1633
|
+
* @returns the accepted hand-off record.
|
|
1634
|
+
*/
|
|
1635
|
+
async upgrade() {
|
|
1636
|
+
const preview = await this.preview();
|
|
1637
|
+
if (preview.imageStatus !== "UPDATE_AVAILABLE") throw new WorkspaceUpgradeError(409, "workspace image is already current or not running");
|
|
1638
|
+
const state = {
|
|
1639
|
+
version: 1,
|
|
1640
|
+
upgradeId: randomUUID(),
|
|
1641
|
+
targetImage: preview.configuredImage,
|
|
1642
|
+
sessions: preview.activeSessions,
|
|
1643
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1644
|
+
};
|
|
1645
|
+
await writeWorkspaceUpgradeState(state, this.statePath);
|
|
1646
|
+
try {
|
|
1647
|
+
await this.restart();
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
await clearWorkspaceUpgradeState(this.statePath);
|
|
1650
|
+
throw error;
|
|
1651
|
+
}
|
|
1652
|
+
return state;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Resume every snapshotted session and send one "继续".
|
|
1656
|
+
*
|
|
1657
|
+
* The method deliberately does not wait for an answer: the user's workspace should
|
|
1658
|
+
* become usable immediately, and an in-flight tool call cannot be replayed safely.
|
|
1659
|
+
* `recoverWorkspaceUpgradeAfterBoot` removes the hand-off file after this attempt.
|
|
1660
|
+
*/
|
|
1661
|
+
async recover() {
|
|
1662
|
+
const state = await readWorkspaceUpgradeState(this.statePath);
|
|
1663
|
+
if (state === null) return {
|
|
1664
|
+
recovered: 0,
|
|
1665
|
+
skipped: 0
|
|
1666
|
+
};
|
|
1667
|
+
if (this.agents() === void 0) throw new Error("DSH agents service is not ready");
|
|
1668
|
+
let recovered = 0;
|
|
1669
|
+
let skipped = 0;
|
|
1670
|
+
for (const session of state.sessions) try {
|
|
1671
|
+
const agent = await this.ensureAgent(session.sessionId);
|
|
1672
|
+
if (agent === void 0) {
|
|
1673
|
+
skipped += 1;
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
agent.followup(userMessage("继续"));
|
|
1677
|
+
recovered += 1;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
skipped += 1;
|
|
1680
|
+
this.ctx.logger.warn(`dataops-managed: could not resume session ${session.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
recovered,
|
|
1684
|
+
skipped
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
/** Remove the hand-off record once recovery has been attempted. */
|
|
1688
|
+
async clearRecoveryState() {
|
|
1689
|
+
await clearWorkspaceUpgradeState(this.statePath);
|
|
1690
|
+
}
|
|
1691
|
+
/** Find a live session or resume it from persisted state. */
|
|
1692
|
+
async ensureAgent(sessionId) {
|
|
1693
|
+
const agents = this.agents();
|
|
1694
|
+
if (agents === void 0) return void 0;
|
|
1695
|
+
const live = agents.get(sessionId);
|
|
1696
|
+
if (live !== void 0) return live;
|
|
1697
|
+
return (await agents.resume({ resumeSessionId: sessionId }))?.agent;
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
/** Delay helper for boot-time recovery without keeping the plugin apply() pending. */
|
|
1701
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
1702
|
+
setTimeout(resolve, ms);
|
|
1703
|
+
});
|
|
1704
|
+
/**
|
|
1705
|
+
* Run recovery after the DSH host and its session persistence have had time to boot.
|
|
1706
|
+
*
|
|
1707
|
+
* `apply()` runs before a persisted session necessarily has its live agent; resuming
|
|
1708
|
+
* eagerly can race that boot. A short bounded retry is enough, and a failed attempt
|
|
1709
|
+
* still removes the hand-off file so the next page load cannot send "继续" forever.
|
|
1710
|
+
* @param upgrade - upgrade service.
|
|
1711
|
+
*/
|
|
1712
|
+
async function recoverWorkspaceUpgradeAfterBoot(upgrade) {
|
|
1713
|
+
let lastState = null;
|
|
1714
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
1715
|
+
const state = await readWorkspaceUpgradeState(upgrade.statePath);
|
|
1716
|
+
if (state === null) return;
|
|
1717
|
+
lastState = state;
|
|
1718
|
+
if (attempt > 0) await sleep(1500);
|
|
1719
|
+
try {
|
|
1720
|
+
if ((await upgrade.recover()).recovered > 0 || state.sessions.length === 0) {
|
|
1721
|
+
await upgrade.clearRecoveryState();
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
console.warn("[dataops-managed] workspace upgrade recovery failed:", error);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (lastState !== null) await upgrade.clearRecoveryState();
|
|
1729
|
+
}
|
|
800
1730
|
//#endregion
|
|
801
1731
|
//#region src/index.ts
|
|
802
1732
|
/** Cordis plugin name for the DataOps-managed integration. */
|
|
@@ -807,7 +1737,9 @@ const inject = [
|
|
|
807
1737
|
"webServer",
|
|
808
1738
|
"tools",
|
|
809
1739
|
"settings",
|
|
810
|
-
"skills"
|
|
1740
|
+
"skills",
|
|
1741
|
+
"approval",
|
|
1742
|
+
"agents"
|
|
811
1743
|
];
|
|
812
1744
|
const MANAGED_AUTH_PATH = "/integrations/dataops/managed-auth";
|
|
813
1745
|
/** This plugin's profile entry id, which owns its Config and sync state. */
|
|
@@ -816,10 +1748,26 @@ const MANAGED_PLUGIN_ID = "dataops-managed";
|
|
|
816
1748
|
const MODEL_SYNC_PATH = "/integrations/dataops/model-sync";
|
|
817
1749
|
/** Same-origin route the skill center uses to read and change plaza sharing. */
|
|
818
1750
|
const SKILL_PLAZA_PATH = "/integrations/dataops/skill-plaza";
|
|
1751
|
+
/** Caller role and review capability. */
|
|
1752
|
+
const REVIEW_CONTEXT_PATH = "/integrations/dataops/review-context";
|
|
1753
|
+
/** Notification inbox and read receipts. */
|
|
1754
|
+
const REVIEW_NOTIFICATIONS_PATH = "/integrations/dataops/review-notifications";
|
|
1755
|
+
/** AI scan and the administrator's decision. */
|
|
1756
|
+
const REVIEW_ACTION_PATH = "/integrations/dataops/review";
|
|
1757
|
+
/** Permission-centre reads and writes. */
|
|
1758
|
+
const RBAC_PATH = "/integrations/dataops/rbac";
|
|
1759
|
+
/** The three plaza-review switches. */
|
|
1760
|
+
const REVIEW_SETTINGS_PATH = "/integrations/dataops/review-settings";
|
|
1761
|
+
/** Department CRUD bridge (task-67). */
|
|
1762
|
+
const DEPARTMENTS_PATH = "/integrations/dataops/departments";
|
|
819
1763
|
/** Same-origin route the Settings panel uses to read and change this workspace's limits. */
|
|
820
1764
|
const WORKSPACE_LIMITS_PATH = "/integrations/dataops/workspace-limits";
|
|
821
1765
|
/** Same-origin route the Settings panel uses to read and change the tool-call timeout. */
|
|
822
1766
|
const TOOL_TIMEOUT_PATH = "/integrations/dataops/tool-timeout";
|
|
1767
|
+
/** Same-origin route reporting what the last DataOps answer said about the credential. */
|
|
1768
|
+
const AUTH_STATE_PATH = "/integrations/dataops/auth-state";
|
|
1769
|
+
/** Same-origin route the DSH page uses to detect and start a workspace image upgrade. */
|
|
1770
|
+
const WORKSPACE_UPGRADE_PATH = "/integrations/dataops/workspace-upgrade";
|
|
823
1771
|
/** The ends a tool-call timeout may take, in whole seconds. */
|
|
824
1772
|
const TOOL_TIMEOUT_MIN_SECONDS = 1;
|
|
825
1773
|
const TOOL_TIMEOUT_MAX_SECONDS = 3600;
|
|
@@ -827,6 +1775,8 @@ const TOOL_TIMEOUT_MAX_SECONDS = 3600;
|
|
|
827
1775
|
let modelSyncRef;
|
|
828
1776
|
/** The live plaza instance, so the skill center routes can act on it after activation. */
|
|
829
1777
|
let skillPlazaRef;
|
|
1778
|
+
/** The live review API, once this plugin activated. */
|
|
1779
|
+
let reviewApiRef;
|
|
830
1780
|
/** The live skill plaza, once this plugin activated. */
|
|
831
1781
|
function currentSkillPlaza() {
|
|
832
1782
|
return skillPlazaRef;
|
|
@@ -903,6 +1853,34 @@ function bearerToken(request) {
|
|
|
903
1853
|
const token = authorization.slice(7).trim();
|
|
904
1854
|
return token === "" ? null : token;
|
|
905
1855
|
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Adopt the JWT the browser gateway attached to one request.
|
|
1858
|
+
*
|
|
1859
|
+
* Every browser request reaches this workspace through the DataOps gateway, which resolves the
|
|
1860
|
+
* caller's session and forwards a fresh `authorization: Bearer <jwt>` — so the current credential
|
|
1861
|
+
* arrives continuously, without any push, handshake or refresh protocol. The routes below are
|
|
1862
|
+
* the only place this plugin can see that header, and reading it here is what keeps the stored
|
|
1863
|
+
* credential from going stale: the value in it was captured once, while the caller's session is
|
|
1864
|
+
* renewed by DataOps for as long as they stay signed in.
|
|
1865
|
+
*
|
|
1866
|
+
* A request without the header is normal rather than an error (same-origin fetches from inside
|
|
1867
|
+
* the workspace do carry it, but a probe or a health check may not), so this only ever improves
|
|
1868
|
+
* the stored value and never rejects the request.
|
|
1869
|
+
*
|
|
1870
|
+
* @param request - the incoming request, whose headers the gateway filled in.
|
|
1871
|
+
* @returns once the credential has been updated, or immediately when there is nothing to adopt.
|
|
1872
|
+
*/
|
|
1873
|
+
function adoptRequestJwt(ctx, accessRef, request) {
|
|
1874
|
+
const token = bearerToken(request);
|
|
1875
|
+
if (token === null) return Promise.resolve();
|
|
1876
|
+
return (async () => {
|
|
1877
|
+
if ((await ctx.credentials.resolve(accessRef))?.value === token) return;
|
|
1878
|
+
await ctx.credentials.set(accessRef, token);
|
|
1879
|
+
})().catch((error) => {
|
|
1880
|
+
ctx.logger.warn("dataops-managed: request JWT could not be adopted");
|
|
1881
|
+
ctx.logger.warn(error);
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
906
1884
|
/** The skill this plugin contributes: how to answer a DataOps data question. */
|
|
907
1885
|
const QUERY_SKILL_NAME = "dataops-query";
|
|
908
1886
|
/** Location of the skill document inside the published package. */
|
|
@@ -1061,10 +2039,20 @@ async function apply(ctx, config) {
|
|
|
1061
2039
|
baseUrl,
|
|
1062
2040
|
credentialRef: config.credentialRef
|
|
1063
2041
|
});
|
|
2042
|
+
const reviewApi = new ReviewApi(ctx, {
|
|
2043
|
+
baseUrl,
|
|
2044
|
+
credentialRef: config.credentialRef
|
|
2045
|
+
});
|
|
2046
|
+
reviewApiRef = reviewApi;
|
|
2047
|
+
ctx.effect(() => registerAiManagementTools(ctx, reviewApi), "dataops-managed: skill and review tools");
|
|
1064
2048
|
setWorkspaceLimits(new WorkspaceLimits(ctx, {
|
|
1065
2049
|
baseUrl,
|
|
1066
2050
|
credentialRef: config.credentialRef
|
|
1067
2051
|
}));
|
|
2052
|
+
const workspaceUpgrade = new WorkspaceUpgrade(ctx, {
|
|
2053
|
+
baseUrl,
|
|
2054
|
+
credentialRef: config.credentialRef
|
|
2055
|
+
});
|
|
1068
2056
|
/**
|
|
1069
2057
|
* Adopt the publisher's models once the workspace has a session.
|
|
1070
2058
|
*
|
|
@@ -1086,7 +2074,7 @@ async function apply(ctx, config) {
|
|
|
1086
2074
|
ctx.logger.warn(error);
|
|
1087
2075
|
}
|
|
1088
2076
|
};
|
|
1089
|
-
if (await ctx.credentials.resolve(accessRef) !== void 0)
|
|
2077
|
+
if (await ctx.credentials.resolve(accessRef) !== void 0) adoptNow();
|
|
1090
2078
|
ctx.effect(() => ctx.webServer.register({
|
|
1091
2079
|
kind: "exact",
|
|
1092
2080
|
path: MANAGED_AUTH_PATH,
|
|
@@ -1099,6 +2087,7 @@ async function apply(ctx, config) {
|
|
|
1099
2087
|
response.end();
|
|
1100
2088
|
return;
|
|
1101
2089
|
}
|
|
2090
|
+
await adoptRequestJwt(ctx, accessRef, request);
|
|
1102
2091
|
const token = bearerToken(request);
|
|
1103
2092
|
if (token === null) {
|
|
1104
2093
|
response.writeHead(401, { "content-length": "0" });
|
|
@@ -1217,12 +2206,18 @@ async function apply(ctx, config) {
|
|
|
1217
2206
|
try {
|
|
1218
2207
|
if (request.method === "GET") {
|
|
1219
2208
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
2209
|
+
const mine = url.searchParams.get("mine") === "1";
|
|
2210
|
+
const page = await plaza.listPage({
|
|
2211
|
+
tag: url.searchParams.get("tag") ?? void 0,
|
|
2212
|
+
query: url.searchParams.get("q") ?? void 0,
|
|
2213
|
+
status: url.searchParams.get("status") ?? void 0,
|
|
2214
|
+
scope: mine ? "mine" : url.searchParams.get("scope") ?? void 0,
|
|
2215
|
+
departmentCode: url.searchParams.get("departmentCode") ?? void 0
|
|
2216
|
+
});
|
|
1220
2217
|
writeJson(response, 200, {
|
|
1221
|
-
skills:
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}),
|
|
1225
|
-
tags: await plaza.tags()
|
|
2218
|
+
skills: mine ? page.skills.filter((skill) => skill.sharedByMe) : page.skills,
|
|
2219
|
+
tags: mine ? [] : await plaza.tags(),
|
|
2220
|
+
...page.counts === void 0 ? {} : { counts: page.counts }
|
|
1226
2221
|
});
|
|
1227
2222
|
return;
|
|
1228
2223
|
}
|
|
@@ -1240,17 +2235,52 @@ async function apply(ctx, config) {
|
|
|
1240
2235
|
writeJson(response, 200, { skills: await readLocalSkills(ctx) });
|
|
1241
2236
|
return;
|
|
1242
2237
|
}
|
|
1243
|
-
if (action === "share") {
|
|
2238
|
+
if (action === "share" || action === "submit") {
|
|
1244
2239
|
const name = typeof body?.name === "string" ? body.name : "";
|
|
1245
2240
|
const instruction = typeof body?.instruction === "string" ? body.instruction : "";
|
|
1246
2241
|
const description = typeof body?.description === "string" ? body.description : void 0;
|
|
1247
2242
|
const tags = Array.isArray(body?.tags) ? body.tags.filter((tag) => typeof tag === "string") : [];
|
|
1248
|
-
|
|
1249
|
-
name
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
2243
|
+
if (name.trim() === "" || instruction.trim() === "") {
|
|
2244
|
+
writeJson(response, 400, { error: "name and instruction are required" });
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
writeJson(response, 200, {
|
|
2248
|
+
ok: true,
|
|
2249
|
+
skill: await plaza.share({
|
|
2250
|
+
name,
|
|
2251
|
+
description,
|
|
2252
|
+
instruction,
|
|
2253
|
+
tags
|
|
2254
|
+
})
|
|
2255
|
+
});
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
if (action === "review") {
|
|
2259
|
+
const api = reviewApiRef;
|
|
2260
|
+
if (api === void 0) {
|
|
2261
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const id = typeof body?.id === "number" ? body.id : void 0;
|
|
2265
|
+
const decision = body?.decision === "reject" ? "reject" : body?.decision === "approve" ? "approve" : null;
|
|
2266
|
+
if (id === void 0 || decision === null) {
|
|
2267
|
+
writeJson(response, 400, { error: "id and decision are required" });
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const result = await api.request("/api/ai/review/decide", {
|
|
2271
|
+
method: "POST",
|
|
2272
|
+
body: JSON.stringify({
|
|
2273
|
+
skills: [id],
|
|
2274
|
+
workflows: [],
|
|
2275
|
+
decision,
|
|
2276
|
+
note: typeof body?.note === "string" ? body.note : "",
|
|
2277
|
+
authorization: {
|
|
2278
|
+
confirmed: true,
|
|
2279
|
+
reason: "skill-plaza-review"
|
|
2280
|
+
}
|
|
2281
|
+
})
|
|
2282
|
+
});
|
|
2283
|
+
writeJson(response, result.status, result.body);
|
|
1254
2284
|
return;
|
|
1255
2285
|
}
|
|
1256
2286
|
if (action === "unshare") {
|
|
@@ -1275,6 +2305,306 @@ async function apply(ctx, config) {
|
|
|
1275
2305
|
}
|
|
1276
2306
|
}), "dataops-managed: skill plaza routes");
|
|
1277
2307
|
/**
|
|
2308
|
+
* Review context, notifications, AI scan/decide, the permission centre and review settings.
|
|
2309
|
+
*
|
|
2310
|
+
* These are thin same-origin bridges: DataOps owns identity, authorization, the state machine
|
|
2311
|
+
* and the audit trail; this route only attaches the workspace JWT and forwards answers so the
|
|
2312
|
+
* browser can show a precise waiting/refused state instead of flattening both to "empty".
|
|
2313
|
+
*/
|
|
2314
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2315
|
+
kind: "exact",
|
|
2316
|
+
path: REVIEW_CONTEXT_PATH,
|
|
2317
|
+
handler: async (request, response) => {
|
|
2318
|
+
if (request.method !== "GET") {
|
|
2319
|
+
response.writeHead(405, {
|
|
2320
|
+
allow: "GET",
|
|
2321
|
+
"content-length": "0"
|
|
2322
|
+
});
|
|
2323
|
+
response.end();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const api = reviewApiRef;
|
|
2327
|
+
if (api === void 0) {
|
|
2328
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
try {
|
|
2332
|
+
const result = await api.request("/api/ai/review/context");
|
|
2333
|
+
writeJson(response, result.status, result.body);
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
ctx.logger.warn("dataops-managed: review context failed");
|
|
2336
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}), "dataops-managed: review context route");
|
|
2340
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2341
|
+
kind: "exact",
|
|
2342
|
+
path: REVIEW_NOTIFICATIONS_PATH,
|
|
2343
|
+
handler: async (request, response) => {
|
|
2344
|
+
const api = reviewApiRef;
|
|
2345
|
+
if (api === void 0) {
|
|
2346
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
try {
|
|
2350
|
+
if (request.method === "GET") {
|
|
2351
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
2352
|
+
const params = new URLSearchParams();
|
|
2353
|
+
if (url.searchParams.get("unreadOnly") === "true") params.set("unreadOnly", "true");
|
|
2354
|
+
if (url.searchParams.get("limit") !== null) params.set("limit", url.searchParams.get("limit"));
|
|
2355
|
+
const result = await api.request("/api/ai/review-notifications" + (params.size === 0 ? "" : "?" + params.toString()));
|
|
2356
|
+
writeJson(response, result.status, result.body);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (request.method !== "POST") {
|
|
2360
|
+
response.writeHead(405, {
|
|
2361
|
+
allow: "GET, POST",
|
|
2362
|
+
"content-length": "0"
|
|
2363
|
+
});
|
|
2364
|
+
response.end();
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const body = await readJson(request);
|
|
2368
|
+
const result = await api.request("/api/ai/review-notifications/read", {
|
|
2369
|
+
method: "POST",
|
|
2370
|
+
body: JSON.stringify(body ?? {})
|
|
2371
|
+
});
|
|
2372
|
+
writeJson(response, result.status, result.body);
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
ctx.logger.warn("dataops-managed: review notifications failed");
|
|
2375
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}), "dataops-managed: review notifications route");
|
|
2379
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2380
|
+
kind: "exact",
|
|
2381
|
+
path: REVIEW_ACTION_PATH,
|
|
2382
|
+
handler: async (request, response) => {
|
|
2383
|
+
if (request.method !== "POST") {
|
|
2384
|
+
response.writeHead(405, {
|
|
2385
|
+
allow: "POST",
|
|
2386
|
+
"content-length": "0"
|
|
2387
|
+
});
|
|
2388
|
+
response.end();
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const api = reviewApiRef;
|
|
2392
|
+
if (api === void 0) {
|
|
2393
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
try {
|
|
2397
|
+
const body = await readJson(request);
|
|
2398
|
+
const action = typeof body?.action === "string" ? body.action : "";
|
|
2399
|
+
const skills = Array.isArray(body?.skills) ? body.skills : [];
|
|
2400
|
+
const workflows = Array.isArray(body?.workflows) ? body.workflows : [];
|
|
2401
|
+
if (action === "scan") {
|
|
2402
|
+
const result = await api.request("/api/ai/review/scan", {
|
|
2403
|
+
method: "POST",
|
|
2404
|
+
body: JSON.stringify({
|
|
2405
|
+
skills,
|
|
2406
|
+
workflows
|
|
2407
|
+
})
|
|
2408
|
+
});
|
|
2409
|
+
writeJson(response, result.status, result.body);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (action === "decide") {
|
|
2413
|
+
const result = await api.request("/api/ai/review/decide", {
|
|
2414
|
+
method: "POST",
|
|
2415
|
+
body: JSON.stringify({
|
|
2416
|
+
skills,
|
|
2417
|
+
workflows,
|
|
2418
|
+
decision: body?.decision,
|
|
2419
|
+
note: body?.note,
|
|
2420
|
+
authorization: body?.authorization
|
|
2421
|
+
})
|
|
2422
|
+
});
|
|
2423
|
+
writeJson(response, result.status, result.body);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
writeJson(response, 400, { error: "unknown review action" });
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
ctx.logger.warn("dataops-managed: review action failed");
|
|
2429
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}), "dataops-managed: review action route");
|
|
2433
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2434
|
+
kind: "exact",
|
|
2435
|
+
path: RBAC_PATH,
|
|
2436
|
+
handler: async (request, response) => {
|
|
2437
|
+
const api = reviewApiRef;
|
|
2438
|
+
if (api === void 0) {
|
|
2439
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
try {
|
|
2443
|
+
if (request.method === "GET") {
|
|
2444
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
2445
|
+
const section = url.searchParams.get("section") ?? "matrix";
|
|
2446
|
+
const suffix = section === "members" ? "?q=" + encodeURIComponent(url.searchParams.get("q") ?? "") + "&departmentCode=" + encodeURIComponent(url.searchParams.get("departmentCode") ?? "") : section === "resources" ? "?kind=" + encodeURIComponent(url.searchParams.get("kind") ?? "") : "";
|
|
2447
|
+
const target = section === "members" ? "/api/system/permission-center/members" + suffix : section === "departments" ? "/api/system/permission-center/departments" : section === "resources" ? "/api/system/permission-center/resources" + suffix : "/api/system/permission-center/matrix" + (url.searchParams.get("scope") === "dsh" ? "?scope=dsh" : "");
|
|
2448
|
+
const result = await api.request(target);
|
|
2449
|
+
writeJson(response, result.status, result.body);
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (request.method !== "PUT") {
|
|
2453
|
+
response.writeHead(405, {
|
|
2454
|
+
allow: "GET, PUT",
|
|
2455
|
+
"content-length": "0"
|
|
2456
|
+
});
|
|
2457
|
+
response.end();
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
const body = await readJson(request);
|
|
2461
|
+
const section = typeof body?.section === "string" ? body.section : "";
|
|
2462
|
+
const value = typeof body?.value === "object" && body.value !== null ? body.value : {};
|
|
2463
|
+
if (section === "roles") {
|
|
2464
|
+
const result = await api.request("/api/system/permission-center/matrix", {
|
|
2465
|
+
method: "PUT",
|
|
2466
|
+
body: JSON.stringify(value)
|
|
2467
|
+
});
|
|
2468
|
+
writeJson(response, result.status, result.body);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
if (section === "members" && typeof value.userId === "string") {
|
|
2472
|
+
const result = await api.request("/api/system/permission-center/members/" + encodeURIComponent(value.userId) + "/roles", {
|
|
2473
|
+
method: "PUT",
|
|
2474
|
+
body: JSON.stringify({ roleCodes: value.roleCodes })
|
|
2475
|
+
});
|
|
2476
|
+
writeJson(response, result.status, result.body);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (section === "departments" && typeof value.code === "string") {
|
|
2480
|
+
const result = await api.request("/api/system/permission-center/departments/" + encodeURIComponent(value.code) + "/admin", {
|
|
2481
|
+
method: "PUT",
|
|
2482
|
+
body: JSON.stringify({ userIds: value.userIds })
|
|
2483
|
+
});
|
|
2484
|
+
writeJson(response, result.status, result.body);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
if (section === "resources" && typeof value.kind === "string" && value.id !== void 0) {
|
|
2488
|
+
const result = await api.request("/api/system/permission-center/resources/" + encodeURIComponent(value.kind) + "/" + encodeURIComponent(String(value.id)) + "/scope", {
|
|
2489
|
+
method: "PUT",
|
|
2490
|
+
body: JSON.stringify({
|
|
2491
|
+
departmentCode: value.departmentCode,
|
|
2492
|
+
visibility: value.visibility,
|
|
2493
|
+
ownerId: value.ownerId
|
|
2494
|
+
})
|
|
2495
|
+
});
|
|
2496
|
+
writeJson(response, result.status, result.body);
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
writeJson(response, 400, { error: "unknown rbac section" });
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
ctx.logger.warn("dataops-managed: permission centre failed");
|
|
2502
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}), "dataops-managed: permission centre route");
|
|
2506
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2507
|
+
kind: "exact",
|
|
2508
|
+
path: DEPARTMENTS_PATH,
|
|
2509
|
+
handler: async (request, response) => {
|
|
2510
|
+
const api = reviewApiRef;
|
|
2511
|
+
if (api === void 0) {
|
|
2512
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
try {
|
|
2516
|
+
const body = await readJson(request);
|
|
2517
|
+
if (request.method === "POST") {
|
|
2518
|
+
const result = await api.request("/api/system/departments", {
|
|
2519
|
+
method: "POST",
|
|
2520
|
+
body: JSON.stringify(body ?? {})
|
|
2521
|
+
});
|
|
2522
|
+
writeJson(response, result.status, result.body);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
if (request.method !== "PATCH" && request.method !== "DELETE") {
|
|
2526
|
+
response.writeHead(405, {
|
|
2527
|
+
allow: "POST, PATCH, DELETE",
|
|
2528
|
+
"content-length": "0"
|
|
2529
|
+
});
|
|
2530
|
+
response.end();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const code = typeof body?.code === "string" ? body.code : "";
|
|
2534
|
+
if (code === "") {
|
|
2535
|
+
writeJson(response, 400, { error: "department code is required" });
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
const current = await api.request("/api/system/departments/" + encodeURIComponent(code));
|
|
2539
|
+
const record = typeof current.body === "object" && current.body !== null ? current.body : {};
|
|
2540
|
+
const department = typeof record.department === "object" && record.department !== null ? record.department : {};
|
|
2541
|
+
const updatedAt = typeof body?.updatedAt === "string" ? body.updatedAt : department.updatedAt;
|
|
2542
|
+
if (typeof updatedAt !== "string") {
|
|
2543
|
+
writeJson(response, current.status === 200 ? 409 : current.status, { error: "department not found" });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (request.method === "PATCH") {
|
|
2547
|
+
const result = await api.request("/api/system/departments/" + encodeURIComponent(code), {
|
|
2548
|
+
method: "PATCH",
|
|
2549
|
+
body: JSON.stringify({
|
|
2550
|
+
updatedAt,
|
|
2551
|
+
name: body?.name,
|
|
2552
|
+
parentCode: body?.parentCode,
|
|
2553
|
+
description: body?.description,
|
|
2554
|
+
owner: body?.owner,
|
|
2555
|
+
enabled: body?.enabled
|
|
2556
|
+
})
|
|
2557
|
+
});
|
|
2558
|
+
writeJson(response, result.status, result.body);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
const result = await api.request("/api/system/departments/" + encodeURIComponent(code), {
|
|
2562
|
+
method: "DELETE",
|
|
2563
|
+
body: JSON.stringify({ updatedAt })
|
|
2564
|
+
});
|
|
2565
|
+
writeJson(response, result.status, result.body);
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
ctx.logger.warn("dataops-managed: department bridge failed");
|
|
2568
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}), "dataops-managed: department CRUD bridge");
|
|
2572
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2573
|
+
kind: "exact",
|
|
2574
|
+
path: REVIEW_SETTINGS_PATH,
|
|
2575
|
+
handler: async (request, response) => {
|
|
2576
|
+
const api = reviewApiRef;
|
|
2577
|
+
if (api === void 0) {
|
|
2578
|
+
writeJson(response, 503, { error: "review api is not active" });
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
try {
|
|
2582
|
+
if (request.method === "GET") {
|
|
2583
|
+
const result = await api.request("/api/integrations/dataops/review-settings");
|
|
2584
|
+
writeJson(response, result.status, result.body);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (request.method !== "PUT" && request.method !== "POST") {
|
|
2588
|
+
response.writeHead(405, {
|
|
2589
|
+
allow: "GET, POST, PUT",
|
|
2590
|
+
"content-length": "0"
|
|
2591
|
+
});
|
|
2592
|
+
response.end();
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
const body = await readJson(request);
|
|
2596
|
+
const result = await api.request("/api/integrations/dataops/review-settings", {
|
|
2597
|
+
method: "POST",
|
|
2598
|
+
body: JSON.stringify(body ?? {})
|
|
2599
|
+
});
|
|
2600
|
+
writeJson(response, result.status, result.body);
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
ctx.logger.warn("dataops-managed: review settings failed");
|
|
2603
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}), "dataops-managed: review settings route");
|
|
2607
|
+
/**
|
|
1278
2608
|
* The workspace limits an administrator sets in DataOps.
|
|
1279
2609
|
*
|
|
1280
2610
|
* Read through the plugin because it holds the DataOps JWT; the page cannot reach those settings
|
|
@@ -1378,6 +2708,67 @@ async function apply(ctx, config) {
|
|
|
1378
2708
|
}
|
|
1379
2709
|
}
|
|
1380
2710
|
}), "dataops-managed: tool timeout route");
|
|
2711
|
+
/**
|
|
2712
|
+
* Workspace image upgrade: DataOps owns the image comparison and restart; this
|
|
2713
|
+
* plugin adds the DSH-side running-session snapshot and post-restart recovery.
|
|
2714
|
+
*/
|
|
2715
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2716
|
+
kind: "exact",
|
|
2717
|
+
path: WORKSPACE_UPGRADE_PATH,
|
|
2718
|
+
handler: async (request, response) => {
|
|
2719
|
+
try {
|
|
2720
|
+
if (request.method === "GET") {
|
|
2721
|
+
const preview = await workspaceUpgrade.preview();
|
|
2722
|
+
writeJson(response, 200, {
|
|
2723
|
+
imageStatus: preview.imageStatus,
|
|
2724
|
+
configuredImage: preview.configuredImage,
|
|
2725
|
+
activeSessions: preview.activeSessions
|
|
2726
|
+
});
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
if (request.method !== "POST") {
|
|
2730
|
+
response.writeHead(405, {
|
|
2731
|
+
allow: "GET, POST",
|
|
2732
|
+
"content-length": "0"
|
|
2733
|
+
});
|
|
2734
|
+
response.end();
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const state = await workspaceUpgrade.upgrade();
|
|
2738
|
+
writeJson(response, 202, {
|
|
2739
|
+
accepted: true,
|
|
2740
|
+
upgradeId: state.upgradeId,
|
|
2741
|
+
sessions: state.sessions.length
|
|
2742
|
+
});
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
writeJson(response, error instanceof WorkspaceUpgradeError ? error.status : 502, { error: error instanceof Error ? error.message : String(error) });
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}), "dataops-managed: workspace upgrade route");
|
|
2748
|
+
recoverWorkspaceUpgradeAfterBoot(workspaceUpgrade);
|
|
2749
|
+
/**
|
|
2750
|
+
* What the last DataOps answer said about this workspace's credential.
|
|
2751
|
+
*
|
|
2752
|
+
* The settings surfaces need this because a 401 is handled deep inside a call that degrades to
|
|
2753
|
+
* "no data", so the panel renders as if the workspace simply has nothing to show. Reading this
|
|
2754
|
+
* route is how the interface learns the reason is an ended session rather than an empty one.
|
|
2755
|
+
* The recorded state is an observation, never an authority: DataOps still decides.
|
|
2756
|
+
*/
|
|
2757
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2758
|
+
kind: "exact",
|
|
2759
|
+
path: AUTH_STATE_PATH,
|
|
2760
|
+
handler: async (request, response) => {
|
|
2761
|
+
if (request.method !== "GET") {
|
|
2762
|
+
response.writeHead(405, {
|
|
2763
|
+
allow: "GET",
|
|
2764
|
+
"content-length": "0"
|
|
2765
|
+
});
|
|
2766
|
+
response.end();
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
writeJson(response, 200, { state: currentAuthState() });
|
|
2770
|
+
}
|
|
2771
|
+
}), "dataops-managed: auth state route");
|
|
1381
2772
|
await connectExisting();
|
|
1382
2773
|
}
|
|
1383
2774
|
/** Read and parse a JSON request body, tolerating an absent one. */
|
|
@@ -1402,4 +2793,4 @@ function writeJson(response, status, body) {
|
|
|
1402
2793
|
response.end(payload);
|
|
1403
2794
|
}
|
|
1404
2795
|
//#endregion
|
|
1405
|
-
export { Config, DEFAULT_TOOL_CALL_TIMEOUT_MS, MANAGED_AUTH_PATH, MANAGED_PLUGIN_ID, MODEL_SYNC_PATH, SKILL_PLAZA_PATH, TOOL_TIMEOUT_MAX_SECONDS, TOOL_TIMEOUT_MIN_SECONDS, TOOL_TIMEOUT_PATH, WORKSPACE_LIMITS_PATH, apply, currentModelSync, currentSettingsSync, currentSkillPlaza, currentWorkspaceLimits, inject, name };
|
|
2796
|
+
export { AUTH_STATE_PATH, Config, DEFAULT_TOOL_CALL_TIMEOUT_MS, DEPARTMENTS_PATH, MANAGED_AUTH_PATH, MANAGED_PLUGIN_ID, MODEL_SYNC_PATH, RBAC_PATH, REVIEW_ACTION_PATH, REVIEW_CONTEXT_PATH, REVIEW_NOTIFICATIONS_PATH, REVIEW_SETTINGS_PATH, SKILL_PLAZA_PATH, TOOL_TIMEOUT_MAX_SECONDS, TOOL_TIMEOUT_MIN_SECONDS, TOOL_TIMEOUT_PATH, WORKSPACE_LIMITS_PATH, WORKSPACE_UPGRADE_PATH, adoptRequestJwt, apply, currentModelSync, currentSettingsSync, currentSkillPlaza, currentWorkspaceLimits, inject, name };
|