@sparkelf/dsh-dataops-managed 0.3.13 → 0.3.15

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
@@ -14,17 +14,17 @@ function modelIdOf(entry) {
14
14
  return typeof id === "string" && id.trim() !== "" ? id : null;
15
15
  }
16
16
  /** Narrow an unknown value to a plain object. */
17
- function asRecord$3(value) {
17
+ function asRecord$4(value) {
18
18
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
19
19
  }
20
20
  /** Read provider profiles out of a settings value. */
21
21
  function providersOf(value) {
22
- const root = asRecord$3(value);
23
- const providers = root === null ? null : asRecord$3(root[PROVIDERS_FIELD]);
22
+ const root = asRecord$4(value);
23
+ const providers = root === null ? null : asRecord$4(root[PROVIDERS_FIELD]);
24
24
  if (providers === null) return {};
25
25
  const result = {};
26
26
  for (const [providerId, profile] of Object.entries(providers)) {
27
- const narrowed = asRecord$3(profile);
27
+ const narrowed = asRecord$4(profile);
28
28
  if (narrowed !== null) result[providerId] = narrowed;
29
29
  }
30
30
  return result;
@@ -58,7 +58,7 @@ function modelIdsByProvider(providers) {
58
58
  for (const [providerId, profile] of Object.entries(providers)) {
59
59
  const models = profile[MODELS_FIELD];
60
60
  if (!Array.isArray(models)) continue;
61
- result[providerId] = models.map((entry) => asRecord$3(entry)).filter((entry) => entry !== null).map((entry) => modelIdOf(entry)).filter((id) => id !== null);
61
+ result[providerId] = models.map((entry) => asRecord$4(entry)).filter((entry) => entry !== null).map((entry) => modelIdOf(entry)).filter((id) => id !== null);
62
62
  }
63
63
  return result;
64
64
  }
@@ -79,7 +79,7 @@ function selectSharedModels(providers, sharedModelIds) {
79
79
  const models = profile[MODELS_FIELD];
80
80
  if (!Array.isArray(models)) continue;
81
81
  const kept = models.filter((entry) => {
82
- const narrowed = asRecord$3(entry);
82
+ const narrowed = asRecord$4(entry);
83
83
  const id = narrowed === null ? null : modelIdOf(narrowed);
84
84
  return id !== null && offered.has(id);
85
85
  });
@@ -113,9 +113,9 @@ function mergeProviders(local, adopted) {
113
113
  continue;
114
114
  }
115
115
  const localModels = Array.isArray(localProfile[MODELS_FIELD]) ? localProfile[MODELS_FIELD] : [];
116
- const localIds = new Set(localModels.map((entry) => asRecord$3(entry)).filter((entry) => entry !== null).map((entry) => modelIdOf(entry)).filter((id) => id !== null));
116
+ const localIds = new Set(localModels.map((entry) => asRecord$4(entry)).filter((entry) => entry !== null).map((entry) => modelIdOf(entry)).filter((id) => id !== null));
117
117
  const missing = adoptedModels.filter((entry) => {
118
- const narrowed = asRecord$3(entry);
118
+ const narrowed = asRecord$4(entry);
119
119
  const id = narrowed === null ? null : modelIdOf(narrowed);
120
120
  return id !== null && !localIds.has(id);
121
121
  });
@@ -147,7 +147,7 @@ async function writeModels(ctx, providers, revision) {
147
147
  //#endregion
148
148
  //#region src/model-sync.ts
149
149
  /** Narrow an unknown value to a plain object. */
150
- function asRecord$2(value) {
150
+ function asRecord$3(value) {
151
151
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
152
152
  }
153
153
  /**
@@ -190,10 +190,10 @@ var ModelSync = class {
190
190
  });
191
191
  if (response.status === 401) return void 0;
192
192
  if (!response.ok) throw new Error(`Shared model read failed with HTTP ${String(response.status)}`);
193
- const body = asRecord$2(await response.json());
193
+ const body = asRecord$3(await response.json());
194
194
  if (body === null) return void 0;
195
- const payload = asRecord$2(body.payload);
196
- const providers = payload === null ? null : asRecord$2(payload.providers);
195
+ const payload = asRecord$3(body.payload);
196
+ const providers = payload === null ? null : asRecord$3(payload.providers);
197
197
  return {
198
198
  sharingEnabled: body.sharingEnabled === true,
199
199
  payload: {
@@ -257,7 +257,7 @@ var ModelSync = class {
257
257
  const response = await fetch(new URL("/api/ai/shared-models/publication", this.apiBase()), { headers: { authorization: `Bearer ${token}` } });
258
258
  if (response.status === 401) return null;
259
259
  if (!response.ok) throw new Error(`Shared model status read failed with HTTP ${String(response.status)}`);
260
- const body = asRecord$2(await response.json());
260
+ const body = asRecord$3(await response.json());
261
261
  return {
262
262
  sharingEnabled: body?.sharingEnabled === true,
263
263
  canPublish: body?.canPublish === true
@@ -391,7 +391,7 @@ var ModelSync = class {
391
391
  /** Default state before this workspace stored anything. */
392
392
  const SECTION_SYNC_DEFAULT = { sections: {} };
393
393
  /** Narrow an unknown value to a plain object. */
394
- function asRecord$1(value) {
394
+ function asRecord$2(value) {
395
395
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
396
396
  }
397
397
  /**
@@ -430,7 +430,7 @@ var SettingsSync = class {
430
430
  readSection(ns) {
431
431
  const descriptor = this.ctx.settings.describe().find((entry) => entry.ns === ns);
432
432
  if (descriptor === void 0) return void 0;
433
- const value = asRecord$1(descriptor.value);
433
+ const value = asRecord$2(descriptor.value);
434
434
  if (value === null) return void 0;
435
435
  return {
436
436
  value,
@@ -534,7 +534,7 @@ var SettingsSync = class {
534
534
  //#endregion
535
535
  //#region src/skill-plaza.ts
536
536
  /** Narrow an unknown value to a plain object. */
537
- function asRecord(value) {
537
+ function asRecord$1(value) {
538
538
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
539
539
  }
540
540
  /**
@@ -658,7 +658,7 @@ var SkillPlaza = class {
658
658
  method: "POST",
659
659
  body: JSON.stringify({ skillId })
660
660
  });
661
- const record = body === void 0 ? null : asRecord(body);
661
+ const record = body === void 0 ? null : asRecord$1(body);
662
662
  if (record === null) return null;
663
663
  return {
664
664
  installed: record.installed === true,
@@ -668,7 +668,7 @@ var SkillPlaza = class {
668
668
  }
669
669
  /** Project one API row into the shape the browser reads. */
670
670
  toPlazaSkill(value) {
671
- const row = asRecord(value);
671
+ const row = asRecord$1(value);
672
672
  if (row === null || typeof row.id !== "number" || typeof row.name !== "string") return [];
673
673
  return [{
674
674
  id: row.id,
@@ -682,6 +682,119 @@ var SkillPlaza = class {
682
682
  }
683
683
  };
684
684
  //#endregion
685
+ //#region src/workspace-limits.ts
686
+ /** Narrow a value to a plain object. */
687
+ function asRecord(value) {
688
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
689
+ }
690
+ /** Narrow a value to a positive integer. */
691
+ function asByteCount(value) {
692
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
693
+ }
694
+ /**
695
+ * A rejection DataOps answered with, carrying its status and message.
696
+ *
697
+ * Without this the caller can only tell that something failed, and a value DataOps refused —
698
+ * which the user can correct — is indistinguishable from a fault in the request path.
699
+ */
700
+ var WorkspaceLimitsRejection = class extends Error {
701
+ status;
702
+ constructor(status, message) {
703
+ super(message);
704
+ this.status = status;
705
+ this.name = "WorkspaceLimitsRejection";
706
+ }
707
+ };
708
+ /** Read DataOps' own explanation from a failed response, falling back to the status line. */
709
+ async function rejectionMessage(response, fallback) {
710
+ try {
711
+ const message = asRecord(await response.json())?.message;
712
+ if (typeof message === "string" && message.trim() !== "") return message;
713
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
714
+ } catch {}
715
+ return `${fallback} with HTTP ${String(response.status)}`;
716
+ }
717
+ /**
718
+ * The workspace limits an administrator sets in DataOps.
719
+ *
720
+ * The plugin holds the DataOps JWT, so the browser cannot read these itself; this class is the
721
+ * only path from the Settings panel to that document.
722
+ */
723
+ var WorkspaceLimits = class {
724
+ ctx;
725
+ config;
726
+ constructor(ctx, config) {
727
+ this.ctx = ctx;
728
+ this.config = config;
729
+ }
730
+ /**
731
+ * Read the DataOps JWT this workspace is currently authorized with.
732
+ * @returns The token, or undefined when the workspace has no session yet.
733
+ */
734
+ async token() {
735
+ return (await this.ctx.credentials.resolve(credentialRef(this.config.credentialRef)))?.value;
736
+ }
737
+ /** The DataOps API origin this workspace reports to. */
738
+ apiBase() {
739
+ return new URL(this.config.baseUrl).origin;
740
+ }
741
+ /**
742
+ * Read the current ceilings.
743
+ * @returns The status, or undefined when the workspace is not authorized.
744
+ */
745
+ async status() {
746
+ const token = await this.token();
747
+ if (token === void 0) return void 0;
748
+ const response = await fetch(new URL("/api/ai/attachment-settings", this.apiBase()), { headers: { authorization: `Bearer ${token}` } });
749
+ if (response.status === 401) return void 0;
750
+ if (!response.ok) throw new WorkspaceLimitsRejection(response.status, await rejectionMessage(response, "Workspace limits read failed"));
751
+ const body = asRecord(await response.json());
752
+ const settings = body === null ? null : asRecord(body.settings);
753
+ const size = settings === null ? null : asRecord(settings.workspaceSize);
754
+ const fileReadMaxBytes = size === null ? null : asByteCount(size.fileReadMaxBytes);
755
+ const uploadMaxBytes = size === null ? null : asByteCount(size.uploadMaxBytes);
756
+ if (fileReadMaxBytes === null || uploadMaxBytes === null) throw new Error("Workspace limits read returned an unrecognized document");
757
+ return {
758
+ limits: {
759
+ fileReadMaxBytes,
760
+ uploadMaxBytes
761
+ },
762
+ canWrite: true,
763
+ readOnlyReason: null
764
+ };
765
+ }
766
+ /**
767
+ * Write new ceilings.
768
+ * @param limits - The ceilings to store.
769
+ * @returns The stored status.
770
+ */
771
+ async save(limits) {
772
+ const token = await this.token();
773
+ if (token === void 0) return void 0;
774
+ const response = await fetch(new URL("/api/ai/attachment-settings", this.apiBase()), {
775
+ method: "PATCH",
776
+ headers: {
777
+ authorization: `Bearer ${token}`,
778
+ "content-type": "application/json"
779
+ },
780
+ body: JSON.stringify({ workspaceSize: limits })
781
+ });
782
+ if (response.status === 401) return void 0;
783
+ if (!response.ok) throw new WorkspaceLimitsRejection(response.status, await rejectionMessage(response, "Workspace limits write failed"));
784
+ return this.status();
785
+ }
786
+ };
787
+ /** The live instance, so the Settings route can act on it after activation. */
788
+ let limitsRef;
789
+ /** @param next - Instance to publish, or undefined to clear. */
790
+ function setWorkspaceLimits(next) {
791
+ limitsRef = next;
792
+ }
793
+ /** @returns The live workspace-limits instance, when the plugin activated. */
794
+ function currentWorkspaceLimits() {
795
+ return limitsRef;
796
+ }
797
+ //#endregion
685
798
  //#region src/index.ts
686
799
  /** Cordis plugin name for the DataOps-managed integration. */
687
800
  const name = "dataops-managed";
@@ -700,6 +813,8 @@ const MANAGED_PLUGIN_ID = "dataops-managed";
700
813
  const MODEL_SYNC_PATH = "/integrations/dataops/model-sync";
701
814
  /** Same-origin route the skill center uses to read and change plaza sharing. */
702
815
  const SKILL_PLAZA_PATH = "/integrations/dataops/skill-plaza";
816
+ /** Same-origin route the Settings panel uses to read and change this workspace's limits. */
817
+ const WORKSPACE_LIMITS_PATH = "/integrations/dataops/workspace-limits";
703
818
  /** The live sync instance, so the settings routes can act on it after activation. */
704
819
  let modelSyncRef;
705
820
  /** The live plaza instance, so the skill center routes can act on it after activation. */
@@ -718,21 +833,54 @@ let settingsSyncRef;
718
833
  function currentSettingsSync() {
719
834
  return settingsSyncRef;
720
835
  }
836
+ /**
837
+ * Read the value behind a possibly-volatile field.
838
+ *
839
+ * A volatile field always arrives as a reference, but the plain shape is accepted too so a
840
+ * caller that validated the config without a settings service still reads its value.
841
+ * @param field - The config field as the runtime supplied it.
842
+ * @param fallback - Value to use when nothing is stored yet.
843
+ * @returns The current value.
844
+ */
845
+ function volatileValue(field, fallback) {
846
+ if (field === void 0) return fallback;
847
+ if (typeof field === "object" && field !== null && "get" in field && typeof field.get === "function") return field.get() ?? fallback;
848
+ return field;
849
+ }
850
+ /**
851
+ * Per-tool-call timeout a workspace starts with.
852
+ *
853
+ * Named rather than inlined so the schema default and the read fallback cannot drift: the field is
854
+ * volatile, so a workspace that never changed it reads no value at all and both places have to
855
+ * answer the same number.
856
+ */
857
+ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 3e5;
858
+ /** Default sync state before this workspace stored anything. */
859
+ const MODEL_SYNC_DEFAULT = {
860
+ detached: false,
861
+ adoptedModelIds: [],
862
+ privateModelIds: []
863
+ };
721
864
  /** Schemastery parser for managed DataOps configuration. */
722
865
  const Config = z.object({
723
866
  baseUrl: z.string().default("http://host.docker.internal:3101"),
724
867
  serverName: z.string().default("dataops"),
725
868
  credentialRef: z.string().role("credential-ref").default("DATAOPS_ACCESS_TOKEN"),
726
- toolCallTimeoutMs: z.number().min(1).default(12e4),
869
+ /**
870
+ * Volatile so the settings service persists it in this workspace's document: the timeout an
871
+ * operator needs depends on the query behind a tool, which is deployment knowledge the profile
872
+ * that mounts this plugin does not have.
873
+ *
874
+ * `z.any()` like the sync state above, and for the same reason: a volatile field is delivered
875
+ * to `apply` as a reference rather than a value, so a narrowed schema would describe a shape
876
+ * the runtime does not pass. The range is enforced where the value is read.
877
+ */
878
+ toolCallTimeoutMs: z.any().default(DEFAULT_TOOL_CALL_TIMEOUT_MS).volatile(),
727
879
  /**
728
880
  * Volatile so the settings service persists it in this workspace's document. The shape is
729
881
  * left open because the sync state is this plugin's own record, not user configuration.
730
882
  */
731
- modelSync: z.any().default({
732
- detached: false,
733
- adoptedModelIds: [],
734
- privateModelIds: []
735
- }).volatile(),
883
+ modelSync: z.any().default(MODEL_SYNC_DEFAULT).volatile(),
736
884
  /** Volatile for the same reason as `modelSync`: it is this plugin's own record. */
737
885
  settingsSync: z.any().default(SECTION_SYNC_DEFAULT).volatile(),
738
886
  /**
@@ -765,7 +913,7 @@ async function apply(ctx, config) {
765
913
  url: `${baseUrl}/api/ai/data-query/mcp`,
766
914
  headers: {},
767
915
  bearerTokenRef: config.credentialRef,
768
- toolCallTimeoutMs: config.toolCallTimeoutMs
916
+ toolCallTimeoutMs: volatileValue(config.toolCallTimeoutMs, DEFAULT_TOOL_CALL_TIMEOUT_MS)
769
917
  });
770
918
  mcpFiber = fiber;
771
919
  try {
@@ -781,23 +929,47 @@ async function apply(ctx, config) {
781
929
  mcpFiber = void 0;
782
930
  if (fiber !== void 0 && fiber.uid !== null) await fiber.dispose();
783
931
  }, "dataops-managed: MCP lifecycle");
784
- if (await ctx.credentials.resolve(accessRef) !== void 0) await ensureMcp();
932
+ /**
933
+ * Connect the MCP client when a JWT is already stored.
934
+ *
935
+ * A connect can fail for reasons this plugin does not own — the stored JWT may have expired,
936
+ * or DataOps may be unreachable at this moment — and those are states the settings routes
937
+ * report and recover from. Awaiting it here would abort activation, leaving the plugin with
938
+ * no routes at all: the panel then cannot read the very state that explains the failure, and
939
+ * the browser reports a connection error instead. The routes below are registered first, and
940
+ * a failed connect is reported rather than thrown.
941
+ */
942
+ const connectExisting = async () => {
943
+ if (await ctx.credentials.resolve(accessRef) === void 0) return;
944
+ try {
945
+ await ensureMcp();
946
+ } catch (error) {
947
+ ctx.logger.warn("dataops-managed: connecting the DataOps MCP server failed; the settings routes stay up so a new JWT can be connected");
948
+ ctx.logger.warn(error);
949
+ }
950
+ };
785
951
  /**
786
952
  * The sync state lives in this plugin's own Config, so the settings service persists it in
787
953
  * the workspace document and it survives a restart.
788
954
  */
789
955
  const syncStore = {
790
- read: () => ({
791
- detached: config.modelSync?.detached === true,
792
- adoptedModelIds: Array.isArray(config.modelSync?.adoptedModelIds) ? config.modelSync.adoptedModelIds : [],
793
- privateModelIds: Array.isArray(config.modelSync?.privateModelIds) ? config.modelSync.privateModelIds : []
794
- }),
956
+ read: () => {
957
+ const state = volatileValue(config.modelSync, MODEL_SYNC_DEFAULT);
958
+ return {
959
+ detached: state?.detached === true,
960
+ adoptedModelIds: Array.isArray(state?.adoptedModelIds) ? state.adoptedModelIds : [],
961
+ privateModelIds: Array.isArray(state?.privateModelIds) ? state.privateModelIds : []
962
+ };
963
+ },
795
964
  write: async (next) => {
796
965
  await ctx.settings.update(MANAGED_PLUGIN_ID, { modelSync: next });
797
966
  }
798
967
  };
799
968
  const settingsSync = new SettingsSync(ctx, {
800
- read: () => ({ sections: typeof config.settingsSync?.sections === "object" && config.settingsSync.sections !== null ? config.settingsSync.sections : {} }),
969
+ read: () => {
970
+ const state = volatileValue(config.settingsSync, SECTION_SYNC_DEFAULT);
971
+ return { sections: typeof state?.sections === "object" && state.sections !== null ? state.sections : {} };
972
+ },
801
973
  write: async (next) => {
802
974
  await ctx.settings.update(MANAGED_PLUGIN_ID, { settingsSync: next });
803
975
  }
@@ -843,6 +1015,10 @@ async function apply(ctx, config) {
843
1015
  baseUrl,
844
1016
  credentialRef: config.credentialRef
845
1017
  });
1018
+ setWorkspaceLimits(new WorkspaceLimits(ctx, {
1019
+ baseUrl,
1020
+ credentialRef: config.credentialRef
1021
+ }));
846
1022
  /**
847
1023
  * Adopt the publisher's models once the workspace has a session.
848
1024
  *
@@ -1052,6 +1228,67 @@ async function apply(ctx, config) {
1052
1228
  }
1053
1229
  }
1054
1230
  }), "dataops-managed: skill plaza routes");
1231
+ /**
1232
+ * The workspace limits an administrator sets in DataOps.
1233
+ *
1234
+ * Read through the plugin because it holds the DataOps JWT; the page cannot reach those settings
1235
+ * itself. Same-origin only, like its neighbours.
1236
+ */
1237
+ ctx.effect(() => ctx.webServer.register({
1238
+ kind: "exact",
1239
+ path: WORKSPACE_LIMITS_PATH,
1240
+ handler: async (request, response) => {
1241
+ const limits = currentWorkspaceLimits();
1242
+ if (limits === void 0) {
1243
+ writeJson(response, 503, { error: "workspace limits are not active" });
1244
+ return;
1245
+ }
1246
+ try {
1247
+ if (request.method === "GET") {
1248
+ const status = await limits.status();
1249
+ if (status === void 0) {
1250
+ writeJson(response, 503, { error: "workspace limits are not active" });
1251
+ return;
1252
+ }
1253
+ writeJson(response, 200, status);
1254
+ return;
1255
+ }
1256
+ if (request.method !== "POST") {
1257
+ response.writeHead(405, {
1258
+ allow: "GET, POST",
1259
+ "content-length": "0"
1260
+ });
1261
+ response.end();
1262
+ return;
1263
+ }
1264
+ const body = await readJson(request);
1265
+ const fileReadMaxBytes = body?.fileReadMaxBytes;
1266
+ const uploadMaxBytes = body?.uploadMaxBytes;
1267
+ if (typeof fileReadMaxBytes !== "number" || typeof uploadMaxBytes !== "number") {
1268
+ writeJson(response, 400, { error: "both byte ceilings are required" });
1269
+ return;
1270
+ }
1271
+ const status = await limits.save({
1272
+ fileReadMaxBytes,
1273
+ uploadMaxBytes
1274
+ });
1275
+ if (status === void 0) {
1276
+ writeJson(response, 503, { error: "workspace limits are not active" });
1277
+ return;
1278
+ }
1279
+ writeJson(response, 200, status);
1280
+ } catch (error) {
1281
+ if (error instanceof WorkspaceLimitsRejection) {
1282
+ writeJson(response, error.status, { error: error.message });
1283
+ return;
1284
+ }
1285
+ ctx.logger.warn("dataops-managed: workspace limits request failed");
1286
+ ctx.logger.warn(error);
1287
+ writeJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
1288
+ }
1289
+ }
1290
+ }), "dataops-managed: workspace limits route");
1291
+ await connectExisting();
1055
1292
  }
1056
1293
  /** Read and parse a JSON request body, tolerating an absent one. */
1057
1294
  async function readJson(request) {
@@ -1075,4 +1312,4 @@ function writeJson(response, status, body) {
1075
1312
  response.end(payload);
1076
1313
  }
1077
1314
  //#endregion
1078
- export { Config, MANAGED_AUTH_PATH, MANAGED_PLUGIN_ID, MODEL_SYNC_PATH, SKILL_PLAZA_PATH, apply, currentModelSync, currentSettingsSync, currentSkillPlaza, inject, name };
1315
+ export { Config, DEFAULT_TOOL_CALL_TIMEOUT_MS, MANAGED_AUTH_PATH, MANAGED_PLUGIN_ID, MODEL_SYNC_PATH, SKILL_PLAZA_PATH, WORKSPACE_LIMITS_PATH, apply, currentModelSync, currentSettingsSync, currentSkillPlaza, currentWorkspaceLimits, inject, name };
@@ -1,6 +1,5 @@
1
1
  import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { en } from './locales.ts';
3
- import { MODEL_SYNC_PATH } from './model-sync-client.ts';
4
3
  /** Values injected by the DSH Settings slot. */
5
4
  export interface ManagedDataOpsSectionInjected {
6
5
  /** Translate one managed DataOps Settings message key. */
@@ -9,10 +8,9 @@ export interface ManagedDataOpsSectionInjected {
9
8
  /** Props accepted by the managed DataOps Settings section. */
10
9
  export type ManagedDataOpsSectionProps = Partial<InjectFace<ManagedDataOpsSectionInjected>>;
11
10
  /**
12
- * Explain the DataOps-managed JWT and permission owner, and share model configuration.
11
+ * The DataOps settings this workspace owns: its connection state, its branding, its file ceilings,
12
+ * and the default configuration it publishes to other workspaces.
13
13
  * @param props - Settings slot injection values.
14
14
  * @returns The localized managed DataOps section, or nothing before injection.
15
15
  */
16
16
  export declare function ManagedDataOpsSection(props: ManagedDataOpsSectionProps): import("react").JSX.Element | null;
17
- /** Re-exported so the settings entry can be discovered from the section alone. */
18
- export { MODEL_SYNC_PATH };
@@ -2,27 +2,23 @@
2
2
  export declare const en: {
3
3
  readonly nav: "DataOps";
4
4
  readonly title: "DataOps";
5
- readonly managed: "Managed by this DataOps workspace";
6
- readonly description: "DSH uses your current DataOps session JWT. DataOps applies its role and resource permissions to every tool request.";
7
- readonly identityLabel: "Identity and permissions";
8
- readonly identityValue: "Current DataOps JWT";
9
- readonly toolsLabel: "DataOps tools";
10
- readonly toolsValue: "Authorized by DataOps on every request";
11
- readonly brandingTitle: "Apply Wanxiang branding";
12
- readonly brandingDescription: "Use the Wanxiang Data Platform identity in navigation, the New Session welcome view, browser title, and favicon.";
13
- readonly brandingToggle: "Apply Wanxiang branding";
14
- readonly modelsTitle: "Model configuration";
15
- readonly modelsDescription: "Share the models configured here with other users, and choose which of them they may use.";
16
- readonly modelsUnavailable: "Model configuration is unavailable until this workspace is connected to DataOps.";
5
+ readonly managed: "Connected to DataOps";
6
+ readonly unavailable: "DataOps is not connected.";
7
+ readonly loadFailed: "Loading failed. Try again.";
8
+ readonly brandingTitle: "Wanxiang branding";
9
+ readonly brandingToggle: "Wanxiang branding";
10
+ readonly limitsTitle: "File size limits";
11
+ readonly limitsFileReadLabel: "Largest file to read (MiB)";
12
+ readonly limitsUploadLabel: "Largest file to upload (MiB)";
13
+ readonly limitsInvalid: "Enter a size in MiB.";
14
+ readonly limitsSave: "Save";
15
+ readonly limitsSaving: "Saving…";
17
16
  readonly defaultsTitle: "Default configuration";
18
- readonly defaultsDescription: "Publish the configuration this workspace holds — models, permissions, and general settings — as the values other users start from. Their workspace takes that copy once and may then change anything, and a change they make is never overwritten.";
19
- readonly defaultsActionTitle: "Publish as the default";
20
- readonly defaultsNotAdmin: "Only an administrator may publish the default configuration.";
21
- readonly defaultsNeverPublished: "Not published yet.";
22
- readonly defaultsPublishedAt: "Last published:";
23
- readonly defaultsPublish: "Publish default configuration";
17
+ readonly defaultsPublish: "Publish";
24
18
  readonly defaultsPublishing: "Publishing…";
25
- readonly loadFailed: "The model configuration could not be loaded. Try again.";
19
+ readonly defaultsPublishedAt: "Last published";
20
+ readonly defaultsNeverPublished: "Not published yet";
21
+ readonly defaultsNotAdmin: "Administrators only";
26
22
  readonly plazaTitle: "Skill plaza";
27
23
  readonly plazaIntro: "Skills other users shared. Install one to use it in this workspace, or share a skill of your own.";
28
24
  readonly plazaSearch: "Search skills";
@@ -0,0 +1,36 @@
1
+ /** Same-origin workspace-limits calls from the DataOps settings surface. */
2
+ /** Route the plugin's Host half serves for workspace limits. */
3
+ export declare const WORKSPACE_LIMITS_PATH = "/integrations/dataops/workspace-limits";
4
+ /** The two byte ceilings, as the panel edits them. */
5
+ export interface WorkspaceLimits {
6
+ /** Largest file a sidebar download or agent read may carry. */
7
+ fileReadMaxBytes: number;
8
+ /** Largest file an upload may carry. */
9
+ uploadMaxBytes: number;
10
+ }
11
+ /** Limits as this workspace currently holds them. */
12
+ export interface WorkspaceLimitsStatus {
13
+ limits: WorkspaceLimits;
14
+ canWrite: boolean;
15
+ readOnlyReason: string | null;
16
+ }
17
+ /**
18
+ * A write DataOps refused, carrying the reason it gave.
19
+ *
20
+ * The message is written for the person who set the value, so the panel shows it as-is rather
21
+ * than replacing it with a generic failure notice.
22
+ */
23
+ export declare class WorkspaceLimitsRejection extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Read the workspace limits the panel shows.
28
+ * @returns The status, or null when the workspace is not connected to DataOps.
29
+ */
30
+ export declare function fetchWorkspaceLimits(): Promise<WorkspaceLimitsStatus | null>;
31
+ /**
32
+ * Store new workspace limits.
33
+ * @param limits - The ceilings to store.
34
+ * @returns The stored status.
35
+ */
36
+ export declare function saveWorkspaceLimits(limits: WorkspaceLimits): Promise<WorkspaceLimitsStatus | null>;
@@ -4,6 +4,7 @@ import z from '@deepseek-ai/schemastery';
4
4
  import { ModelSync } from './model-sync.ts';
5
5
  import { SettingsSync, type SectionSyncState } from './settings-sync.ts';
6
6
  import { SkillPlaza } from './skill-plaza.ts';
7
+ import { currentWorkspaceLimits } from './workspace-limits.ts';
7
8
  /** Cordis plugin name for the DataOps-managed integration. */
8
9
  export declare const name = "dataops-managed";
9
10
  /** Host services required by managed JWT and MCP ownership. */
@@ -15,12 +16,27 @@ export declare const MANAGED_PLUGIN_ID = "dataops-managed";
15
16
  export declare const MODEL_SYNC_PATH = "/integrations/dataops/model-sync";
16
17
  /** Same-origin route the skill center uses to read and change plaza sharing. */
17
18
  export declare const SKILL_PLAZA_PATH = "/integrations/dataops/skill-plaza";
19
+ /** Same-origin route the Settings panel uses to read and change this workspace's limits. */
20
+ export declare const WORKSPACE_LIMITS_PATH = "/integrations/dataops/workspace-limits";
18
21
  /** The live skill plaza, once this plugin activated. */
19
22
  export declare function currentSkillPlaza(): SkillPlaza | undefined;
20
23
  /** The live model-sync instance, once this plugin activated. */
21
24
  export declare function currentModelSync(): ModelSync | undefined;
25
+ export { currentWorkspaceLimits };
22
26
  /** @returns The live instance, or undefined before this plugin activated. */
23
27
  export declare function currentSettingsSync(): SettingsSync | undefined;
28
+ /**
29
+ * A volatile config reference.
30
+ *
31
+ * A field marked volatile is not stored with the plugin's own configuration: the settings
32
+ * service keeps it in this workspace's document and hands the plugin a reference that it
33
+ * updates in place, so a value written through the Settings panel is visible without a
34
+ * restart. Reading the field directly therefore reads the reference, not the value.
35
+ */
36
+ interface VolatileRef<T> {
37
+ /** The current value behind the reference. */
38
+ get(): T;
39
+ }
24
40
  /** Configuration for the DataOps-managed MCP connection. */
25
41
  export interface Config {
26
42
  /** DataOps browser/API origin reachable from the DSH Host. */
@@ -29,23 +45,32 @@ export interface Config {
29
45
  serverName: string;
30
46
  /** DSH credential reference that stores the current DataOps access JWT. */
31
47
  credentialRef: string;
32
- /** Per-tool-call timeout forwarded to the MCP client. */
33
- toolCallTimeoutMs: number;
48
+ /**
49
+ * Per-tool-call timeout forwarded to the MCP client.
50
+ *
51
+ * Volatile, so the settings service stores the platform default in this workspace's document
52
+ * and the panel can raise it without a profile edit. The read goes through `volatileValue`.
53
+ */
54
+ toolCallTimeoutMs: number | VolatileRef<number>;
34
55
  /**
35
56
  * Model-sync state this workspace keeps.
36
57
  *
37
58
  * Volatile, so the settings service stores it in this workspace's own document. It belongs
38
59
  * to the workspace rather than to DataOps because it records whether the user here wrote
39
60
  * their own models, which is a fact about this workspace.
61
+ *
62
+ * Volatile, so the runtime supplies a reference and the read goes through `volatileValue`.
40
63
  */
41
- modelSync: ModelSyncState;
64
+ modelSync: ModelSyncState | VolatileRef<ModelSyncState>;
42
65
  /**
43
66
  * Settings-distribution state this workspace keeps.
44
67
  *
45
68
  * Volatile, like the model-sync state beside it, and for the same reason: it records whether
46
69
  * the user here wrote their own values, which is a fact about this workspace.
70
+ *
71
+ * Volatile, like the field above, so the read goes through `volatileValue`.
47
72
  */
48
- settingsSync: SectionSyncState;
73
+ settingsSync: SectionSyncState | VolatileRef<SectionSyncState>;
49
74
  /**
50
75
  * Settings namespaces this deployment distributes to other workspaces.
51
76
  *
@@ -63,6 +88,14 @@ export interface ModelSyncState {
63
88
  /** Model ids the administrator keeps private; every other model is offered by default. */
64
89
  privateModelIds: string[];
65
90
  }
91
+ /**
92
+ * Per-tool-call timeout a workspace starts with.
93
+ *
94
+ * Named rather than inlined so the schema default and the read fallback cannot drift: the field is
95
+ * volatile, so a workspace that never changed it reads no value at all and both places have to
96
+ * answer the same number.
97
+ */
98
+ export declare const DEFAULT_TOOL_CALL_TIMEOUT_MS = 300000;
66
99
  /** Schemastery parser for managed DataOps configuration. */
67
100
  export declare const Config: z<Config>;
68
101
  /**