@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +83 -34
  2. package/lib/bin/environment.js +1 -1
  3. package/lib/bin/presentation-lint.js +21 -0
  4. package/lib/bridges.js +2 -2
  5. package/lib/client/credentials/create-datasource.js +67 -0
  6. package/lib/client/credentials/install.js +191 -0
  7. package/lib/client/credentials/model.js +494 -0
  8. package/lib/client/credentials/styles.js +17 -0
  9. package/lib/client/workspace-header-action.js +11 -0
  10. package/lib/client.js +1133 -736
  11. package/lib/compatibility.js +1 -1
  12. package/lib/datasource/authoring.js +70 -0
  13. package/lib/datasource/bridge-programs.js +30 -18
  14. package/lib/datasource/bridge.js +100 -8
  15. package/lib/datasource/index.js +3 -2
  16. package/lib/datasource/python.js +115 -0
  17. package/lib/datasource/resolver-program.js +112 -0
  18. package/lib/datasource/rpc.js +88 -0
  19. package/lib/datasource/service.js +687 -0
  20. package/lib/datasource/shell-env.js +2 -333
  21. package/lib/datasource/test.js +24 -51
  22. package/lib/environment/binding.js +54 -9
  23. package/lib/environment/runtime.js +89 -78
  24. package/lib/environment/subprocess.js +6 -1
  25. package/lib/environment/summary.js +3 -3
  26. package/lib/environment/workspace.js +5 -0
  27. package/lib/index.js +1 -1
  28. package/lib/plugin.js +168 -51
  29. package/lib/presentation/assets/portable.js +606 -0
  30. package/lib/presentation/assets/static.js +945 -0
  31. package/lib/presentation/build/index.js +71 -0
  32. package/lib/presentation/commit.js +156 -0
  33. package/lib/presentation/contracts/charts.js +476 -0
  34. package/lib/presentation/contracts/editing.js +58 -0
  35. package/lib/presentation/contracts/index.js +651 -0
  36. package/lib/presentation/contracts/interaction.js +62 -0
  37. package/lib/presentation/contracts/types.js +15 -0
  38. package/lib/presentation/contracts/values.js +28 -0
  39. package/lib/presentation/delivery.js +56 -0
  40. package/lib/presentation/files.js +83 -0
  41. package/lib/presentation/index.js +6 -0
  42. package/lib/presentation/lint.js +46 -0
  43. package/lib/presentation/projection/files.js +85 -0
  44. package/lib/presentation/projection/index.js +195 -0
  45. package/lib/presentation/projection/program.js +144 -0
  46. package/lib/presentation/projection/sql-code.js +79 -0
  47. package/lib/presentation/receipt.js +34 -0
  48. package/lib/presentation/reports.js +122 -0
  49. package/lib/presentation/rpc.js +148 -0
  50. package/lib/presentation/tool.js +122 -0
  51. package/lib/python-execution-contracts.js +1 -0
  52. package/lib/python-execution.js +232 -0
  53. package/lib/semantic-browser/contracts.js +80 -0
  54. package/lib/semantic-browser/definition.js +64 -0
  55. package/lib/semantic-browser/program.js +119 -0
  56. package/lib/semantic-browser/service.js +72 -0
  57. package/lib/semantic-reference/bridge.js +118 -0
  58. package/lib/semantic-reference/contracts.js +113 -0
  59. package/lib/semantic-reference/index.js +3 -0
  60. package/lib/semantic-reference/rpc.js +122 -0
  61. package/lib/semantic-reference/search.js +94 -0
  62. package/lib/semantic-reference/usage.js +143 -0
  63. package/lib/types/bridges.d.ts +2 -2
  64. package/lib/types/client/credentials/create-datasource.d.ts +5 -0
  65. package/lib/types/client/credentials/install.d.ts +1 -0
  66. package/lib/types/client/credentials/model.d.ts +59 -0
  67. package/lib/types/client/credentials/styles.d.ts +1 -0
  68. package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
  69. package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
  70. package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
  71. package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
  72. package/lib/types/client/presentation/chart-specials.d.ts +10 -0
  73. package/lib/types/client/presentation/chart-styles.d.ts +1 -0
  74. package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
  75. package/lib/types/client/presentation/chart-view.d.ts +23 -0
  76. package/lib/types/client/presentation/copy-context.d.ts +5 -0
  77. package/lib/types/client/presentation/delivery-model.d.ts +54 -0
  78. package/lib/types/client/presentation/delivery.d.ts +77 -0
  79. package/lib/types/client/presentation/editor-controls.d.ts +12 -0
  80. package/lib/types/client/presentation/global-filters.d.ts +6 -0
  81. package/lib/types/client/presentation/host-entry.d.ts +12 -0
  82. package/lib/types/client/presentation/icons.d.ts +4 -0
  83. package/lib/types/client/presentation/install.d.ts +15 -0
  84. package/lib/types/client/presentation/markdown.d.ts +5 -0
  85. package/lib/types/client/presentation/model.d.ts +62 -0
  86. package/lib/types/client/presentation/reader.d.ts +11 -0
  87. package/lib/types/client/presentation/source-code-model.d.ts +16 -0
  88. package/lib/types/client/presentation/source-code.d.ts +11 -0
  89. package/lib/types/client/presentation/source-dialog.d.ts +13 -0
  90. package/lib/types/client/presentation/source-facts.d.ts +20 -0
  91. package/lib/types/client/presentation/source-format.d.ts +5 -0
  92. package/lib/types/client/presentation/source-highlight.d.ts +7 -0
  93. package/lib/types/client/presentation/sources.d.ts +13 -0
  94. package/lib/types/client/presentation/styles.d.ts +1 -0
  95. package/lib/types/client/presentation/table.d.ts +12 -0
  96. package/lib/types/client/presentation/use-element-width.d.ts +5 -0
  97. package/lib/types/client/workspace-header-action.d.ts +7 -0
  98. package/lib/types/client.d.ts +4 -122
  99. package/lib/types/compatibility.d.ts +2 -2
  100. package/lib/types/datasource/authoring.d.ts +25 -0
  101. package/lib/types/datasource/bridge.d.ts +21 -3
  102. package/lib/types/datasource/index.d.ts +3 -5
  103. package/lib/types/datasource/python.d.ts +4 -0
  104. package/lib/types/datasource/resolver-program.d.ts +6 -0
  105. package/lib/types/datasource/rpc.d.ts +4 -0
  106. package/lib/types/datasource/service.d.ts +112 -0
  107. package/lib/types/datasource/shell-env.d.ts +0 -29
  108. package/lib/types/datasource/test.d.ts +6 -29
  109. package/lib/types/environment/binding.d.ts +5 -1
  110. package/lib/types/environment/errors.d.ts +1 -1
  111. package/lib/types/environment/runtime.d.ts +5 -5
  112. package/lib/types/environment/types.d.ts +13 -2
  113. package/lib/types/index.d.ts +1 -1
  114. package/lib/types/plugin.d.ts +11 -5
  115. package/lib/types/presentation/build/index.d.ts +11 -0
  116. package/lib/types/presentation/commit.d.ts +4 -0
  117. package/lib/types/presentation/contracts/charts.d.ts +13 -0
  118. package/lib/types/presentation/contracts/editing.d.ts +8 -0
  119. package/lib/types/presentation/contracts/index.d.ts +11 -0
  120. package/lib/types/presentation/contracts/interaction.d.ts +8 -0
  121. package/lib/types/presentation/contracts/types.d.ts +252 -0
  122. package/lib/types/presentation/contracts/values.d.ts +9 -0
  123. package/lib/types/presentation/delivery.d.ts +3 -0
  124. package/lib/types/presentation/files.d.ts +7 -0
  125. package/lib/types/presentation/index.d.ts +6 -0
  126. package/lib/types/presentation/lint.d.ts +13 -0
  127. package/lib/types/presentation/projection/files.d.ts +2 -0
  128. package/lib/types/presentation/projection/index.d.ts +20 -0
  129. package/lib/types/presentation/projection/program.d.ts +2 -0
  130. package/lib/types/presentation/projection/sql-code.d.ts +2 -0
  131. package/lib/types/presentation/receipt.d.ts +14 -0
  132. package/lib/types/presentation/reports.d.ts +5 -0
  133. package/lib/types/presentation/rpc.d.ts +24 -0
  134. package/lib/types/presentation/tool.d.ts +12 -0
  135. package/lib/types/python-execution-contracts.d.ts +10 -0
  136. package/lib/types/python-execution.d.ts +10 -0
  137. package/lib/types/semantic-browser/contracts.d.ts +40 -0
  138. package/lib/types/semantic-browser/definition.d.ts +20 -0
  139. package/lib/types/semantic-browser/program.d.ts +2 -0
  140. package/lib/types/semantic-browser/service.d.ts +20 -0
  141. package/lib/types/semantic-reference/bridge.d.ts +11 -0
  142. package/lib/types/semantic-reference/contracts.d.ts +51 -0
  143. package/lib/types/semantic-reference/index.d.ts +3 -0
  144. package/lib/types/semantic-reference/rpc.d.ts +15 -0
  145. package/lib/types/semantic-reference/search.d.ts +10 -0
  146. package/lib/types/semantic-reference/usage.d.ts +43 -0
  147. package/package.json +43 -19
  148. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  149. package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
  150. package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
  151. package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
  152. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
  153. package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
  154. package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
  155. package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
  156. package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
  157. package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
  158. package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
  159. package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
  160. package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
  161. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
  162. package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
  163. package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
  164. package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
  165. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
  166. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
  167. package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
  168. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
  169. package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
  170. package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
  171. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
  172. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
  173. package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
  174. package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
  175. package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
  176. package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
  177. package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
  178. package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
  179. package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
  180. package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
  181. package/lib/datasource/access.js +0 -60
  182. package/lib/datasource/credentials.js +0 -24
  183. package/lib/evidence/bridge-program.js +0 -153
  184. package/lib/evidence/bridge.js +0 -166
  185. package/lib/evidence/index.js +0 -2
  186. package/lib/evidence/sources.js +0 -263
  187. package/lib/types/datasource/access.d.ts +0 -27
  188. package/lib/types/evidence/bridge.d.ts +0 -40
  189. package/lib/types/evidence/index.d.ts +0 -4
  190. package/lib/types/evidence/sources.d.ts +0 -57
  191. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  192. package/skills/dsh-data-analysis-report/SKILL.md +0 -57
  193. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
  194. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
  195. package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
@@ -0,0 +1,687 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
3
+ import { marivoCredentialStorageRef } from "./shell-env.js";
4
+ export const CREDENTIAL_CHANNEL = '/dsh-data-analysis-credentials';
5
+ const RETENTION = 30 * 60_000;
6
+ const CAPACITY = 512;
7
+ export class CredentialServiceError extends Error {
8
+ code;
9
+ constructor(code) {
10
+ super(code);
11
+ this.code = code;
12
+ this.name = 'CredentialServiceError';
13
+ }
14
+ }
15
+ export function credentialError(error) {
16
+ return error instanceof CredentialServiceError ? error.code : 'credential-operation-failed';
17
+ }
18
+ function assert(condition, code) {
19
+ if (!condition)
20
+ throw new CredentialServiceError(code);
21
+ }
22
+ /** Owns only plugin operations and execution-scoped waits. No value cache or datasource registry. */
23
+ export class MarivoCredentialService {
24
+ generation = randomUUID();
25
+ #store;
26
+ #contexts = new Map();
27
+ #requests = new Map();
28
+ #operations = new Map();
29
+ #controllers = new Map();
30
+ #tasks = new Set();
31
+ #versions = new Map();
32
+ #activeRefs = new Map();
33
+ #history = new Map();
34
+ #lifetime = new AbortController();
35
+ #agents = new WeakMap();
36
+ #listeners = new Set();
37
+ #revision = 0;
38
+ #tail = Promise.resolve();
39
+ interaction;
40
+ now;
41
+ constructor(store, interaction = 'web', now = Date.now) {
42
+ this.#store = store;
43
+ this.interaction = interaction;
44
+ this.now = now;
45
+ }
46
+ executionSignal(caller, agent) {
47
+ let lifecycle;
48
+ if (agent) {
49
+ lifecycle = this.#agents.get(agent);
50
+ if (!lifecycle) {
51
+ lifecycle = new AbortController();
52
+ this.#agents.set(agent, lifecycle);
53
+ }
54
+ }
55
+ return AbortSignal.any([
56
+ caller,
57
+ this.#lifetime.signal,
58
+ ...(lifecycle ? [lifecycle.signal] : []),
59
+ ]);
60
+ }
61
+ #changed() {
62
+ this.#revision++;
63
+ for (const listener of this.#listeners)
64
+ listener();
65
+ }
66
+ #prune() {
67
+ const now = this.now();
68
+ for (const [id, op] of this.#operations)
69
+ if (op.endedAt !== undefined && now - op.endedAt >= RETENTION)
70
+ this.#operations.delete(id);
71
+ for (const [id, request] of this.#requests)
72
+ if (request.view.endedAt !== undefined && now - request.view.endedAt >= RETENTION)
73
+ this.#requests.delete(id);
74
+ for (const [id, context] of this.#contexts)
75
+ if (now - context.touched >= RETENTION &&
76
+ ![...this.#requests.values()].some((r) => r.context === context && r.view.endedAt === undefined) &&
77
+ ![...this.#operations.values()].some((op) => op.scope === id && op.status === 'running'))
78
+ this.#contexts.delete(id);
79
+ for (const [key, history] of this.#history)
80
+ if (now - history.at >= RETENTION)
81
+ this.#history.delete(key);
82
+ const retainedRefs = new Set([
83
+ ...[...this.#contexts.values()].flatMap((context) => context.description.refs),
84
+ ...[...this.#history.values()].flatMap((history) => history.refs),
85
+ ...this.#activeRefs.keys(),
86
+ ]);
87
+ for (const ref of this.#versions.keys())
88
+ if (!retainedRefs.has(ref))
89
+ this.#versions.delete(ref);
90
+ }
91
+ async #locked(fn, signal) {
92
+ const previous = this.#tail;
93
+ let release;
94
+ this.#tail = new Promise((resolve) => {
95
+ release = resolve;
96
+ });
97
+ await previous;
98
+ try {
99
+ signal.throwIfAborted();
100
+ return await fn();
101
+ }
102
+ finally {
103
+ release();
104
+ }
105
+ }
106
+ #version(context) {
107
+ return JSON.stringify(context.description.refs.map((ref) => [ref, this.#versions.get(ref) ?? 0]));
108
+ }
109
+ #historyKey(context) {
110
+ return JSON.stringify([
111
+ context.bridge.binding.fingerprint,
112
+ context.description.name,
113
+ context.description.definition,
114
+ ]);
115
+ }
116
+ invalidateStorageRef(storageRef) {
117
+ const refs = new Set([...this.#contexts.values()].flatMap((context) => context.description.refs));
118
+ for (const ref of this.#activeRefs.keys())
119
+ refs.add(ref);
120
+ for (const history of this.#history.values())
121
+ for (const ref of history.refs)
122
+ refs.add(ref);
123
+ this.invalidate([...refs].filter((ref) => marivoCredentialStorageRef(ref) === storageRef));
124
+ }
125
+ invalidate(refs) {
126
+ for (const ref of refs)
127
+ this.#versions.set(ref, (this.#versions.get(ref) ?? 0) + 1);
128
+ this.#changed();
129
+ }
130
+ async #current(context, signal) {
131
+ signal.throwIfAborted();
132
+ const bridge = await context.resolve();
133
+ assert(bridge.binding.fingerprint === context.bridge.binding.fingerprint, 'context-changed');
134
+ const described = await bridge.describe(context.description.name, signal);
135
+ assert(described.definition === context.description.definition, 'context-changed');
136
+ signal.throwIfAborted();
137
+ context.touched = this.now();
138
+ }
139
+ async #view(context) {
140
+ const credentials = Object.create(null);
141
+ for (const ref of context.description.refs) {
142
+ try {
143
+ credentials[ref] = await this.#store.describe(credentialRef(marivoCredentialStorageRef(ref)));
144
+ }
145
+ catch {
146
+ throw new CredentialServiceError('credential-state-unavailable');
147
+ }
148
+ }
149
+ const history = this.#history.get(this.#historyKey(context));
150
+ return {
151
+ token: context.token,
152
+ workspaceId: context.workspaceId,
153
+ name: context.description.name,
154
+ backend: context.description.backend ?? '',
155
+ properties: context.description.properties ?? {},
156
+ refs: context.description.refs,
157
+ fields: context.description.fields,
158
+ version: this.#version(context),
159
+ credentials,
160
+ ...(history
161
+ ? {
162
+ lastTest: {
163
+ at: history.at,
164
+ result: history.result,
165
+ stale: history.version !== this.#version(context),
166
+ },
167
+ }
168
+ : {}),
169
+ };
170
+ }
171
+ async #bind(workspaceId, resolve, name, signal) {
172
+ this.#prune();
173
+ assert(!this.#lifetime.signal.aborted, 'disposed');
174
+ const bridge = await resolve();
175
+ const description = await bridge.describe(name, signal);
176
+ assert(description.name === name, 'invalid-datasource-context');
177
+ for (const ref of description.refs)
178
+ marivoCredentialStorageRef(ref);
179
+ assert(typeof description.definition === 'string' && !!description.fields, 'invalid-datasource-context');
180
+ const context = {
181
+ token: randomUUID(),
182
+ workspaceId,
183
+ bridge,
184
+ description,
185
+ resolve,
186
+ touched: this.now(),
187
+ };
188
+ await this.#current(context, signal);
189
+ return context;
190
+ }
191
+ async overview(workspaceId, resolve, caller) {
192
+ const signal = AbortSignal.any([caller, this.#lifetime.signal]);
193
+ const bridge = await resolve();
194
+ const items = await bridge.inventory(signal);
195
+ assert(items.length <= 128, 'datasource-limit-exceeded');
196
+ const views = [];
197
+ this.#prune();
198
+ assert(!signal.aborted, 'disposed');
199
+ for (const item of items) {
200
+ for (const ref of item.refs)
201
+ marivoCredentialStorageRef(ref);
202
+ const context = {
203
+ token: randomUUID(),
204
+ workspaceId,
205
+ bridge,
206
+ description: item,
207
+ resolve,
208
+ touched: this.now(),
209
+ };
210
+ const old = [...this.#contexts.values()].find((c) => c.workspaceId === workspaceId &&
211
+ c.description.name === context.description.name &&
212
+ c.description.definition === context.description.definition &&
213
+ c.bridge.binding.fingerprint === context.bridge.binding.fingerprint);
214
+ if (old) {
215
+ this.#contexts.delete(old.token);
216
+ context.token = old.token;
217
+ }
218
+ assert(this.#contexts.size < CAPACITY, 'capacity-exceeded');
219
+ this.#contexts.set(context.token, context);
220
+ views.push(await this.#view(context));
221
+ }
222
+ const current = await resolve();
223
+ assert(current.binding.fingerprint === bridge.binding.fingerprint, 'context-changed');
224
+ signal.throwIfAborted();
225
+ return views;
226
+ }
227
+ async createDatasource(generation, fingerprint, input, resolve, caller) {
228
+ const signal = AbortSignal.any([caller, this.#lifetime.signal]);
229
+ return this.#locked(async () => {
230
+ assert(generation === this.generation, 'context-changed');
231
+ const bridge = await resolve();
232
+ assert(bridge.binding.fingerprint === fingerprint, 'context-changed');
233
+ assert(bridge.create, 'datasource-authoring-unavailable');
234
+ signal.throwIfAborted();
235
+ const result = await bridge.create(input, signal);
236
+ assert(!result.error && result.name, result.error ?? 'datasource-definition-invalid');
237
+ return { name: result.name };
238
+ }, signal);
239
+ }
240
+ #watchRefs(refs) {
241
+ const unique = new Set(refs);
242
+ for (const ref of unique)
243
+ this.#activeRefs.set(ref, (this.#activeRefs.get(ref) ?? 0) + 1);
244
+ return () => {
245
+ for (const ref of unique) {
246
+ const count = this.#activeRefs.get(ref) - 1;
247
+ if (count === 0)
248
+ this.#activeRefs.delete(ref);
249
+ else
250
+ this.#activeRefs.set(ref, count);
251
+ }
252
+ };
253
+ }
254
+ async #snapshot(contexts, signal) {
255
+ const stop = this.#watchRefs(contexts.flatMap((context) => context.description.refs));
256
+ const values = Object.create(null);
257
+ try {
258
+ return await this.#locked(async () => {
259
+ for (const context of contexts)
260
+ await this.#current(context, signal);
261
+ const versions = contexts.map((context) => this.#version(context));
262
+ for (const ref of new Set(contexts.flatMap((context) => context.description.refs))) {
263
+ let resolved;
264
+ try {
265
+ resolved = await this.#store.resolve(credentialRef(marivoCredentialStorageRef(ref)));
266
+ }
267
+ catch {
268
+ throw new CredentialServiceError('credential-unavailable');
269
+ }
270
+ signal.throwIfAborted();
271
+ assert(resolved?.value, 'credential-missing');
272
+ values[ref] = resolved.value;
273
+ }
274
+ for (const [index, context] of contexts.entries()) {
275
+ await this.#current(context, signal);
276
+ assert(versions[index] === this.#version(context), 'credentials-changed');
277
+ }
278
+ return values;
279
+ }, signal);
280
+ }
281
+ catch (error) {
282
+ for (const ref of Object.keys(values))
283
+ delete values[ref];
284
+ throw error;
285
+ }
286
+ finally {
287
+ stop();
288
+ }
289
+ }
290
+ async #test(context, signal) {
291
+ const stop = this.#watchRefs(context.description.refs);
292
+ let values;
293
+ try {
294
+ const version = this.#version(context);
295
+ values = await this.#snapshot([context], signal);
296
+ const result = await context.bridge.test(context.description, values, signal);
297
+ await this.#current(context, signal);
298
+ assert(version === this.#version(context), 'credentials-changed');
299
+ const key = this.#historyKey(context);
300
+ this.#history.delete(key);
301
+ if (this.#history.size >= CAPACITY)
302
+ this.#history.delete(this.#history.keys().next().value);
303
+ this.#history.set(key, {
304
+ at: this.now(),
305
+ version,
306
+ refs: [...context.description.refs],
307
+ result,
308
+ });
309
+ return result;
310
+ }
311
+ finally {
312
+ if (values)
313
+ for (const ref of Object.keys(values))
314
+ delete values[ref];
315
+ stop();
316
+ }
317
+ }
318
+ async #awaitCredentials(exec, context, view, signal) {
319
+ const agent = exec.agent;
320
+ const missing = view.refs.filter((ref) => !view.credentials[ref]?.configured);
321
+ if (missing.length === 0)
322
+ return undefined;
323
+ if (this.interaction === 'none' ||
324
+ agent.session.header.origin === 'subagent' ||
325
+ (agent.session.header.delegationDepth ?? 0) > 0)
326
+ return { status: 'needs-credentials', name: context.description.name, refs: missing };
327
+ this.#prune();
328
+ assert(this.#requests.size < CAPACITY / 2, 'capacity-exceeded');
329
+ assert(this.#contexts.size < CAPACITY, 'capacity-exceeded');
330
+ this.#contexts.set(context.token, context);
331
+ return new Promise((finish, reject) => {
332
+ const id = randomUUID();
333
+ const request = {
334
+ context,
335
+ exec,
336
+ signal,
337
+ finish,
338
+ reject,
339
+ stop: () => signal.removeEventListener('abort', abort),
340
+ view: { id, sessionId: agent.session.id, context: view, status: 'awaiting-input' },
341
+ };
342
+ const abort = () => {
343
+ if (request.view.endedAt !== undefined)
344
+ return;
345
+ request.view.status = 'call-ended';
346
+ request.view.endedAt = this.now();
347
+ request.stop();
348
+ reject(new CredentialServiceError('call-ended'));
349
+ this.#changed();
350
+ };
351
+ this.#requests.set(id, request);
352
+ signal.addEventListener('abort', abort, { once: true });
353
+ if (signal.aborted)
354
+ abort();
355
+ this.#changed();
356
+ });
357
+ }
358
+ async prepare(_kind, exec, resolve, name) {
359
+ assert(exec.agent, 'agent-required');
360
+ const signal = this.executionSignal(exec.signal, exec.agent);
361
+ const context = await this.#bind('', resolve, name, signal);
362
+ const stop = this.#watchRefs(context.description.refs);
363
+ try {
364
+ const tested = await this.#awaitCredentials(exec, context, await this.#view(context), signal);
365
+ return tested
366
+ ? 'result' in tested
367
+ ? tested.result
368
+ : tested
369
+ : await this.#test(context, signal);
370
+ }
371
+ finally {
372
+ stop();
373
+ }
374
+ }
375
+ async prepareExecution(exec, resolve, names) {
376
+ assert(exec.agent, 'agent-required');
377
+ assert(names.length <= 16 &&
378
+ new Set(names).size === names.length &&
379
+ names.every((name) => typeof name === 'string' && !!name.trim() && name.length <= 256), 'invalid-datasources');
380
+ const signal = this.executionSignal(exec.signal, exec.agent);
381
+ const session = exec.agent.session;
382
+ const currentWorkspace = () => JSON.stringify([Reflect.get(session.header, 'workspaceId'), session.header.cwd]);
383
+ const workspace = currentWorkspace();
384
+ const contexts = [];
385
+ const stops = [];
386
+ const versions = new Map();
387
+ let values = {};
388
+ let released = false;
389
+ const release = () => {
390
+ if (released)
391
+ return;
392
+ released = true;
393
+ for (const ref of Object.keys(values))
394
+ delete values[ref];
395
+ for (const stop of stops)
396
+ stop();
397
+ };
398
+ try {
399
+ for (const name of names) {
400
+ const context = await this.#bind('', resolve, name, signal);
401
+ assert(!contexts.length ||
402
+ context.bridge.binding.fingerprint === contexts[0].bridge.binding.fingerprint, 'context-changed');
403
+ contexts.push(context);
404
+ stops.push(this.#watchRefs(context.description.refs));
405
+ const view = await this.#view(context);
406
+ if (view.refs.every((ref) => view.credentials[ref]?.configured))
407
+ versions.set(context, view.version);
408
+ }
409
+ for (const context of contexts) {
410
+ // Another datasource form may have supplied shared refs while this call waited.
411
+ const view = await this.#view(context);
412
+ if (!versions.has(context) && view.refs.every((ref) => view.credentials[ref]?.configured))
413
+ versions.set(context, view.version);
414
+ const prepared = await this.#awaitCredentials(exec, context, view, signal);
415
+ if (prepared && (!('result' in prepared) || !prepared.result.ok)) {
416
+ release();
417
+ return 'result' in prepared ? prepared.result : prepared;
418
+ }
419
+ if (prepared) {
420
+ assert(!versions.has(context) || versions.get(context) === prepared.version, 'credentials-changed');
421
+ versions.set(context, prepared.version);
422
+ }
423
+ }
424
+ const assertCurrent = () => {
425
+ assert(!released, 'execution-ended');
426
+ signal.throwIfAborted();
427
+ assert(exec.agent?.session === session && currentWorkspace() === workspace, 'context-changed');
428
+ for (const context of contexts)
429
+ assert(versions.get(context) === this.#version(context), 'credentials-changed');
430
+ };
431
+ assertCurrent();
432
+ values = await this.#snapshot(contexts, signal);
433
+ assertCurrent();
434
+ const bridge = contexts[0]?.bridge ?? (await resolve());
435
+ if (!contexts.length) {
436
+ const fingerprint = bridge.binding.fingerprint;
437
+ const current = await resolve();
438
+ assert(current.binding.fingerprint === fingerprint, 'context-changed');
439
+ }
440
+ assertCurrent();
441
+ return {
442
+ status: 'ready',
443
+ bridge,
444
+ values,
445
+ grants: Object.fromEntries(contexts.map((c) => [c.description.name, c.description])),
446
+ signal,
447
+ assertCurrent,
448
+ release,
449
+ };
450
+ }
451
+ catch (error) {
452
+ release();
453
+ throw error;
454
+ }
455
+ }
456
+ watch(sessionId, cursor) {
457
+ this.#prune();
458
+ void cursor;
459
+ return {
460
+ generation: this.generation,
461
+ cursor: `${this.generation}:${this.#revision}`,
462
+ requests: [...this.#requests.values()]
463
+ .filter((r) => r.view.sessionId === sessionId)
464
+ .map((r) => structuredClone(r.view)),
465
+ };
466
+ }
467
+ async waitWatch(sessionId, cursor, signal) {
468
+ if (cursor === this.watch(sessionId).cursor &&
469
+ !signal.aborted &&
470
+ !this.#lifetime.signal.aborted) {
471
+ await new Promise((resolve) => {
472
+ const end = () => {
473
+ clearTimeout(timer);
474
+ this.#listeners.delete(end);
475
+ signal.removeEventListener('abort', end);
476
+ resolve();
477
+ };
478
+ const timer = setTimeout(end, 25_000);
479
+ this.#listeners.add(end);
480
+ signal.addEventListener('abort', end, { once: true });
481
+ if (signal.aborted)
482
+ end();
483
+ });
484
+ }
485
+ signal.throwIfAborted();
486
+ for (const request of this.#requests.values()) {
487
+ if (request.view.sessionId !== sessionId || request.view.endedAt !== undefined)
488
+ continue;
489
+ const view = await this.#view(request.context);
490
+ if (JSON.stringify(view) !== JSON.stringify(request.view.context)) {
491
+ request.view.context = view;
492
+ this.#changed();
493
+ }
494
+ }
495
+ return this.watch(sessionId);
496
+ }
497
+ operation(generation, id, scope) {
498
+ this.#prune();
499
+ if (generation !== this.generation)
500
+ return undefined;
501
+ const operation = this.#operations.get(id);
502
+ assert(!operation || operation.scope === scope, 'operation-scope-mismatch');
503
+ return operation ? structuredClone(operation) : undefined;
504
+ }
505
+ start(input) {
506
+ this.#prune();
507
+ assert(input.generation === this.generation && !this.#lifetime.signal.aborted, 'context-changed');
508
+ const existing = this.#operations.get(input.id);
509
+ if (existing) {
510
+ assert(existing.scope === input.scope && existing.action === input.action, 'operation-scope-mismatch');
511
+ return structuredClone(existing);
512
+ }
513
+ assert(this.#operations.size < CAPACITY, 'capacity-exceeded');
514
+ const context = this.#contexts.get(input.scope);
515
+ assert(context, 'context-changed');
516
+ const request = input.requestId ? this.#requests.get(input.requestId) : undefined;
517
+ assert(!input.requestId ||
518
+ (request &&
519
+ request.context === context &&
520
+ request.view.endedAt === undefined &&
521
+ !request.signal.aborted), 'call-ended');
522
+ 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');
524
+ if (request)
525
+ assert(request.view.status !== 'executing', 'operation-busy');
526
+ assert(input.version === this.#version(context), 'credentials-changed');
527
+ const changes = input.changes ?? {};
528
+ assert(Object.keys(changes).every((ref) => context.description.refs.includes(ref) &&
529
+ typeof changes[ref] === 'string' &&
530
+ changes[ref].length > 0 &&
531
+ changes[ref].length <= 65536), 'invalid-changes');
532
+ assert(!Object.keys(changes).length ||
533
+ input.action === 'save' ||
534
+ input.action === 'update' ||
535
+ input.action === 'submit', 'invalid-changes');
536
+ assert(input.action !== 'delete' ||
537
+ (input.reference && context.description.refs.includes(input.reference)), 'invalid-reference');
538
+ const op = {
539
+ id: input.id,
540
+ scope: input.scope,
541
+ action: input.action,
542
+ status: 'running',
543
+ phase: 'saving',
544
+ saved: [],
545
+ errors: [],
546
+ };
547
+ this.#operations.set(op.id, op);
548
+ const controller = new AbortController();
549
+ this.#controllers.set(op.id, controller);
550
+ const signal = AbortSignal.any([
551
+ controller.signal,
552
+ this.#lifetime.signal,
553
+ ...(request ? [request.signal] : []),
554
+ ]);
555
+ if (request)
556
+ request.view.status = 'executing';
557
+ const task = this.#run(context, request, op, changes, input.reference, input.version, signal).finally(() => {
558
+ this.#controllers.delete(op.id);
559
+ this.#tasks.delete(task);
560
+ });
561
+ this.#tasks.add(task);
562
+ this.#changed();
563
+ return structuredClone(op);
564
+ }
565
+ async #run(context, request, op, changes, reference, version, signal) {
566
+ try {
567
+ await this.#current(context, signal);
568
+ if (op.action === 'diagnose') {
569
+ assert(request?.view.failure, 'no-test-failure');
570
+ op.result = request.view.failure;
571
+ request.view.status = 'handed-off';
572
+ request.view.endedAt = this.now();
573
+ request.stop();
574
+ request.finish({ result: op.result, version: this.#version(context) });
575
+ }
576
+ else {
577
+ await this.#locked(async () => {
578
+ await this.#current(context, signal);
579
+ assert(version === this.#version(context), 'credentials-changed');
580
+ const refs = op.action === 'delete' ? [reference] : Object.keys(changes);
581
+ for (const ref of refs) {
582
+ signal.throwIfAborted();
583
+ // Revocation precedes writes; a snapshot already committed may finish.
584
+ this.invalidate([ref]);
585
+ try {
586
+ const storageRef = credentialRef(marivoCredentialStorageRef(ref));
587
+ if (op.action === 'delete')
588
+ await this.#store.unset(storageRef);
589
+ else
590
+ await this.#store.set(storageRef, changes[ref]);
591
+ op.saved.push(ref);
592
+ }
593
+ catch {
594
+ op.errors.push(ref);
595
+ }
596
+ }
597
+ }, signal);
598
+ assert(op.errors.length === 0, 'credential-save-failed');
599
+ for (const ref of Object.keys(changes))
600
+ delete changes[ref];
601
+ if (op.action !== 'delete' && op.action !== 'save') {
602
+ op.phase = 'validating';
603
+ this.#changed();
604
+ const testedVersion = this.#version(context);
605
+ op.result = await this.#test(context, signal);
606
+ if (request && request.view.endedAt === undefined) {
607
+ request.view.context = await this.#view(context);
608
+ signal.throwIfAborted();
609
+ assert(testedVersion === this.#version(context), 'credentials-changed');
610
+ if (op.result.ok) {
611
+ request.view.status = 'succeeded';
612
+ request.view.endedAt = this.now();
613
+ request.stop();
614
+ request.finish({ result: op.result, version: testedVersion });
615
+ }
616
+ else {
617
+ request.view.status = 'awaiting-decision';
618
+ request.view.failure = op.result;
619
+ }
620
+ }
621
+ }
622
+ }
623
+ op.status = 'succeeded';
624
+ }
625
+ catch (error) {
626
+ op.status = signal.aborted ? 'cancelled' : 'failed';
627
+ op.errors.push(signal.aborted ? 'cancelled' : credentialError(error));
628
+ if (request && request.view.endedAt === undefined) {
629
+ if (credentialError(error) === 'context-changed') {
630
+ request.view.status = 'context-changed';
631
+ request.view.endedAt = this.now();
632
+ request.stop();
633
+ request.reject(new CredentialServiceError('context-changed'));
634
+ }
635
+ else {
636
+ request.view.status = 'awaiting-input';
637
+ try {
638
+ request.view.context = await this.#view(context);
639
+ }
640
+ catch {
641
+ /* keep last safe facts */
642
+ }
643
+ }
644
+ }
645
+ }
646
+ finally {
647
+ for (const ref of Object.keys(changes))
648
+ delete changes[ref];
649
+ op.phase = 'settled';
650
+ op.endedAt = this.now();
651
+ this.#changed();
652
+ }
653
+ }
654
+ cancelOperation(generation, id, scope) {
655
+ const operation = this.operation(generation, id, scope);
656
+ assert(operation, 'operation-unrecoverable');
657
+ this.#controllers.get(id)?.abort();
658
+ }
659
+ cancelRequest(id) {
660
+ const request = this.#requests.get(id);
661
+ assert(request && request.view.endedAt === undefined && !request.signal.aborted, 'call-ended');
662
+ request.exec.agent.cancel({ kind: 'user' }, { keepInbox: true });
663
+ }
664
+ disposeAgent(agent) {
665
+ this.#agents.get(agent)?.abort();
666
+ for (const request of this.#requests.values())
667
+ if (request.exec.agent === agent && request.view.endedAt === undefined) {
668
+ request.view.status = 'call-ended';
669
+ request.view.endedAt = this.now();
670
+ request.stop();
671
+ request.reject(new CredentialServiceError('call-ended'));
672
+ }
673
+ this.#changed();
674
+ }
675
+ track(task) {
676
+ const tracked = task
677
+ .then(() => { }, () => { })
678
+ .finally(() => this.#tasks.delete(tracked));
679
+ this.#tasks.add(tracked);
680
+ return task;
681
+ }
682
+ async close() {
683
+ this.#lifetime.abort();
684
+ this.#changed();
685
+ await Promise.allSettled([...this.#tasks]);
686
+ }
687
+ }