@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.
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/assets/logo-horizontal.svg +42 -0
- package/assets/logo-mark.svg +20 -0
- package/lib/bin/environment.js +99 -76
- package/lib/client/analysis-ui/offline-runtime.generated.js +1 -1
- package/lib/client/credentials/install.js +42 -38
- package/lib/client/credentials/model.js +9 -0
- package/lib/client/credentials/report-publishing.js +84 -0
- package/lib/client/credentials/styles.js +2 -0
- package/lib/client/i18n/credentials.js +18 -0
- package/lib/client/i18n/presentation.js +24 -0
- package/lib/client/right-tabs/install.js +9 -1
- package/lib/client/right-tabs/report-actions.js +30 -4
- package/lib/client/right-tabs/report-publishing.js +107 -0
- package/lib/client/right-tabs/styles.js +3 -0
- package/lib/client/ui/styles.generated.js +3 -0
- package/lib/client-composition.js +202 -191
- package/lib/client.js +205 -194
- package/lib/datasource/resolver-program.js +4 -4
- package/lib/environment/binding.js +16 -23
- package/lib/environment/doctor.js +8 -1
- package/lib/environment/identity.js +32 -0
- package/lib/environment/project-preparation.js +5 -5
- package/lib/environment/runtime.js +15 -16
- package/lib/plugin-agents.js +6 -1
- package/lib/plugin.js +18 -0
- package/lib/presentation/rpc.js +19 -0
- package/lib/report-publishing/adapters.js +136 -0
- package/lib/report-publishing/config.js +58 -0
- package/lib/report-publishing/credential-contract.js +25 -0
- package/lib/report-publishing/service.js +133 -0
- package/lib/skill-build.generated.js +2 -2
- package/lib/types/client/credentials/install.d.ts +2 -1
- package/lib/types/client/credentials/model.d.ts +30 -0
- package/lib/types/client/credentials/report-publishing.d.ts +5 -0
- package/lib/types/client/credentials/styles.d.ts +1 -1
- package/lib/types/client/i18n/copy.d.ts +21 -0
- package/lib/types/client/i18n/credentials.d.ts +18 -0
- package/lib/types/client/i18n/presentation.d.ts +24 -0
- package/lib/types/client/right-tabs/report-actions.d.ts +5 -1
- package/lib/types/client/right-tabs/report-publishing.d.ts +21 -0
- package/lib/types/client/right-tabs/styles.d.ts +1 -1
- package/lib/types/environment/identity.d.ts +6 -0
- package/lib/types/plugin-agents.d.ts +2 -0
- package/lib/types/plugin.d.ts +4 -0
- package/lib/types/presentation/rpc.d.ts +1358 -0
- package/lib/types/report-publishing/adapters.d.ts +6 -0
- package/lib/types/report-publishing/config.d.ts +23 -0
- package/lib/types/report-publishing/credential-contract.d.ts +61 -0
- package/lib/types/report-publishing/service.d.ts +31 -0
- package/package.json +4 -1
- 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 (
|
|
50
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
identity.marivoVersion !==
|
|
239
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
18
|
-
|
|
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
|
-
|
|
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":
|
|
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 =
|
|
153
|
-
const selectedPython =
|
|
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:
|
|
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 (
|
|
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 (
|
|
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));
|
package/lib/plugin-agents.js
CHANGED
|
@@ -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:
|
|
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();
|
package/lib/presentation/rpc.js
CHANGED
|
@@ -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
|
+
]);
|