@chengxianglibra/dsh-data-analysis 0.3.0 → 0.3.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 (53) hide show
  1. package/README.en.md +7 -2
  2. package/README.md +7 -2
  3. package/assets/logo-horizontal.svg +42 -0
  4. package/assets/logo-mark.svg +20 -0
  5. package/lib/bin/environment.js +99 -76
  6. package/lib/client/analysis-ui/offline-runtime.generated.js +1 -1
  7. package/lib/client/credentials/install.js +42 -38
  8. package/lib/client/credentials/model.js +9 -0
  9. package/lib/client/credentials/report-publishing.js +84 -0
  10. package/lib/client/credentials/styles.js +2 -0
  11. package/lib/client/i18n/credentials.js +18 -0
  12. package/lib/client/i18n/presentation.js +24 -0
  13. package/lib/client/right-tabs/install.js +9 -1
  14. package/lib/client/right-tabs/report-actions.js +30 -4
  15. package/lib/client/right-tabs/report-publishing.js +107 -0
  16. package/lib/client/right-tabs/styles.js +3 -0
  17. package/lib/client/ui/styles.generated.js +3 -0
  18. package/lib/client-composition.js +202 -191
  19. package/lib/client.js +205 -194
  20. package/lib/datasource/resolver-program.js +4 -4
  21. package/lib/environment/binding.js +16 -23
  22. package/lib/environment/doctor.js +8 -1
  23. package/lib/environment/identity.js +32 -0
  24. package/lib/environment/project-preparation.js +5 -5
  25. package/lib/environment/runtime.js +15 -16
  26. package/lib/plugin-agents.js +6 -1
  27. package/lib/plugin.js +18 -0
  28. package/lib/presentation/rpc.js +19 -0
  29. package/lib/report-publishing/adapters.js +136 -0
  30. package/lib/report-publishing/config.js +58 -0
  31. package/lib/report-publishing/credential-contract.js +25 -0
  32. package/lib/report-publishing/service.js +133 -0
  33. package/lib/skill-build.generated.js +2 -2
  34. package/lib/types/client/credentials/install.d.ts +2 -1
  35. package/lib/types/client/credentials/model.d.ts +30 -0
  36. package/lib/types/client/credentials/report-publishing.d.ts +5 -0
  37. package/lib/types/client/credentials/styles.d.ts +1 -1
  38. package/lib/types/client/i18n/copy.d.ts +21 -0
  39. package/lib/types/client/i18n/credentials.d.ts +18 -0
  40. package/lib/types/client/i18n/presentation.d.ts +24 -0
  41. package/lib/types/client/right-tabs/report-actions.d.ts +5 -1
  42. package/lib/types/client/right-tabs/report-publishing.d.ts +21 -0
  43. package/lib/types/client/right-tabs/styles.d.ts +1 -1
  44. package/lib/types/environment/identity.d.ts +6 -0
  45. package/lib/types/plugin-agents.d.ts +2 -0
  46. package/lib/types/plugin.d.ts +4 -0
  47. package/lib/types/presentation/rpc.d.ts +1358 -0
  48. package/lib/types/report-publishing/adapters.d.ts +6 -0
  49. package/lib/types/report-publishing/config.d.ts +23 -0
  50. package/lib/types/report-publishing/credential-contract.d.ts +61 -0
  51. package/lib/types/report-publishing/service.d.ts +31 -0
  52. package/package.json +4 -1
  53. package/skills/dsh-data-analysis-files/references/delivery.md +3 -1
@@ -1,3 +1,4 @@
1
+ import { PYTHON_IMPORT_IDENTITY } from "../environment/identity.js";
1
2
  import { PYTHON_EXCEPTION_PROGRAM } from "../python-exception.js";
2
3
  import { PYTHON_RESULT_HEADER } from "../python-result-references.js";
3
4
  /** Shared Python adapter. All inputs below arrive on a Host-owned pipe. */
@@ -43,12 +44,11 @@ class SnapshotResolver:
43
44
  datasource=request.datasource, fields=request.fields)
44
45
  `.trim();
45
46
  /** The worker checks the exact binding and enters scope before any user code. */
46
- export const PYTHON_WORKER = `${PYTHON_EXCEPTION_PROGRAM}\n${RESOLVER_PROGRAM}\n${String.raw `
47
+ export const PYTHON_WORKER = `${PYTHON_IMPORT_IDENTITY}\n${PYTHON_EXCEPTION_PROGRAM}\n${RESOLVER_PROGRAM}\n${String.raw `
47
48
  payload = json.load(sys.stdin)
48
49
  expected = payload["identity"]
49
- if (os.path.abspath(sys.executable) != expected["pythonExecutable"] or
50
- marivo.__version__ != expected["marivoVersion"] or
51
- os.path.abspath(marivo.__file__) != expected["packagePath"] or
50
+ if (_dsh_import_identity(sys.executable, marivo.__version__, marivo.__file__) !=
51
+ _dsh_import_identity(expected["pythonExecutable"], expected["marivoVersion"], expected["packagePath"]) or
52
52
  str(Path.cwd().resolve()) != payload["project_root"]):
53
53
  raise RuntimeError("Marivo execution identity changed; rebind required")
54
54
  resolver = SnapshotResolver(payload)
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { admitDoctorReport, parseDoctorReport } from "./doctor.js";
6
6
  import { MarivoEnvironmentError } from "./errors.js";
7
+ import { canonicalImportIdentity, canonicalPythonExecutable, PYTHON_IMPORT_IDENTITY, } from "./identity.js";
7
8
  import { FixedSubprocessPolicy } from "./subprocess.js";
8
9
  const DOCTOR_LIMITS = Object.freeze({
9
10
  timeoutMs: 30_000,
@@ -15,30 +16,24 @@ const IDENTITY_LIMITS = Object.freeze({
15
16
  stdoutMaxBytes: 16_384,
16
17
  stderrMaxBytes: 16_384,
17
18
  });
18
- const CHECKED_IDENTITY_PRELUDE = String.raw `
19
+ const CHECKED_IDENTITY_PRELUDE = `${PYTHON_IMPORT_IDENTITY}\n${String.raw `
19
20
  import json as _dsh_json
20
21
  import os as _dsh_os
21
22
  import sys as _dsh_sys
22
23
  try:
23
24
  import marivo as _dsh_marivo
24
- _dsh_actual = {
25
- "python_executable": _dsh_os.path.abspath(_dsh_sys.executable),
26
- "marivo_version": _dsh_marivo.__version__,
27
- "package_path": _dsh_os.path.abspath(_dsh_marivo.__file__ or ""),
28
- }
25
+ _dsh_actual = _dsh_import_identity(
26
+ _dsh_sys.executable, _dsh_marivo.__version__, _dsh_marivo.__file__,
27
+ )
29
28
  except Exception:
30
29
  raise SystemExit(78) from None
31
30
 
32
- _dsh_expected = {
33
- "python_executable": _dsh_os.path.abspath(_dsh_sys.argv[1]),
34
- "marivo_version": _dsh_sys.argv[2],
35
- "package_path": _dsh_os.path.abspath(_dsh_sys.argv[3]),
36
- }
31
+ _dsh_expected = _dsh_import_identity(*_dsh_sys.argv[1:4])
37
32
  if _dsh_actual != _dsh_expected:
38
33
  print(_dsh_json.dumps({"kind": "identity-mismatch", "actual": _dsh_actual}), file=_dsh_sys.stderr)
39
34
  raise SystemExit(78)
40
35
  _dsh_sys.argv = [_dsh_sys.argv[0], *_dsh_sys.argv[4:]]
41
- `.trim();
36
+ `.trim()}`;
42
37
  const IMPORT_IDENTITY_PROGRAM = String.raw `
43
38
  print(_dsh_json.dumps(_dsh_actual, sort_keys=True))
44
39
  `.trim();
@@ -137,7 +132,7 @@ async function resolvePythonExecutable(projectRoot, configured) {
137
132
  ? path.join(projectRoot, process.platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
138
133
  : path.normalize(configured);
139
134
  await assertExecutable(executable);
140
- return executable;
135
+ return canonicalPythonExecutable(executable);
141
136
  }
142
137
  function fingerprint(binding) {
143
138
  const payload = [
@@ -232,11 +227,11 @@ export class MarivoEnvironment {
232
227
  });
233
228
  }
234
229
  try {
235
- const identity = parseImportIdentity(result.stdout);
236
- if (normalizeAbsolute(identity.pythonExecutable) !==
237
- normalizeAbsolute(this.binding.pythonExecutable) ||
238
- identity.marivoVersion !== this.binding.marivoVersion ||
239
- normalizeAbsolute(identity.packagePath) !== normalizeAbsolute(this.binding.packagePath))
230
+ const identity = canonicalImportIdentity(parseImportIdentity(result.stdout));
231
+ const expected = this.binding;
232
+ if (identity.pythonExecutable !== expected.pythonExecutable ||
233
+ identity.marivoVersion !== expected.marivoVersion ||
234
+ identity.packagePath !== expected.packagePath)
240
235
  throw new Error('identity values differ from binding');
241
236
  return identity;
242
237
  }
@@ -256,9 +251,7 @@ function createRuntimeRunner(projectRoot, identity, environment) {
256
251
  const subprocessPolicy = new FixedSubprocessPolicy(projectRoot, environment);
257
252
  const partialBinding = {
258
253
  projectRoot,
259
- pythonExecutable: identity.pythonExecutable,
260
- marivoVersion: identity.marivoVersion,
261
- packagePath: identity.packagePath,
254
+ ...canonicalImportIdentity(identity),
262
255
  subprocessPolicyId: subprocessPolicy.id,
263
256
  };
264
257
  return new MarivoEnvironment({ ...partialBinding, fingerprint: fingerprint(partialBinding) }, subprocessPolicy);
@@ -269,7 +262,7 @@ export async function bindSharedMarivoWorkspace(projectRoot, runtime) {
269
262
  const runner = createRuntimeRunner(projectRoot, {
270
263
  pythonExecutable,
271
264
  marivoVersion: runtime.marivoVersion,
272
- packagePath: normalizeAbsolute(runtime.packagePath),
265
+ packagePath: runtime.packagePath,
273
266
  });
274
267
  const result = await runner.runChecked({
275
268
  program: PROJECT_CONFIG_PROGRAM,
@@ -318,7 +311,7 @@ export async function bindMarivoEnvironment(config, options = {}) {
318
311
  projectRoot,
319
312
  pythonExecutable,
320
313
  marivoVersion: report.marivo.version,
321
- packagePath: normalizeAbsolute(report.marivo.package_path),
314
+ packagePath: await realpath(report.marivo.package_path),
322
315
  subprocessPolicyId: subprocessPolicy.id,
323
316
  };
324
317
  const binding = {
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { MarivoEnvironmentError } from "./errors.js";
3
+ import { canonicalPythonExecutable } from "./identity.js";
3
4
  const DOCTOR_STATUSES = new Set(['ok', 'info', 'warning', 'fail', 'skipped']);
4
5
  const OVERALL_STATUSES = new Set(['ok', 'warning', 'fail']);
5
6
  function isRecord(value) {
@@ -99,7 +100,13 @@ export function admitDoctorReport(report, expectedProjectRoot, expectedPythonExe
99
100
  if (normalizeAbsolute(report.project_root) !== normalizeAbsolute(expectedProjectRoot)) {
100
101
  admissionFailure('project_root', `reported ${report.project_root}`);
101
102
  }
102
- if (normalizeAbsolute(report.python_executable) !== normalizeAbsolute(expectedPythonExecutable)) {
103
+ try {
104
+ if (canonicalPythonExecutable(report.python_executable) !==
105
+ canonicalPythonExecutable(expectedPythonExecutable)) {
106
+ admissionFailure('python_executable', `reported ${report.python_executable}`);
107
+ }
108
+ }
109
+ catch {
103
110
  admissionFailure('python_executable', `reported ${report.python_executable}`);
104
111
  }
105
112
  if (report.marivo.version.length === 0)
@@ -0,0 +1,32 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Resolve directory aliases without following a venv's Python link into its base installation. */
4
+ export function canonicalPythonExecutable(executable) {
5
+ const absolute = path.resolve(executable);
6
+ return path.join(realpathSync(path.dirname(absolute)), path.basename(absolute));
7
+ }
8
+ export function canonicalImportIdentity(identity) {
9
+ return {
10
+ pythonExecutable: canonicalPythonExecutable(identity.pythonExecutable),
11
+ marivoVersion: identity.marivoVersion,
12
+ packagePath: realpathSync(identity.packagePath),
13
+ };
14
+ }
15
+ /** The Python equivalent of canonicalImportIdentity, shared by every execution boundary. */
16
+ export const PYTHON_IMPORT_IDENTITY = String.raw `
17
+ import os.path as _dsh_identity_path
18
+
19
+ def _dsh_python_executable(executable):
20
+ absolute = _dsh_identity_path.abspath(executable)
21
+ return _dsh_identity_path.join(
22
+ _dsh_identity_path.realpath(_dsh_identity_path.dirname(absolute)),
23
+ _dsh_identity_path.basename(absolute),
24
+ )
25
+
26
+ def _dsh_import_identity(executable, version, package_path):
27
+ return {
28
+ "python_executable": _dsh_python_executable(executable),
29
+ "marivo_version": version,
30
+ "package_path": _dsh_identity_path.realpath(package_path),
31
+ }
32
+ `.trim();
@@ -1,6 +1,7 @@
1
1
  import { lstat, realpath } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { MarivoEnvironmentError } from "./errors.js";
4
+ import { PYTHON_IMPORT_IDENTITY } from "./identity.js";
4
5
  const locks = new Map();
5
6
  const limits = { timeoutMs: 30_000, stdoutMaxBytes: 1_048_576, stderrMaxBytes: 65_536 };
6
7
  const cli = String.raw `
@@ -9,19 +10,18 @@ sys.argv = ["marivo", *sys.argv[1:]]
9
10
  runpy.run_module("marivo", run_name="__main__")
10
11
  `.trim();
11
12
  /** The Host Shell owns policy/cancellation; this program only invokes the public CLI. */
12
- export const MARIVO_PROJECT_INIT_PROGRAM = String.raw `
13
+ export const MARIVO_PROJECT_INIT_PROGRAM = `${PYTHON_IMPORT_IDENTITY}\n${String.raw `
13
14
  import json, os, runpy, sys
14
15
  from pathlib import Path
15
16
  import marivo
16
17
  expected = json.load(sys.stdin)
17
- if (os.path.abspath(sys.executable) != os.path.abspath(expected["pythonExecutable"]) or
18
- marivo.__version__ != expected["marivoVersion"] or
19
- os.path.abspath(marivo.__file__ or "") != os.path.abspath(expected["packagePath"]) or
18
+ if (_dsh_import_identity(sys.executable, marivo.__version__, marivo.__file__) !=
19
+ _dsh_import_identity(expected["pythonExecutable"], expected["marivoVersion"], expected["packagePath"]) or
20
20
  str(Path.cwd().resolve()) != expected["projectRoot"]):
21
21
  raise RuntimeError("Marivo project preparation identity changed; rebind required")
22
22
  sys.argv = ["marivo", "init", "--project-root", expected["projectRoot"]]
23
23
  runpy.run_module("marivo", run_name="__main__")
24
- `.trim();
24
+ `.trim()}`;
25
25
  async function manifestPresent(root) {
26
26
  try {
27
27
  const info = await lstat(path.join(root, 'marivo.toml'));
@@ -6,6 +6,7 @@ import process from 'node:process';
6
6
  import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
7
7
  import { MARIVO_PACKAGE_SPEC, MARIVO_VERSION, RUNTIME_INSTALLATION_VERSION, } from "../compatibility.js";
8
8
  import { MarivoEnvironmentError } from "./errors.js";
9
+ import { canonicalPythonExecutable, PYTHON_IMPORT_IDENTITY } from "./identity.js";
9
10
  import { FixedSubprocessPolicy } from "./subprocess.js";
10
11
  export const SHARED_PYTHON_SPEC = '3.10';
11
12
  const PINNED_MARIVO_VERSION = MARIVO_VERSION;
@@ -15,7 +16,7 @@ const INSTALLATION_SCHEMA = RUNTIME_INSTALLATION_VERSION;
15
16
  const INSTALLATION_FILENAME = 'installation.json';
16
17
  const SKILL_NAMES = ['marivo-analysis', 'marivo-semantic'];
17
18
  const SERVICE_SKILL = 'marivo-service';
18
- const PROBE_SCRIPT = String.raw `
19
+ const PROBE_SCRIPT = `${PYTHON_IMPORT_IDENTITY}\n${String.raw `
19
20
  import json
20
21
  import os
21
22
  import sys
@@ -32,22 +33,20 @@ print(json.dumps({
32
33
  "marivo_distribution_version": dist.version,
33
34
  "marivo_index_install": dist.read_text("direct_url.json") is None,
34
35
  "marivo_import_identity": os.path.realpath(marivo.__file__) == os.path.realpath(dist.locate_file("marivo/__init__.py")),
35
- "python_executable": os.path.abspath(sys.executable),
36
- "marivo_version": marivo.__version__,
37
- "package_path": os.path.abspath(marivo.__file__ or ""),
36
+ **_dsh_import_identity(sys.executable, marivo.__version__, marivo.__file__),
38
37
  }, sort_keys=True))
39
- `.trim();
40
- const PYTHON_VERSION_SCRIPT = String.raw `
38
+ `.trim()}`;
39
+ const PYTHON_VERSION_SCRIPT = `${PYTHON_IMPORT_IDENTITY}\n${String.raw `
41
40
  import json
42
41
  import os
43
42
  import sys
44
43
 
45
44
  print(json.dumps({
46
- "python_executable": os.path.abspath(sys.executable),
45
+ "python_executable": _dsh_python_executable(sys.executable),
47
46
  "version": list(sys.version_info[:3]),
48
47
  "prefix": os.path.abspath(sys.prefix),
49
48
  }, sort_keys=True))
50
- `.trim();
49
+ `.trim()}`;
51
50
  function administratorRepair(executable) {
52
51
  const commands = [
53
52
  {
@@ -100,7 +99,7 @@ async function assertExecutable(executable) {
100
99
  if (!info.isFile())
101
100
  throw new Error('not a file');
102
101
  await access(selected, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
103
- return selected;
102
+ return canonicalPythonExecutable(selected);
104
103
  }
105
104
  catch (cause) {
106
105
  throw new MarivoEnvironmentError('python-unavailable', `Shared Marivo Python executable is missing or not executable: ${selected}`, { pythonExecutable: selected }, { cause });
@@ -149,8 +148,8 @@ async function probeRuntime(runtimeRoot, executable, environment, timeoutMs, adm
149
148
  probe.package_path.length === 0) {
150
149
  throw new MarivoEnvironmentError('shared-runtime-identity-mismatch', 'Shared Runtime returned an incomplete or mismatched package identity', { probe, ...(repair === undefined ? {} : { repairCommands: repair.commands }) });
151
150
  }
152
- const actualPython = await realpath(path.resolve(probe.python_executable));
153
- const selectedPython = await realpath(canonical);
151
+ const actualPython = canonicalPythonExecutable(probe.python_executable);
152
+ const selectedPython = canonicalPythonExecutable(canonical);
154
153
  if (expectedMarivoVersion !== undefined && probe.marivo_version !== expectedMarivoVersion) {
155
154
  throw new MarivoEnvironmentError('shared-runtime-version-unsupported', `Shared Marivo runtime version ${probe.marivo_version} is unsupported; install ${expectedMarivoVersion}.${repair === undefined ? '' : ` ${repair.message}`}`, {
156
155
  supportedMarivoVersion: expectedMarivoVersion,
@@ -172,7 +171,7 @@ async function probeRuntime(runtimeRoot, executable, environment, timeoutMs, adm
172
171
  marivo_import_identity: true,
173
172
  python_executable: canonical,
174
173
  marivo_version: probe.marivo_version,
175
- package_path: path.resolve(probe.package_path),
174
+ package_path: await realpath(probe.package_path),
176
175
  };
177
176
  }
178
177
  async function availableSkills(skillsRoot) {
@@ -301,14 +300,14 @@ async function validatedExisting(runtimeRoot, configuredPython, environment, tim
301
300
  return undefined;
302
301
  const expectedPython = configuredPython ?? venvPython(runtimeRoot);
303
302
  const expectedSkillsRoot = path.join(runtimeRoot, 'skills');
304
- if (path.normalize(record.skillsRoot) !== path.normalize(expectedSkillsRoot))
305
- return undefined;
306
303
  try {
304
+ if ((await realpath(record.skillsRoot)) !== (await realpath(expectedSkillsRoot)))
305
+ return undefined;
307
306
  const selectedPython = await assertExecutable(expectedPython);
308
- if (path.normalize(record.pythonExecutable) !== selectedPython)
307
+ if (canonicalPythonExecutable(record.pythonExecutable) !== selectedPython)
309
308
  return undefined;
310
309
  const probe = await probeRuntime(runtimeRoot, selectedPython, environment, timeoutMs, configuredPython !== undefined, record.marivoVersion);
311
- if (path.normalize(probe.package_path) !== path.normalize(record.packagePath))
310
+ if (probe.package_path !== (await realpath(record.packagePath)))
312
311
  return undefined;
313
312
  const sourceRoot = path.join(path.dirname(probe.package_path), 'skills');
314
313
  await validateSkills(record.skillsRoot, await availableSkills(sourceRoot));
@@ -11,6 +11,7 @@ import { SavedWriteProofs } from "./next-steps/saved-writes.js";
11
11
  import { AnalysisDrafts } from "./presentation/drafts.js";
12
12
  import { installMarivoPresentationCodeDelivery, registerMarivoPresentTool, registerMarivoReportDeleteTool, registerMarivoUiReadTool, } from "./presentation/index.js";
13
13
  import { restorePythonResults } from "./python-results-context.js";
14
+ import { registerReportPublishTool } from "./report-publishing/adapters.js";
14
15
  import { resolvePresentationWorkspace } from "./workspace-identity.js";
15
16
  const ROUTING_PROMPT = [
16
17
  'For attachment or Workspace data files, load dsh-data-analysis-files. For governed analysis, load marivo-analysis; load marivo-semantic when shared definitions are needed or authoring is requested. For project HTTP services, load marivo-service when the bound Runtime provides it.',
@@ -92,6 +93,8 @@ export function createMarivoAgentInstallation(ctx, environmentOrResolver, option
92
93
  controller.addDisposer(() => credentialService.disposeAgent(agent));
93
94
  controller.addDisposer(registerMarivoUiReadTool(agent.ctx, workspace, presentationDrafts));
94
95
  controller.addDisposer(registerMarivoReportDeleteTool(agent.ctx, workspace));
96
+ if (options.reportPublishing)
97
+ controller.addDisposer(registerReportPublishTool(agent.ctx, options.reportPublishing, agent.session));
95
98
  controller.addDisposer(registerMarivoPresentTool(agent.ctx, workspace, agent.session, presentationDrafts, async () => resolveMarivoEnvironmentSource(source), savedWrites.committed));
96
99
  controller.addDisposer(installMarivoPresentationCodeDelivery(agent.ctx));
97
100
  controller.addDisposer(installNextStepsAgent(ctx, agent, workspace, options.nextStepsPublish ?? (() => { }), savedWrites));
@@ -99,7 +102,9 @@ export function createMarivoAgentInstallation(ctx, environmentOrResolver, option
99
102
  controller.addDisposer(agent.ctx.systemPrompt.section({
100
103
  name: 'marivo:routing',
101
104
  order: 180,
102
- text: ROUTING_PROMPT,
105
+ text: options.reportPublishing
106
+ ? `${ROUTING_PROMPT}\nThis Host has object-storage publishing enabled. Use marivo_publish_report with the exact saved report ref only when the user explicitly requests publication to object storage.`
107
+ : ROUTING_PROMPT,
103
108
  }));
104
109
  controller.addDisposer(agent.ctx.on('agent/pre-step', async ({ signal }, next) => {
105
110
  const decision = await next();
package/lib/plugin.js CHANGED
@@ -24,6 +24,9 @@ import { MarivoHelpBridge } from "./disclosure/bridge.js";
24
24
  import { createSharedMarivoRuntimeRunner, DEFAULT_SHARED_RUNTIME_INSTALL_TIMEOUT_MS, ensureSharedMarivoRuntime, MarivoWorkspaceEnvironmentManager, } from "./environment/index.js";
25
25
  import { AnalysisDrafts } from "./presentation/drafts.js";
26
26
  import { MarivoPresentationFileService, registerMarivoPresentationRpc, } from "./presentation/index.js";
27
+ import { registerPublishingRpc } from "./report-publishing/adapters.js";
28
+ import { resolvePublishingConfig } from "./report-publishing/config.js";
29
+ import { ReportPublishingService } from "./report-publishing/service.js";
27
30
  import { browserFailure, SemanticBrowserService } from "./semantic-browser/service.js";
28
31
  import { registerSemanticReferenceRpc, SemanticReferenceService, SemanticReferenceUsage, } from "./semantic-reference/index.js";
29
32
  import { preparePackagedSkillSnapshot } from "./skill-build.js";
@@ -44,6 +47,8 @@ export const inject = [
44
47
  ];
45
48
  /** Cordis loader schema. Runtime defaults are resolved in {@link apply}. */
46
49
  export const Config = z.object({
50
+ // Validate at apply time so loader errors cannot echo supplied values.
51
+ reportPublishing: z.any(),
47
52
  pythonTimeoutMs: z
48
53
  .number()
49
54
  .min(1)
@@ -71,6 +76,7 @@ export async function apply(ctx, config = {}) {
71
76
  }
72
77
  /** Compose an extension at the public seam while retaining the ordinary plugin behavior. */
73
78
  export async function applyWithIntegration(ctx, config = {}, integration = {}) {
79
+ const publishingConfig = resolvePublishingConfig(config.reportPublishing);
74
80
  const pythonOptions = currentPythonOptions(config);
75
81
  const pythonExecutable = config.pythonExecutable ?? process.env.DSH_DATA_ANALYSIS_PYTHON;
76
82
  const runtimeRoot = config.runtimeRoot ?? process.env.DSH_DATA_ANALYSIS_RUNTIME_ROOT;
@@ -105,6 +111,7 @@ export async function applyWithIntegration(ctx, config = {}, integration = {}) {
105
111
  let disposeCredentials;
106
112
  let disposeReferences;
107
113
  let disposePresentation;
114
+ let disposePublishing;
108
115
  let referenceService;
109
116
  let agentInstallation;
110
117
  let pythonSettings;
@@ -123,6 +130,7 @@ export async function applyWithIntegration(ctx, config = {}, integration = {}) {
123
130
  () => pythonSettings?.dispose(),
124
131
  () => disposeCredentials?.(),
125
132
  disposeChanges,
133
+ () => disposePublishing?.(),
126
134
  () => disposePresentation?.(),
127
135
  () => disposeReferences?.(),
128
136
  () => browserService.close(),
@@ -165,12 +173,22 @@ export async function applyWithIntegration(ctx, config = {}, integration = {}) {
165
173
  return workspace ? { id: String(workspace.id), path: workspace.path } : undefined;
166
174
  }, presentationDrafts);
167
175
  disposePresentation = registerMarivoPresentationRpc(ctx.connection, presentationFiles);
176
+ const reportPublishing = publishingConfig
177
+ ? new ReportPublishingService(publishingConfig, ctx.credentials, presentationFiles, (id) => {
178
+ const workspace = ctx.workspaceRegistry.get(WorkspaceId(id));
179
+ if (!workspace)
180
+ throw new Error('workspace-unavailable');
181
+ return workspace.title;
182
+ })
183
+ : undefined;
184
+ disposePublishing = registerPublishingRpc(ctx.connection, reportPublishing, (id) => Boolean(ctx.workspaceRegistry.get(WorkspaceId(id))));
168
185
  agentInstallation = createMarivoAgentInstallation(ctx, resolveEnvironment, {
169
186
  ...pythonOptions,
170
187
  pythonOptionsSource: pythonSettings.get,
171
188
  helpBridgeSource: helpBridge,
172
189
  credentialService,
173
190
  presentationDrafts,
191
+ ...(reportPublishing ? { reportPublishing } : {}),
174
192
  nextStepsPublish: (notice) => nextSteps.publish(notice),
175
193
  });
176
194
  agentInstallation.install();
@@ -389,6 +389,25 @@ export class MarivoPresentationFileService {
389
389
  await check();
390
390
  return receipt;
391
391
  }
392
+ readSavedReport(scope, ref, caller) {
393
+ return this.#tasks.track(this.#readSavedReport(scope, ref, caller));
394
+ }
395
+ async #readSavedReport(scope, ref, caller) {
396
+ const signal = AbortSignal.any([caller, this.#abort.signal]);
397
+ signal.throwIfAborted();
398
+ const workspace = await this.#workspace(scope);
399
+ if (!workspace)
400
+ throw new Error('workspace-unavailable');
401
+ if (ref.workspaceId !== workspace.id)
402
+ throw new Error('report-workspace-mismatch');
403
+ const root = path.resolve(workspace.path);
404
+ const document = await readAnalysisRecord(root, workspace.id, ref.reportId, ref.buildId, signal);
405
+ const current = await this.#workspace(scope);
406
+ if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
407
+ throw new Error('workspace-changed');
408
+ signal.throwIfAborted();
409
+ return document;
410
+ }
392
411
  read(payload, caller = new AbortController().signal) {
393
412
  return this.#tasks.track(this.#read(payload, caller));
394
413
  }
@@ -0,0 +1,136 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { z } from 'zod';
3
+ import { parseReportRef } from "../presentation/contracts/report-ref.js";
4
+ import { registerPluginRpc } from "../rpc.js";
5
+ import { registerMarivoTool } from "../tool-lifecycle.js";
6
+ import { publishingFieldSchema, REPORT_PUBLISHING_CHANNEL } from "./credential-contract.js";
7
+ const workspace = z.object({ workspaceId: z.string().min(1).max(512) });
8
+ const describe = workspace.strict();
9
+ const mutation = workspace.extend({ configId: z.string().uuid() });
10
+ const field = publishingFieldSchema;
11
+ const set = mutation.extend({ field, value: z.string().min(1).max(65_536) }).strict();
12
+ const unset = mutation.extend({ field }).strict();
13
+ const refInput = { ref: z.string().min(1).max(4096) };
14
+ const publish = mutation.extend(refInput).strict();
15
+ const toolInput = z.strictObject(refInput);
16
+ function parseSavedRef(value) {
17
+ const ref = parseReportRef(value);
18
+ if (ref.kind !== 'saved')
19
+ throw new Error('report-saved-ref-required');
20
+ return ref;
21
+ }
22
+ export function registerPublishingRpc(connection, service, workspaceExists) {
23
+ return registerPluginRpc(connection, REPORT_PUBLISHING_CHANNEL, ['describe', 'set', 'unset', 'publish'], async (endpoint, payload, signal) => {
24
+ try {
25
+ signal.throwIfAborted();
26
+ const { workspaceId } = workspace.parse(payload);
27
+ if (!workspaceExists(workspaceId))
28
+ throw new Error('workspace-unavailable');
29
+ if (endpoint === 'describe') {
30
+ describe.parse(payload);
31
+ return {
32
+ ok: true,
33
+ value: service ? await service.describe() : { enabled: false, fields: [] },
34
+ };
35
+ }
36
+ if (!service)
37
+ throw new Error('report-publishing-disabled');
38
+ service.assertConfig(mutation.parse(payload).configId);
39
+ if (endpoint === 'publish') {
40
+ const input = publish.parse(payload);
41
+ return {
42
+ ok: true,
43
+ value: await service.publish({ workspaceId }, parseSavedRef(input.ref), signal),
44
+ };
45
+ }
46
+ if (endpoint === 'set') {
47
+ const input = set.parse(payload);
48
+ await service.change(input.field, input.value, signal);
49
+ }
50
+ else {
51
+ await service.change(unset.parse(payload).field, undefined, signal);
52
+ }
53
+ return { ok: true, value: await service.describe() };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ ok: false,
58
+ error: { code: 'internal', message: publishingError(error, signal), details: {} },
59
+ };
60
+ }
61
+ });
62
+ }
63
+ export function registerReportPublishTool(ctx, service, session) {
64
+ return registerMarivoTool(ctx, defineTool({
65
+ name: 'marivo_publish_report',
66
+ description: 'Publish one exact saved report version as self-contained HTML to the configured object storage only when the user explicitly asks to publish it. Pass the saved ref from marivo_report_write or marivo_report_read. The HTML includes the saved data snapshots. Returns the URL and upload receipt. An unconfirmed upload may already exist. If credentials are missing, direct the user to the Credential management tab of Datasources and credentials.',
67
+ parameters: {
68
+ ref: {
69
+ type: 'string',
70
+ required: true,
71
+ description: 'Exact saved report ref.',
72
+ },
73
+ },
74
+ output: {
75
+ schema: {
76
+ type: 'object',
77
+ properties: {
78
+ url: { type: 'string', required: true },
79
+ key: { type: 'string', required: true },
80
+ storage: { type: 'string', required: true },
81
+ bucket: { type: 'string', required: true },
82
+ workspaceId: { type: 'string', required: true },
83
+ reportId: { type: 'string', required: true },
84
+ buildId: { type: 'string', required: true },
85
+ sha256: { type: 'string', required: true },
86
+ bytes: { type: 'integer', required: true },
87
+ publishedAt: { type: 'string', required: true },
88
+ },
89
+ additionalProperties: false,
90
+ },
91
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
92
+ },
93
+ timeoutMs: 120_000,
94
+ async execute(args, exec) {
95
+ if (exec.agent?.session !== session)
96
+ throw new Error('presentation-session-mismatch');
97
+ try {
98
+ const input = toolInput.parse(args);
99
+ return await service.publish({ sessionId: String(session.id) }, parseSavedRef(input.ref), exec.signal);
100
+ }
101
+ catch (error) {
102
+ throw new Error(publishingError(error, exec.signal));
103
+ }
104
+ },
105
+ }));
106
+ }
107
+ function publishingError(error, signal) {
108
+ if (error instanceof Error && error.message === 'report-publishing-upload-unconfirmed')
109
+ return error.message;
110
+ if (signal.aborted)
111
+ return 'cancelled';
112
+ if (error instanceof z.ZodError)
113
+ return 'invalid-request';
114
+ if (error instanceof Error) {
115
+ if (error.message.startsWith('report-ref-invalid:'))
116
+ return 'report-ref-invalid';
117
+ if (error.message === 'report-build-not-found' || ('code' in error && error.code === 'ENOENT'))
118
+ return 'report-publishing-build-unavailable';
119
+ if (error.name === 'TimeoutError')
120
+ return 'report-publishing-timeout';
121
+ if ([
122
+ 'workspace-unavailable',
123
+ 'workspace-changed',
124
+ 'report-version-unsupported',
125
+ 'report-document-incompatible',
126
+ 'report-saved-ref-required',
127
+ 'report-workspace-mismatch',
128
+ 'report-publishing-config-changed',
129
+ 'report-publishing-disabled',
130
+ 'report-publishing-disabled-or-field-unavailable',
131
+ 'report-publishing-credentials-missing',
132
+ ].includes(error.message))
133
+ return error.message;
134
+ }
135
+ return 'report-publishing-operation-failed';
136
+ }
@@ -0,0 +1,58 @@
1
+ import { isCredentialRefName } from '@deepseek-ai/dsh-credentials';
2
+ import { z } from 'zod';
3
+ const reference = z.string().refine(isCredentialRefName);
4
+ const url = z
5
+ .string()
6
+ .trim()
7
+ .url()
8
+ .refine((value) => {
9
+ const parsed = URL.parse(value);
10
+ return (parsed !== null &&
11
+ (parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
12
+ !parsed.username &&
13
+ !parsed.password &&
14
+ !parsed.search &&
15
+ !parsed.hash);
16
+ });
17
+ const enabledPublishing = z
18
+ .strictObject({
19
+ enabled: z.literal(true),
20
+ storage: z.strictObject({
21
+ name: z.string().min(1).max(100),
22
+ protocol: z.literal('s3').default('s3'),
23
+ endpoint: url,
24
+ region: z.string().min(1),
25
+ bucket: z
26
+ .string()
27
+ .min(1)
28
+ .max(255)
29
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/),
30
+ forcePathStyle: z.boolean().default(false),
31
+ accessKeyIdRef: reference,
32
+ secretAccessKeyRef: reference,
33
+ sessionTokenRef: reference.optional(),
34
+ }),
35
+ pathPrefix: z
36
+ .string()
37
+ .default('reports')
38
+ .refine((value) => value === '' ||
39
+ value.split('/').every((part) => /^[a-zA-Z0-9_-][a-zA-Z0-9._-]*$/.test(part))),
40
+ publicBaseUrl: url,
41
+ })
42
+ .refine(({ storage }) => {
43
+ const refs = [storage.accessKeyIdRef, storage.secretAccessKeyRef, storage.sessionTokenRef];
44
+ return new Set(refs).size === refs.length;
45
+ });
46
+ export function resolvePublishingConfig(value) {
47
+ if (value === undefined)
48
+ return undefined;
49
+ if (typeof value === 'object' &&
50
+ value !== null &&
51
+ !Array.isArray(value) &&
52
+ (!('enabled' in value) || value.enabled === undefined || value.enabled === false))
53
+ return undefined;
54
+ const result = enabledPublishing.safeParse(value);
55
+ if (!result.success)
56
+ throw new Error('report-publishing-config-invalid');
57
+ return result.data;
58
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ export const REPORT_PUBLISHING_CHANNEL = '/dsh-report-publishing';
3
+ export const publishingFields = ['accessKeyId', 'secretAccessKey', 'sessionToken'];
4
+ export const publishingFieldSchema = z.enum(publishingFields);
5
+ const publishingCredentialViewSchema = z.discriminatedUnion('enabled', [
6
+ z.strictObject({ enabled: z.literal(false), fields: z.tuple([]) }),
7
+ z.strictObject({
8
+ enabled: z.literal(true),
9
+ configId: z.string().uuid(),
10
+ name: z.string(),
11
+ bucket: z.string(),
12
+ fields: z.array(z.strictObject({
13
+ field: publishingFieldSchema,
14
+ reference: z.string(),
15
+ required: z.boolean(),
16
+ configured: z.boolean(),
17
+ writable: z.boolean(),
18
+ source: z.string().optional(),
19
+ })),
20
+ }),
21
+ ]);
22
+ export const publishingCredentialResultSchema = z.discriminatedUnion('ok', [
23
+ z.object({ ok: z.literal(true), value: publishingCredentialViewSchema }),
24
+ z.object({ ok: z.literal(false), error: z.object({ message: z.string() }) }),
25
+ ]);