@sparkelf/dsh-dataops-managed 0.3.19 → 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/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) return void 0;
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) return null;
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) return void 0;
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
- if (!Array.isArray(body)) return [];
624
- return body.flatMap((row) => this.toPlazaSkill(row));
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) return void 0;
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) return void 0;
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) await adoptNow();
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: await plaza.list({
1222
- tag: url.searchParams.get("tag") ?? void 0,
1223
- query: url.searchParams.get("q") ?? void 0
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
- writeJson(response, 200, { skill: await plaza.share({
1249
- name,
1250
- description,
1251
- instruction,
1252
- tags
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 };