@chengxianglibra/dsh-data-analysis 0.1.2 → 0.2.1

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.
Files changed (222) hide show
  1. package/README.en.md +135 -0
  2. package/README.md +105 -98
  3. package/cordis.patch.yml +1 -1
  4. package/lib/bin/environment.js +8 -6
  5. package/lib/client/credentials/changes.js +74 -0
  6. package/lib/client/credentials/create-datasource.js +126 -22
  7. package/lib/client/credentials/credential-draft.js +42 -0
  8. package/lib/client/credentials/defaults.js +11 -0
  9. package/lib/client/credentials/descriptions.js +46 -0
  10. package/lib/client/credentials/install.js +209 -81
  11. package/lib/client/credentials/model.js +323 -51
  12. package/lib/client/credentials/report-publishing.js +96 -0
  13. package/lib/client/credentials/styles.js +13 -9
  14. package/lib/client/i18n/context.js +22 -0
  15. package/lib/client/i18n/copy.js +60 -0
  16. package/lib/client/i18n/credentials.js +475 -0
  17. package/lib/client/i18n/host.js +28 -0
  18. package/lib/client/i18n/navigation.js +41 -0
  19. package/lib/client/i18n/presentation.js +577 -0
  20. package/lib/client/i18n/python.js +36 -0
  21. package/lib/client/i18n/semantic.js +441 -0
  22. package/lib/client/i18n/settings.js +30 -0
  23. package/lib/client/input-context.js +21 -0
  24. package/lib/client/python-tool/card.js +35 -0
  25. package/lib/client/python-tool/execution-error.js +35 -0
  26. package/lib/client/python-tool/install.js +9 -0
  27. package/lib/client/python-tool/model.js +101 -0
  28. package/lib/client/right-tabs/install.js +535 -0
  29. package/lib/client/right-tabs/live-delivery.js +58 -0
  30. package/lib/client/right-tabs/navigation.js +47 -0
  31. package/lib/client/right-tabs/page.js +178 -0
  32. package/lib/client/right-tabs/styles.js +11 -0
  33. package/lib/client/rpc.js +5 -0
  34. package/lib/client/semantic-reference.js +10 -0
  35. package/lib/client/settings/card.js +90 -0
  36. package/lib/client/settings/install.js +10 -0
  37. package/lib/client/workspace-header-action.js +14 -1
  38. package/lib/client/workspace-shortcuts.js +26 -0
  39. package/lib/client.js +388 -242
  40. package/lib/compatibility.js +1 -1
  41. package/lib/datasource/authoring.js +43 -4
  42. package/lib/datasource/bridge-programs.js +23 -20
  43. package/lib/datasource/bridge.js +46 -3
  44. package/lib/datasource/changes-contract.js +41 -0
  45. package/lib/datasource/changes.js +49 -0
  46. package/lib/datasource/configure.js +39 -0
  47. package/lib/datasource/defaults.js +25 -0
  48. package/lib/datasource/index.js +2 -1
  49. package/lib/datasource/python-options.js +15 -0
  50. package/lib/datasource/python.js +119 -25
  51. package/lib/datasource/rpc.js +75 -13
  52. package/lib/datasource/service.js +286 -11
  53. package/lib/datasource/test.js +3 -2
  54. package/lib/disclosure/activation.js +19 -7
  55. package/lib/disclosure/execution-guidance.js +49 -0
  56. package/lib/disclosure/help.js +4 -1
  57. package/lib/environment/runtime.js +35 -34
  58. package/lib/environment/workspace.js +18 -1
  59. package/lib/lifecycle.js +33 -0
  60. package/lib/plugin-agents.js +154 -0
  61. package/lib/plugin.js +98 -172
  62. package/lib/presentation/asset.js +34 -0
  63. package/lib/presentation/assets/portable.js +14 -14
  64. package/lib/presentation/assets/static.js +344 -73
  65. package/lib/presentation/build/index.js +3 -2
  66. package/lib/presentation/commit.js +19 -12
  67. package/lib/presentation/contracts/asset-path.js +9 -0
  68. package/lib/presentation/contracts/catalog.js +62 -0
  69. package/lib/presentation/contracts/charts.js +0 -23
  70. package/lib/presentation/contracts/index.js +17 -5
  71. package/lib/presentation/contracts/types.js +1 -1
  72. package/lib/presentation/delivery.js +45 -30
  73. package/lib/presentation/export-html.js +214 -0
  74. package/lib/presentation/files.js +4 -5
  75. package/lib/presentation/projection/index.js +2 -1
  76. package/lib/presentation/projection/program.js +2 -9
  77. package/lib/presentation/projection/sql-code.js +13 -13
  78. package/lib/presentation/reports.js +114 -17
  79. package/lib/presentation/rpc.js +96 -58
  80. package/lib/presentation/tool.js +6 -10
  81. package/lib/report-publishing/adapters.js +111 -0
  82. package/lib/report-publishing/config.js +69 -0
  83. package/lib/report-publishing/service.js +180 -0
  84. package/lib/rpc.js +49 -0
  85. package/lib/semantic-browser/contracts.js +17 -0
  86. package/lib/semantic-browser/program.js +10 -0
  87. package/lib/semantic-browser/service.js +40 -6
  88. package/lib/semantic-reference/bridge.js +10 -1
  89. package/lib/semantic-reference/contracts.js +20 -6
  90. package/lib/semantic-reference/environment.js +56 -0
  91. package/lib/semantic-reference/labels.js +16 -0
  92. package/lib/semantic-reference/rpc.js +39 -18
  93. package/lib/semantic-reference/search.js +5 -4
  94. package/lib/semantic-reference/usage.js +12 -3
  95. package/lib/settings.js +21 -0
  96. package/lib/tool-lifecycle.js +22 -0
  97. package/lib/types/client/credentials/changes.d.ts +9 -0
  98. package/lib/types/client/credentials/create-datasource.d.ts +3 -1
  99. package/lib/types/client/credentials/credential-draft.d.ts +14 -0
  100. package/lib/types/client/credentials/defaults.d.ts +2 -0
  101. package/lib/types/client/credentials/descriptions.d.ts +1 -0
  102. package/lib/types/client/credentials/install.d.ts +19 -1
  103. package/lib/types/client/credentials/model.d.ts +31 -11
  104. package/lib/types/client/credentials/report-publishing.d.ts +5 -0
  105. package/lib/types/client/credentials/styles.d.ts +1 -1
  106. package/lib/types/client/i18n/context.d.ts +17 -0
  107. package/lib/types/client/i18n/copy.d.ts +821 -0
  108. package/lib/types/client/i18n/credentials.d.ts +475 -0
  109. package/lib/types/client/i18n/host.d.ts +17 -0
  110. package/lib/types/client/i18n/navigation.d.ts +41 -0
  111. package/lib/types/client/i18n/presentation.d.ts +577 -0
  112. package/lib/types/client/i18n/python.d.ts +19 -0
  113. package/lib/types/client/i18n/semantic.d.ts +441 -0
  114. package/lib/types/client/i18n/settings.d.ts +16 -0
  115. package/lib/types/client/input-context.d.ts +19 -0
  116. package/lib/types/client/presentation/ask-dsh.d.ts +4 -8
  117. package/lib/types/client/presentation/catalog-model.d.ts +24 -0
  118. package/lib/types/client/presentation/catalog.d.ts +20 -0
  119. package/lib/types/client/presentation/chart-tooltip.d.ts +2 -2
  120. package/lib/types/client/presentation/context-reference.d.ts +12 -0
  121. package/lib/types/client/presentation/copy-context.d.ts +2 -2
  122. package/lib/types/client/presentation/delivery-model.d.ts +27 -6
  123. package/lib/types/client/presentation/delivery.d.ts +8 -7
  124. package/lib/types/client/presentation/download.d.ts +2 -0
  125. package/lib/types/client/presentation/export-menu.d.ts +26 -0
  126. package/lib/types/client/presentation/export-view.d.ts +63 -0
  127. package/lib/types/client/presentation/host-entry.d.ts +8 -2
  128. package/lib/types/client/presentation/icons.d.ts +1 -1
  129. package/lib/types/client/presentation/model.d.ts +14 -15
  130. package/lib/types/client/presentation/reader.d.ts +15 -2
  131. package/lib/types/client/presentation/reference-source.d.ts +9 -0
  132. package/lib/types/client/presentation/source-code-model.d.ts +2 -1
  133. package/lib/types/client/presentation/source-code.d.ts +0 -5
  134. package/lib/types/client/presentation/source-dialog.d.ts +1 -2
  135. package/lib/types/client/presentation/sources.d.ts +5 -5
  136. package/lib/types/client/presentation/styles.d.ts +1 -1
  137. package/lib/types/client/presentation/table.d.ts +5 -1
  138. package/lib/types/client/python-tool/card.d.ts +2 -0
  139. package/lib/types/client/python-tool/execution-error.d.ts +3 -0
  140. package/lib/types/client/python-tool/install.d.ts +2 -0
  141. package/lib/types/client/python-tool/model.d.ts +20 -0
  142. package/lib/types/client/right-tabs/install.d.ts +18 -0
  143. package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
  144. package/lib/types/client/right-tabs/navigation.d.ts +17 -0
  145. package/lib/types/client/right-tabs/page.d.ts +36 -0
  146. package/lib/types/client/right-tabs/styles.d.ts +2 -0
  147. package/lib/types/client/rpc.d.ts +3 -0
  148. package/lib/types/client/semantic-reference.d.ts +4 -0
  149. package/lib/types/client/settings/card.d.ts +9 -0
  150. package/lib/types/client/settings/install.d.ts +2 -0
  151. package/lib/types/client/workspace-header-action.d.ts +1 -1
  152. package/lib/types/client/workspace-shortcuts.d.ts +15 -0
  153. package/lib/types/client.d.ts +5 -2
  154. package/lib/types/compatibility.d.ts +1 -1
  155. package/lib/types/datasource/authoring.d.ts +10 -0
  156. package/lib/types/datasource/bridge.d.ts +23 -2
  157. package/lib/types/datasource/changes-contract.d.ts +18 -0
  158. package/lib/types/datasource/changes.d.ts +15 -0
  159. package/lib/types/datasource/configure.d.ts +7 -0
  160. package/lib/types/datasource/defaults.d.ts +7 -0
  161. package/lib/types/datasource/index.d.ts +6 -1
  162. package/lib/types/datasource/python-options.d.ts +8 -0
  163. package/lib/types/datasource/python.d.ts +17 -2
  164. package/lib/types/datasource/rpc.d.ts +2 -1
  165. package/lib/types/datasource/service.d.ts +48 -5
  166. package/lib/types/datasource/test.d.ts +1 -1
  167. package/lib/types/disclosure/activation.d.ts +2 -1
  168. package/lib/types/disclosure/execution-guidance.d.ts +6 -0
  169. package/lib/types/disclosure/help.d.ts +1 -1
  170. package/lib/types/environment/types.d.ts +2 -1
  171. package/lib/types/environment/workspace.d.ts +1 -0
  172. package/lib/types/lifecycle.d.ts +8 -0
  173. package/lib/types/plugin-agents.d.ts +29 -0
  174. package/lib/types/plugin.d.ts +12 -28
  175. package/lib/types/presentation/asset.d.ts +6 -0
  176. package/lib/types/presentation/build/index.d.ts +7 -2
  177. package/lib/types/presentation/commit.d.ts +1 -1
  178. package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
  179. package/lib/types/presentation/contracts/catalog.d.ts +27 -0
  180. package/lib/types/presentation/contracts/charts.d.ts +0 -2
  181. package/lib/types/presentation/contracts/types.d.ts +7 -4
  182. package/lib/types/presentation/delivery.d.ts +1 -1
  183. package/lib/types/presentation/export-html.d.ts +13 -0
  184. package/lib/types/presentation/reports.d.ts +6 -1
  185. package/lib/types/presentation/rpc.d.ts +3 -2
  186. package/lib/types/presentation/tool.d.ts +2 -2
  187. package/lib/types/report-publishing/adapters.d.ts +6 -0
  188. package/lib/types/report-publishing/config.d.ts +22 -0
  189. package/lib/types/report-publishing/service.d.ts +68 -0
  190. package/lib/types/rpc.d.ts +3 -0
  191. package/lib/types/semantic-browser/contracts.d.ts +7 -0
  192. package/lib/types/semantic-browser/service.d.ts +7 -1
  193. package/lib/types/semantic-reference/bridge.d.ts +1 -0
  194. package/lib/types/semantic-reference/contracts.d.ts +5 -2
  195. package/lib/types/semantic-reference/environment.d.ts +22 -0
  196. package/lib/types/semantic-reference/labels.d.ts +16 -0
  197. package/lib/types/semantic-reference/rpc.d.ts +1 -1
  198. package/lib/types/semantic-reference/search.d.ts +1 -1
  199. package/lib/types/settings.d.ts +14 -0
  200. package/lib/types/tool-lifecycle.d.ts +4 -0
  201. package/lib/types/workspace-identity.d.ts +6 -0
  202. package/lib/workspace-identity.js +9 -0
  203. package/package.json +93 -61
  204. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  205. package/skills/dsh-data-analysis-files/SKILL.md +44 -0
  206. package/skills/dsh-data-analysis-files/references/delivery.md +15 -0
  207. package/skills/dsh-data-analysis-files/references/examples.md +93 -0
  208. package/skills/dsh-data-analysis-presentation/SKILL.md +15 -15
  209. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +13 -4
  210. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +3 -2
  211. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +3 -2
  212. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +12 -5
  213. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +3 -2
  214. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +3 -2
  215. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +12 -4
  216. package/skills/dsh-data-analysis-presentation/references/examples.md +3 -1
  217. package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -0
  218. package/skills/dsh-data-analysis-presentation/references/interaction.md +1 -1
  219. package/skills/dsh-data-analysis-presentation/references/narrative.md +1 -1
  220. package/skills/dsh-data-analysis-presentation/references/publishing.md +3 -0
  221. package/skills/dsh-data-analysis-presentation/references/schema.md +29 -2
  222. package/lib/types/client/presentation/install.d.ts +0 -15
@@ -1,19 +1,34 @@
1
1
  import { z } from 'zod';
2
+ import { registerPluginRpc } from "../rpc.js";
3
+ import { withDatasourceDefaults } from "./defaults.js";
2
4
  import { CREDENTIAL_CHANNEL, credentialError } from "./service.js";
3
5
  const text = z.string().min(1).max(256);
4
6
  const handle = { generation: z.string().uuid(), id: z.string().uuid(), scope: z.string().uuid() };
5
7
  const mutation = z
6
8
  .object({
7
9
  ...handle,
8
- action: z.enum(['save', 'update', 'delete', 'test', 'submit', 'diagnose']),
10
+ action: z.enum(['save', 'update', 'delete', 'delete-datasource', 'test', 'submit', 'diagnose']),
11
+ deleteCredentials: z.boolean().optional(),
9
12
  version: z.string().max(65536),
10
13
  requestId: z.string().uuid().optional(),
11
14
  reference: text.optional(),
12
15
  changes: z.record(text, z.string().min(1).max(65536)).optional(),
13
16
  })
14
17
  .strict();
15
- export function registerCredentialRpc(connection, service, workspace) {
16
- const unregister = connection.rpc.handle(CREDENTIAL_CHANNEL, async (endpoint, payload, signal) => {
18
+ export function registerCredentialRpc(connection, service, workspace, datasourceDefaults) {
19
+ const unregister = registerPluginRpc(connection, CREDENTIAL_CHANNEL, [
20
+ 'overview',
21
+ 'authoring',
22
+ 'create-datasource',
23
+ 'configuration',
24
+ 'update-datasource',
25
+ 'select-configuration',
26
+ 'watch',
27
+ 'start',
28
+ 'operation',
29
+ 'cancel-operation',
30
+ 'cancel-request',
31
+ ], async (endpoint, payload, signal) => {
17
32
  try {
18
33
  if (Buffer.byteLength(JSON.stringify(payload) ?? '') > 1_048_576)
19
34
  throw new Error('invalid-request');
@@ -26,24 +41,74 @@ export function registerCredentialRpc(connection, service, workspace) {
26
41
  };
27
42
  }
28
43
  else if (endpoint === 'authoring') {
29
- const input = z.object({ workspaceId: text }).strict().parse(payload);
44
+ const input = z
45
+ .object({ workspaceId: text, mode: z.enum(['create', 'edit']).default('create') })
46
+ .strict()
47
+ .parse(payload);
30
48
  const bridge = await workspace(input.workspaceId);
31
49
  if (!bridge.authoring)
32
50
  throw new Error('authoring-unavailable');
33
- value = { generation: service.generation, ...(await bridge.authoring(signal)) };
51
+ const schema = await bridge.authoring(signal);
52
+ value = {
53
+ generation: service.generation,
54
+ ...(input.mode === 'edit'
55
+ ? schema
56
+ : withDatasourceDefaults(schema, datasourceDefaults)),
57
+ };
34
58
  }
35
- else if (endpoint === 'create-datasource') {
59
+ else if (endpoint === 'configuration') {
60
+ const input = z.object({ workspaceId: text, name: text }).strict().parse(payload);
61
+ const bridge = await workspace(input.workspaceId);
62
+ if (!bridge.configuration)
63
+ throw new Error('authoring-unavailable');
64
+ value = await bridge.configuration(input.name, signal);
65
+ }
66
+ else if (endpoint === 'select-configuration') {
67
+ const input = z
68
+ .object({ workspaceId: text, requestId: z.string().uuid(), name: text })
69
+ .strict()
70
+ .parse(payload);
71
+ value = await service.selectConfiguration(input.requestId, input.workspaceId, input.name, () => workspace(input.workspaceId), signal);
72
+ }
73
+ else if (endpoint === 'create-datasource' || endpoint === 'update-datasource') {
36
74
  const input = z
37
75
  .object({
38
76
  workspaceId: text,
39
77
  generation: z.string().uuid(),
40
78
  fingerprint: z.string().min(1).max(256),
41
79
  backend: text,
80
+ name: text.optional(),
81
+ version: text.optional(),
82
+ requestId: z.string().uuid().optional(),
42
83
  fields: z.record(text, z.unknown()),
43
84
  })
44
85
  .strict()
45
86
  .parse(payload);
46
- value = await service.createDatasource(input.generation, input.fingerprint, input, () => workspace(input.workspaceId), signal);
87
+ const request = input.requestId
88
+ ? await service.configurationRequest(input.requestId, input.workspaceId, () => workspace(input.workspaceId), signal)
89
+ : undefined;
90
+ const operationSignal = request ? AbortSignal.any([signal, request.signal]) : signal;
91
+ const resolve = async () => {
92
+ if (input.requestId)
93
+ await service.configurationRequest(input.requestId, input.workspaceId, () => workspace(input.workspaceId), operationSignal);
94
+ return workspace(input.workspaceId);
95
+ };
96
+ if (endpoint === 'update-datasource') {
97
+ const { name, version } = z.object({ name: text, version: text }).parse(input);
98
+ if (request?.view.configuration.mode === 'edit' &&
99
+ request.view.configuration.name !== name)
100
+ throw new Error('invalid-request');
101
+ value = await service.updateDatasource(input.generation, input.fingerprint, { backend: input.backend, fields: input.fields, name, version }, resolve, operationSignal);
102
+ }
103
+ else {
104
+ if (input.name || input.version || request?.view.configuration.mode === 'edit')
105
+ throw new Error('invalid-request');
106
+ value = await service.createDatasource(input.generation, input.fingerprint, { backend: input.backend, fields: input.fields }, resolve, operationSignal);
107
+ }
108
+ if (input.requestId) {
109
+ const configured = await service.selectConfiguration(input.requestId, input.workspaceId, value.name, () => workspace(input.workspaceId), operationSignal);
110
+ value = { ...value, request: configured };
111
+ }
47
112
  }
48
113
  else if (endpoint === 'watch') {
49
114
  const input = z
@@ -79,10 +144,7 @@ export function registerCredentialRpc(connection, service, workspace) {
79
144
  error: { code: 'internal', message: credentialError(error), details: {} },
80
145
  };
81
146
  }
82
- }, { authority: 'trusted-host' });
83
- return async () => {
84
- const draining = unregister();
85
- await service.close();
86
- await draining;
87
- };
147
+ });
148
+ // The profile owns this shared service; withdrawing its Web entry must not close it.
149
+ return unregister;
88
150
  }
@@ -1,9 +1,18 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
3
+ import { finishCleanup } from "../lifecycle.js";
3
4
  import { marivoCredentialStorageRef } from "./shell-env.js";
4
5
  export const CREDENTIAL_CHANNEL = '/dsh-data-analysis-credentials';
5
6
  const RETENTION = 30 * 60_000;
6
7
  const CAPACITY = 512;
8
+ function sessionIdentity(exec) {
9
+ const session = exec.agent.session;
10
+ return JSON.stringify([
11
+ session.id,
12
+ Reflect.get(session.header, 'workspaceId'),
13
+ session.header.cwd,
14
+ ]);
15
+ }
7
16
  export class CredentialServiceError extends Error {
8
17
  code;
9
18
  constructor(code) {
@@ -24,9 +33,12 @@ export class MarivoCredentialService {
24
33
  generation = randomUUID();
25
34
  #store;
26
35
  #contexts = new Map();
36
+ #activeContexts = new Set();
37
+ #configurations = new Map();
27
38
  #requests = new Map();
28
39
  #operations = new Map();
29
40
  #controllers = new Map();
41
+ #closing;
30
42
  #tasks = new Set();
31
43
  #versions = new Map();
32
44
  #activeRefs = new Map();
@@ -68,6 +80,9 @@ export class MarivoCredentialService {
68
80
  for (const [id, op] of this.#operations)
69
81
  if (op.endedAt !== undefined && now - op.endedAt >= RETENTION)
70
82
  this.#operations.delete(id);
83
+ for (const [id, request] of this.#configurations)
84
+ if (request.view.endedAt !== undefined && now - request.view.endedAt >= RETENTION)
85
+ this.#configurations.delete(id);
71
86
  for (const [id, request] of this.#requests)
72
87
  if (request.view.endedAt !== undefined && now - request.view.endedAt >= RETENTION)
73
88
  this.#requests.delete(id);
@@ -128,11 +143,13 @@ export class MarivoCredentialService {
128
143
  this.#changed();
129
144
  }
130
145
  async #current(context, signal) {
146
+ assert(!context.invalidated, 'context-changed');
131
147
  signal.throwIfAborted();
132
148
  const bridge = await context.resolve();
133
149
  assert(bridge.binding.fingerprint === context.bridge.binding.fingerprint, 'context-changed');
134
150
  const described = await bridge.describe(context.description.name, signal);
135
151
  assert(described.definition === context.description.definition, 'context-changed');
152
+ assert(!context.invalidated, 'context-changed');
136
153
  signal.throwIfAborted();
137
154
  context.touched = this.now();
138
155
  }
@@ -146,7 +163,12 @@ export class MarivoCredentialService {
146
163
  throw new CredentialServiceError('credential-state-unavailable');
147
164
  }
148
165
  }
149
- const history = this.#history.get(this.#historyKey(context));
166
+ const historyKey = this.#historyKey(context);
167
+ const previous = [...this.#history.entries()].reverse().find(([key]) => {
168
+ const [fingerprint, name] = JSON.parse(key);
169
+ return fingerprint === context.bridge.binding.fingerprint && name === context.description.name;
170
+ });
171
+ const history = this.#history.get(historyKey) ?? previous?.[1];
150
172
  return {
151
173
  token: context.token,
152
174
  workspaceId: context.workspaceId,
@@ -162,7 +184,7 @@ export class MarivoCredentialService {
162
184
  lastTest: {
163
185
  at: history.at,
164
186
  result: history.result,
165
- stale: history.version !== this.#version(context),
187
+ stale: !this.#history.has(historyKey) || history.version !== this.#version(context),
166
188
  },
167
189
  }
168
190
  : {}),
@@ -207,7 +229,8 @@ export class MarivoCredentialService {
207
229
  resolve,
208
230
  touched: this.now(),
209
231
  };
210
- const old = [...this.#contexts.values()].find((c) => c.workspaceId === workspaceId &&
232
+ const old = [...this.#contexts.values()].find((c) => !c.invalidated &&
233
+ c.workspaceId === workspaceId &&
211
234
  c.description.name === context.description.name &&
212
235
  c.description.definition === context.description.definition &&
213
236
  c.bridge.binding.fingerprint === context.bridge.binding.fingerprint);
@@ -234,9 +257,147 @@ export class MarivoCredentialService {
234
257
  signal.throwIfAborted();
235
258
  const result = await bridge.create(input, signal);
236
259
  assert(!result.error && result.name, result.error ?? 'datasource-definition-invalid');
260
+ this.#changed();
237
261
  return { name: result.name };
238
262
  }, signal);
239
263
  }
264
+ async updateDatasource(generation, fingerprint, input, resolve, caller) {
265
+ const signal = AbortSignal.any([caller, this.#lifetime.signal]);
266
+ return this.#locked(async () => {
267
+ assert(generation === this.generation, 'context-changed');
268
+ const bridge = await resolve();
269
+ assert(bridge.binding.fingerprint === fingerprint, 'context-changed');
270
+ assert(bridge.update, 'datasource-authoring-unavailable');
271
+ assert(![...this.#operations.values()].some((op) => op.status === 'running' &&
272
+ this.#contexts.get(op.scope)?.bridge.binding.fingerprint === fingerprint &&
273
+ this.#contexts.get(op.scope)?.description.name === input.name), 'operation-busy');
274
+ const result = await bridge.update(input, signal);
275
+ assert(!result.error && result.name, result.error ?? 'datasource-definition-invalid');
276
+ this.#changed();
277
+ return { name: result.name };
278
+ }, signal);
279
+ }
280
+ async configure(exec, resolve, input) {
281
+ assert(exec.agent, 'agent-required');
282
+ assert((input.mode === 'create' && input.name === undefined) ||
283
+ (input.mode === 'edit' &&
284
+ typeof input.name === 'string' &&
285
+ !!input.name.trim() &&
286
+ input.name.length <= 256), 'invalid-request');
287
+ assert(typeof input.reason === 'string' && !!input.reason.trim() && input.reason.length <= 1000, 'invalid-request');
288
+ const agent = exec.agent;
289
+ if (this.interaction === 'none' ||
290
+ agent.session.header.origin === 'subagent' ||
291
+ (agent.session.header.delegationDepth ?? 0) > 0)
292
+ return { status: 'needs-configuration', ...(input.name ? { name: input.name } : {}) };
293
+ const controller = new AbortController();
294
+ const signal = AbortSignal.any([this.executionSignal(exec.signal, agent), controller.signal]);
295
+ const identity = sessionIdentity(exec);
296
+ const bridge = await resolve();
297
+ signal.throwIfAborted();
298
+ assert(identity === sessionIdentity(exec), 'context-changed');
299
+ this.#prune();
300
+ assert(this.#configurations.size < CAPACITY / 2, 'capacity-exceeded');
301
+ return new Promise((finish) => {
302
+ const id = randomUUID();
303
+ const request = {
304
+ exec,
305
+ settled: false,
306
+ controller,
307
+ resolve,
308
+ fingerprint: bridge.binding.fingerprint,
309
+ sessionIdentity: identity,
310
+ signal,
311
+ finish,
312
+ view: {
313
+ id,
314
+ sessionId: agent.session.id,
315
+ workspaceId: String(Reflect.get(agent.session.header, 'workspaceId') ?? ''),
316
+ configuration: { ...input },
317
+ status: 'awaiting-input',
318
+ },
319
+ stop: () => signal.removeEventListener('abort', abort),
320
+ };
321
+ const abort = () => this.#endConfiguration(request, { status: 'call-ended' });
322
+ this.#configurations.set(id, request);
323
+ signal.addEventListener('abort', abort, { once: true });
324
+ if (signal.aborted)
325
+ abort();
326
+ this.#changed();
327
+ });
328
+ }
329
+ #endConfiguration(request, result) {
330
+ if (request.settled)
331
+ return;
332
+ request.settled = true;
333
+ request.view.endedAt = this.now();
334
+ request.view.status =
335
+ result.status === 'ok'
336
+ ? 'succeeded'
337
+ : result.status === 'failed'
338
+ ? 'handed-off'
339
+ : result.status === 'context-changed'
340
+ ? 'context-changed'
341
+ : result.status === 'cancelled'
342
+ ? 'cancelled'
343
+ : 'call-ended';
344
+ request.stop();
345
+ request.finish(result);
346
+ this.#changed();
347
+ }
348
+ async #configurationCurrent(request, signal) {
349
+ assert(request.view.endedAt === undefined && !request.signal.aborted, 'call-ended');
350
+ signal.throwIfAborted();
351
+ const bridge = await request.resolve();
352
+ assert(bridge.binding.fingerprint === request.fingerprint &&
353
+ sessionIdentity(request.exec) === request.sessionIdentity, 'context-changed');
354
+ return bridge;
355
+ }
356
+ async configurationRequest(id, workspaceId, resolve, caller) {
357
+ const request = this.#configurations.get(id);
358
+ assert(request, 'call-ended');
359
+ const bridge = await this.#configurationCurrent(request, caller);
360
+ assert((!request.view.workspaceId || request.view.workspaceId === workspaceId) &&
361
+ (await resolve()).binding.fingerprint === bridge.binding.fingerprint, 'context-changed');
362
+ assert(request.view.status !== 'executing', 'operation-busy');
363
+ return request;
364
+ }
365
+ async selectConfiguration(id, workspaceId, name, resolve, caller) {
366
+ const request = await this.configurationRequest(id, workspaceId, resolve, caller);
367
+ assert(request.view.configuration.mode !== 'edit' || request.view.configuration.name === name, 'datasource-identity-fixed');
368
+ const signal = AbortSignal.any([caller, request.signal]);
369
+ const context = await this.#bind(workspaceId, request.resolve, name, signal);
370
+ const view = await this.#view(context);
371
+ await this.#configurationCurrent(request, signal);
372
+ assert(request.view.status !== 'executing', 'operation-busy');
373
+ assert(this.#contexts.size < CAPACITY, 'capacity-exceeded');
374
+ this.#contexts.set(context.token, context);
375
+ request.view.context = view;
376
+ request.view.status = 'awaiting-input';
377
+ delete request.view.failure;
378
+ // Once a datasource is chosen, use the existing credential/test operation lifecycle.
379
+ const attached = {
380
+ view: request.view,
381
+ context,
382
+ exec: request.exec,
383
+ signal: request.signal,
384
+ stop: () => { },
385
+ finish: ({ result }) => {
386
+ this.#endConfiguration(request, {
387
+ status: result.ok ? 'ok' : 'failed',
388
+ name: result.name,
389
+ latency_ms: result.latency_ms,
390
+ ...(!result.ok ? { failure: result.failure, repair: result.repair } : {}),
391
+ });
392
+ },
393
+ reject: () => {
394
+ this.#endConfiguration(request, { status: 'context-changed' });
395
+ },
396
+ };
397
+ this.#requests.set(id, attached);
398
+ this.#changed();
399
+ return structuredClone(request.view);
400
+ }
240
401
  #watchRefs(refs) {
241
402
  const unique = new Set(refs);
242
403
  for (const ref of unique)
@@ -359,6 +520,7 @@ export class MarivoCredentialService {
359
520
  assert(exec.agent, 'agent-required');
360
521
  const signal = this.executionSignal(exec.signal, exec.agent);
361
522
  const context = await this.#bind('', resolve, name, signal);
523
+ this.#activeContexts.add(context);
362
524
  const stop = this.#watchRefs(context.description.refs);
363
525
  try {
364
526
  const tested = await this.#awaitCredentials(exec, context, await this.#view(context), signal);
@@ -370,6 +532,7 @@ export class MarivoCredentialService {
370
532
  }
371
533
  finally {
372
534
  stop();
535
+ this.#activeContexts.delete(context);
373
536
  }
374
537
  }
375
538
  async prepareExecution(exec, resolve, names) {
@@ -394,6 +557,8 @@ export class MarivoCredentialService {
394
557
  delete values[ref];
395
558
  for (const stop of stops)
396
559
  stop();
560
+ for (const context of contexts)
561
+ this.#activeContexts.delete(context);
397
562
  };
398
563
  try {
399
564
  for (const name of names) {
@@ -401,6 +566,7 @@ export class MarivoCredentialService {
401
566
  assert(!contexts.length ||
402
567
  context.bridge.binding.fingerprint === contexts[0].bridge.binding.fingerprint, 'context-changed');
403
568
  contexts.push(context);
569
+ this.#activeContexts.add(context);
404
570
  stops.push(this.#watchRefs(context.description.refs));
405
571
  const view = await this.#view(context);
406
572
  if (view.refs.every((ref) => view.credentials[ref]?.configured))
@@ -425,8 +591,10 @@ export class MarivoCredentialService {
425
591
  assert(!released, 'execution-ended');
426
592
  signal.throwIfAborted();
427
593
  assert(exec.agent?.session === session && currentWorkspace() === workspace, 'context-changed');
428
- for (const context of contexts)
594
+ for (const context of contexts) {
595
+ assert(!context.invalidated, 'context-changed');
429
596
  assert(versions.get(context) === this.#version(context), 'credentials-changed');
597
+ }
430
598
  };
431
599
  assertCurrent();
432
600
  values = await this.#snapshot(contexts, signal);
@@ -459,9 +627,12 @@ export class MarivoCredentialService {
459
627
  return {
460
628
  generation: this.generation,
461
629
  cursor: `${this.generation}:${this.#revision}`,
462
- requests: [...this.#requests.values()]
630
+ configurationRequests: [...this.#configurations.values()]
463
631
  .filter((r) => r.view.sessionId === sessionId)
464
632
  .map((r) => structuredClone(r.view)),
633
+ requests: [...this.#requests.values()]
634
+ .filter((r) => r.view.sessionId === sessionId && !this.#configurations.has(r.view.id))
635
+ .map((r) => structuredClone(r.view)),
465
636
  };
466
637
  }
467
638
  async waitWatch(sessionId, cursor, signal) {
@@ -483,6 +654,17 @@ export class MarivoCredentialService {
483
654
  });
484
655
  }
485
656
  signal.throwIfAborted();
657
+ for (const request of this.#configurations.values()) {
658
+ if (request.view.sessionId !== sessionId || request.view.endedAt !== undefined)
659
+ continue;
660
+ try {
661
+ await this.#configurationCurrent(request, signal);
662
+ }
663
+ catch {
664
+ if (!signal.aborted)
665
+ this.#endConfiguration(request, { status: 'context-changed' });
666
+ }
667
+ }
486
668
  for (const request of this.#requests.values()) {
487
669
  if (request.view.sessionId !== sessionId || request.view.endedAt !== undefined)
488
670
  continue;
@@ -513,14 +695,20 @@ export class MarivoCredentialService {
513
695
  assert(this.#operations.size < CAPACITY, 'capacity-exceeded');
514
696
  const context = this.#contexts.get(input.scope);
515
697
  assert(context, 'context-changed');
698
+ assert(!context.invalidated, 'context-changed');
699
+ assert(input.action === 'delete-datasource' || input.deleteCredentials === undefined, 'invalid-action');
516
700
  const request = input.requestId ? this.#requests.get(input.requestId) : undefined;
517
701
  assert(!input.requestId ||
518
702
  (request &&
519
- request.context === context &&
703
+ request.context.token === context.token &&
520
704
  request.view.endedAt === undefined &&
521
705
  !request.signal.aborted), 'call-ended');
522
706
  assert((input.action === 'submit' || input.action === 'diagnose') === !!request, 'invalid-action');
523
- assert(![...this.#operations.values()].some((op) => op.scope === input.scope && op.status === 'running'), 'operation-busy');
707
+ assert(![...this.#operations.values()].some((op) => op.status === 'running' &&
708
+ (op.scope === input.scope ||
709
+ (this.#contexts.get(op.scope)?.bridge.binding.fingerprint ===
710
+ context.bridge.binding.fingerprint &&
711
+ this.#contexts.get(op.scope)?.description.name === context.description.name))), 'operation-busy');
524
712
  if (request)
525
713
  assert(request.view.status !== 'executing', 'operation-busy');
526
714
  assert(input.version === this.#version(context), 'credentials-changed');
@@ -543,6 +731,9 @@ export class MarivoCredentialService {
543
731
  phase: 'saving',
544
732
  saved: [],
545
733
  errors: [],
734
+ ...(input.action === 'delete-datasource'
735
+ ? { deleteCredentials: input.deleteCredentials === true }
736
+ : {}),
546
737
  };
547
738
  this.#operations.set(op.id, op);
548
739
  const controller = new AbortController();
@@ -565,7 +756,70 @@ export class MarivoCredentialService {
565
756
  async #run(context, request, op, changes, reference, version, signal) {
566
757
  try {
567
758
  await this.#current(context, signal);
568
- if (op.action === 'diagnose') {
759
+ const configuration = request && this.#configurations.get(request.view.id);
760
+ if (configuration)
761
+ await this.#configurationCurrent(configuration, signal);
762
+ if (op.action === 'delete-datasource') {
763
+ await this.#locked(async () => {
764
+ await this.#current(context, signal);
765
+ assert(version === this.#version(context), 'credentials-changed');
766
+ assert(context.bridge.remove, 'datasource-remove-unavailable');
767
+ op.phase = 'removing';
768
+ // Revoke pending uses even if the subprocess result is later lost.
769
+ for (const bound of new Set([
770
+ ...this.#contexts.values(),
771
+ ...this.#activeContexts,
772
+ ...[...this.#requests.values()].map((pending) => pending.context),
773
+ ])) {
774
+ if (bound.bridge.binding.fingerprint !== context.bridge.binding.fingerprint ||
775
+ bound.description.name !== context.description.name)
776
+ continue;
777
+ bound.invalidated = true;
778
+ }
779
+ for (const pending of this.#requests.values()) {
780
+ if (!pending.context.invalidated || pending.view.endedAt !== undefined)
781
+ continue;
782
+ pending.view.status = 'context-changed';
783
+ pending.view.endedAt = this.now();
784
+ pending.stop();
785
+ pending.reject(new CredentialServiceError('context-changed'));
786
+ }
787
+ for (const pending of this.#configurations.values()) {
788
+ if (pending.fingerprint === context.bridge.binding.fingerprint &&
789
+ (pending.view.context?.name === context.description.name ||
790
+ (pending.view.configuration.mode === 'edit' &&
791
+ pending.view.configuration.name === context.description.name)))
792
+ this.#endConfiguration(pending, { status: 'context-changed' });
793
+ }
794
+ this.#changed();
795
+ const result = await context.bridge.remove(context.description, signal);
796
+ assert(!result.error && result.name === context.description.name, result.error ?? 'datasource-remove-failed');
797
+ op.datasourceRemoved = true;
798
+ for (const key of this.#history.keys()) {
799
+ const [fingerprint, name] = JSON.parse(key);
800
+ if (fingerprint === context.bridge.binding.fingerprint &&
801
+ name === context.description.name)
802
+ this.#history.delete(key);
803
+ }
804
+ if (op.deleteCredentials) {
805
+ op.deletedCredentials = [];
806
+ op.credentialDeleteFailures = [];
807
+ for (const ref of new Set(context.description.refs)) {
808
+ signal.throwIfAborted();
809
+ this.invalidate([ref]);
810
+ try {
811
+ await this.#store.unset(credentialRef(marivoCredentialStorageRef(ref)));
812
+ op.deletedCredentials.push(ref);
813
+ }
814
+ catch {
815
+ op.credentialDeleteFailures.push(ref);
816
+ }
817
+ }
818
+ assert(op.credentialDeleteFailures.length === 0, 'credential-delete-failed');
819
+ }
820
+ }, signal);
821
+ }
822
+ else if (op.action === 'diagnose') {
569
823
  assert(request?.view.failure, 'no-test-failure');
570
824
  op.result = request.view.failure;
571
825
  request.view.status = 'handed-off';
@@ -606,6 +860,9 @@ export class MarivoCredentialService {
606
860
  if (request && request.view.endedAt === undefined) {
607
861
  request.view.context = await this.#view(context);
608
862
  signal.throwIfAborted();
863
+ await this.#current(context, signal);
864
+ if (configuration)
865
+ await this.#configurationCurrent(configuration, signal);
609
866
  assert(testedVersion === this.#version(context), 'credentials-changed');
610
867
  if (op.result.ok) {
611
868
  request.view.status = 'succeeded';
@@ -657,6 +914,16 @@ export class MarivoCredentialService {
657
914
  this.#controllers.get(id)?.abort();
658
915
  }
659
916
  cancelRequest(id) {
917
+ const configuration = this.#configurations.get(id);
918
+ if (configuration) {
919
+ assert(configuration.view.endedAt === undefined, 'call-ended');
920
+ for (const op of this.#operations.values())
921
+ if (op.scope === configuration.view.context?.token && op.status === 'running')
922
+ this.#controllers.get(op.id)?.abort();
923
+ this.#endConfiguration(configuration, { status: 'cancelled' });
924
+ configuration.controller.abort();
925
+ return;
926
+ }
660
927
  const request = this.#requests.get(id);
661
928
  assert(request && request.view.endedAt === undefined && !request.signal.aborted, 'call-ended');
662
929
  request.exec.agent.cancel({ kind: 'user' }, { keepInbox: true });
@@ -679,9 +946,17 @@ export class MarivoCredentialService {
679
946
  this.#tasks.add(tracked);
680
947
  return task;
681
948
  }
682
- async close() {
949
+ close() {
950
+ if (this.#closing)
951
+ return this.#closing;
683
952
  this.#lifetime.abort();
684
- this.#changed();
685
- await Promise.allSettled([...this.#tasks]);
953
+ this.#closing = finishCleanup([
954
+ () => this.#changed(),
955
+ async () => {
956
+ while (this.#tasks.size)
957
+ await Promise.allSettled([...this.#tasks]);
958
+ },
959
+ ]);
960
+ return this.#closing;
686
961
  }
687
962
  }
@@ -1,10 +1,11 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { registerMarivoTool } from "../tool-lifecycle.js";
2
3
  import { resolveMarivoDatasourceBridge } from "./bridge.js";
3
4
  export const MARIVO_DATASOURCE_TEST_TOOL_NAME = 'marivo_datasource_test';
4
5
  export function createMarivoDatasourceTestTool(source, service) {
5
6
  return defineTool({
6
7
  name: MARIVO_DATASOURCE_TEST_TOOL_NAME,
7
- description: 'Test a Marivo datasource through Host credentials. Missing credentials wait for the Web form while this call remains alive; configured connection failures return directly.',
8
+ description: 'Test a Marivo datasource through Host credentials after datasource changes, credential rotation, connection failures, or explicit user requests. Normal analysis with configured credentials needs no preliminary test; successful marivo_datasource_configure already tested that configuration. Missing credentials wait for the Web form only while this call remains alive; configured connection failures return directly. Only status=ok proves this connection test succeeded; verify the requested table and read access separately.',
8
9
  parameters: {
9
10
  name: { type: 'string', required: true, description: 'Configured datasource name.' },
10
11
  },
@@ -20,7 +21,7 @@ export function createMarivoDatasourceTestTool(source, service) {
20
21
  });
21
22
  }
22
23
  export function registerMarivoDatasourceTestTool(ctx, source, service) {
23
- return ctx.tools.register(createMarivoDatasourceTestTool(source, service));
24
+ return registerMarivoTool(ctx, createMarivoDatasourceTestTool(source, service));
24
25
  }
25
26
  /** Preserve the datasource tools' public status family; raw Marivo results stay inside the service. */
26
27
  export function datasourceToolValue(result) {