@chengxianglibra/dsh-data-analysis 0.1.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/LICENSE +21 -0
- package/README.md +72 -0
- package/cordis.patch.yml +11 -0
- package/lib/bin/environment.js +91 -0
- package/lib/bridges.js +11 -0
- package/lib/client.js +744 -0
- package/lib/compatibility.js +66 -0
- package/lib/datasource/access.js +60 -0
- package/lib/datasource/bridge-programs.js +80 -0
- package/lib/datasource/bridge.js +204 -0
- package/lib/datasource/credentials.js +24 -0
- package/lib/datasource/index.js +4 -0
- package/lib/datasource/shell-env.js +386 -0
- package/lib/datasource/test.js +65 -0
- package/lib/disclosure/activation.js +369 -0
- package/lib/disclosure/bridge-program.js +11 -0
- package/lib/disclosure/bridge.js +65 -0
- package/lib/disclosure/help.js +227 -0
- package/lib/disclosure/index.js +3 -0
- package/lib/environment/binding.js +273 -0
- package/lib/environment/doctor.js +123 -0
- package/lib/environment/errors.js +11 -0
- package/lib/environment/index.js +7 -0
- package/lib/environment/runtime.js +558 -0
- package/lib/environment/source.js +6 -0
- package/lib/environment/subprocess.js +178 -0
- package/lib/environment/summary.js +19 -0
- package/lib/environment/workspace.js +49 -0
- package/lib/evidence/bridge-program.js +153 -0
- package/lib/evidence/bridge.js +166 -0
- package/lib/evidence/index.js +2 -0
- package/lib/evidence/sources.js +263 -0
- package/lib/index.js +7 -0
- package/lib/plugin.js +179 -0
- package/lib/types/bridges.d.ts +11 -0
- package/lib/types/client.d.ts +125 -0
- package/lib/types/compatibility.d.ts +29 -0
- package/lib/types/datasource/access.d.ts +27 -0
- package/lib/types/datasource/bridge.d.ts +53 -0
- package/lib/types/datasource/index.d.ts +8 -0
- package/lib/types/datasource/shell-env.d.ts +38 -0
- package/lib/types/datasource/test.d.ts +32 -0
- package/lib/types/disclosure/activation.d.ts +73 -0
- package/lib/types/disclosure/bridge.d.ts +28 -0
- package/lib/types/disclosure/help.d.ts +64 -0
- package/lib/types/disclosure/index.d.ts +5 -0
- package/lib/types/environment/binding.d.ts +26 -0
- package/lib/types/environment/doctor.d.ts +8 -0
- package/lib/types/environment/errors.d.ts +7 -0
- package/lib/types/environment/index.d.ts +9 -0
- package/lib/types/environment/runtime.d.ts +17 -0
- package/lib/types/environment/source.d.ts +7 -0
- package/lib/types/environment/subprocess.d.ts +19 -0
- package/lib/types/environment/types.d.ts +96 -0
- package/lib/types/environment/workspace.d.ts +10 -0
- package/lib/types/evidence/bridge.d.ts +40 -0
- package/lib/types/evidence/index.d.ts +4 -0
- package/lib/types/evidence/sources.d.ts +57 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/plugin.d.ts +43 -0
- package/package.json +149 -0
- package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-report/SKILL.md +57 -0
- package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +113 -0
- package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +1201 -0
- package/skills/dsh-data-analysis-report/assets/report-data.js +449 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, constants, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { admitDoctorReport, parseDoctorReport } from "./doctor.js";
|
|
6
|
+
import { MarivoEnvironmentError } from "./errors.js";
|
|
7
|
+
import { FixedSubprocessPolicy } from "./subprocess.js";
|
|
8
|
+
const DOCTOR_LIMITS = Object.freeze({
|
|
9
|
+
timeoutMs: 30_000,
|
|
10
|
+
stdoutMaxBytes: 1_048_576,
|
|
11
|
+
stderrMaxBytes: 65_536,
|
|
12
|
+
});
|
|
13
|
+
const IDENTITY_LIMITS = Object.freeze({
|
|
14
|
+
timeoutMs: 10_000,
|
|
15
|
+
stdoutMaxBytes: 16_384,
|
|
16
|
+
stderrMaxBytes: 16_384,
|
|
17
|
+
});
|
|
18
|
+
const CHECKED_IDENTITY_PRELUDE = String.raw `
|
|
19
|
+
import json as _dsh_json
|
|
20
|
+
import os as _dsh_os
|
|
21
|
+
import sys as _dsh_sys
|
|
22
|
+
import marivo as _dsh_marivo
|
|
23
|
+
|
|
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
|
+
}
|
|
29
|
+
_dsh_expected = {
|
|
30
|
+
"python_executable": _dsh_os.path.abspath(_dsh_sys.argv[1]),
|
|
31
|
+
"marivo_version": _dsh_sys.argv[2],
|
|
32
|
+
"package_path": _dsh_os.path.abspath(_dsh_sys.argv[3]),
|
|
33
|
+
}
|
|
34
|
+
if _dsh_actual != _dsh_expected:
|
|
35
|
+
print(_dsh_json.dumps({"kind": "identity-mismatch", "actual": _dsh_actual}), file=_dsh_sys.stderr)
|
|
36
|
+
raise SystemExit(78)
|
|
37
|
+
_dsh_sys.argv = [_dsh_sys.argv[0], *_dsh_sys.argv[4:]]
|
|
38
|
+
`.trim();
|
|
39
|
+
const IMPORT_IDENTITY_PROGRAM = String.raw `
|
|
40
|
+
print(_dsh_json.dumps(_dsh_actual, sort_keys=True))
|
|
41
|
+
`.trim();
|
|
42
|
+
function redactSubprocessOutput(result, environmentOverlay) {
|
|
43
|
+
if (environmentOverlay === undefined)
|
|
44
|
+
return result;
|
|
45
|
+
const secrets = Object.values(environmentOverlay).filter((value) => value !== undefined && value !== '');
|
|
46
|
+
const redactText = (source) => {
|
|
47
|
+
let text = source;
|
|
48
|
+
for (const secret of secrets)
|
|
49
|
+
text = text.split(secret).join('[REDACTED]');
|
|
50
|
+
return text;
|
|
51
|
+
};
|
|
52
|
+
const redactJsonValue = (value) => {
|
|
53
|
+
if (typeof value === 'string')
|
|
54
|
+
return redactText(value);
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return value.map(redactJsonValue);
|
|
57
|
+
if (typeof value !== 'object' || value === null)
|
|
58
|
+
return value;
|
|
59
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactJsonValue(item)]));
|
|
60
|
+
};
|
|
61
|
+
const redactStdout = () => {
|
|
62
|
+
const text = result.stdout.toString('utf8');
|
|
63
|
+
try {
|
|
64
|
+
return Buffer.from(JSON.stringify(redactJsonValue(JSON.parse(text))));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return Buffer.from(redactText(text));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
...result,
|
|
72
|
+
stdout: redactStdout(),
|
|
73
|
+
stderr: Buffer.from(redactText(result.stderr.toString('utf8'))),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeAbsolute(value) {
|
|
77
|
+
return path.normalize(path.resolve(value));
|
|
78
|
+
}
|
|
79
|
+
async function assertProjectRoot(projectRoot) {
|
|
80
|
+
const resolved = normalizeAbsolute(projectRoot);
|
|
81
|
+
try {
|
|
82
|
+
const info = await stat(resolved);
|
|
83
|
+
if (!info.isDirectory())
|
|
84
|
+
throw new Error('not a directory');
|
|
85
|
+
return await realpath(resolved);
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
throw new MarivoEnvironmentError('project-root-invalid', `Marivo project root is not an existing directory: ${resolved}`, { projectRoot: resolved }, { cause });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function assertExecutable(executable) {
|
|
92
|
+
try {
|
|
93
|
+
const info = await stat(executable);
|
|
94
|
+
if (!info.isFile())
|
|
95
|
+
throw new Error('not a file');
|
|
96
|
+
await access(executable, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
throw new MarivoEnvironmentError('python-unavailable', `Marivo Python executable is missing or not executable: ${executable}`, { pythonExecutable: executable }, { cause });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function resolvePythonExecutable(projectRoot, configured) {
|
|
103
|
+
if (configured !== undefined && !path.isAbsolute(configured)) {
|
|
104
|
+
throw new MarivoEnvironmentError('python-path-relative', `Configured Marivo Python executable must be absolute: ${configured}`, { pythonExecutable: configured });
|
|
105
|
+
}
|
|
106
|
+
const executable = configured === undefined
|
|
107
|
+
? path.join(projectRoot, process.platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
|
|
108
|
+
: path.normalize(configured);
|
|
109
|
+
await assertExecutable(executable);
|
|
110
|
+
return executable;
|
|
111
|
+
}
|
|
112
|
+
function fingerprint(binding) {
|
|
113
|
+
const payload = [
|
|
114
|
+
binding.projectRoot,
|
|
115
|
+
binding.pythonExecutable,
|
|
116
|
+
binding.marivoVersion,
|
|
117
|
+
binding.packagePath,
|
|
118
|
+
binding.subprocessPolicyId,
|
|
119
|
+
];
|
|
120
|
+
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
121
|
+
}
|
|
122
|
+
function parseImportIdentity(stdout) {
|
|
123
|
+
try {
|
|
124
|
+
const value = JSON.parse(stdout.toString('utf8'));
|
|
125
|
+
if (typeof value.python_executable !== 'string' ||
|
|
126
|
+
typeof value.marivo_version !== 'string' ||
|
|
127
|
+
typeof value.package_path !== 'string')
|
|
128
|
+
throw new TypeError('identity fields must be strings');
|
|
129
|
+
return {
|
|
130
|
+
pythonExecutable: value.python_executable,
|
|
131
|
+
marivoVersion: value.marivo_version,
|
|
132
|
+
packagePath: value.package_path,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new MarivoEnvironmentError('binding-identity-mismatch', 'Marivo import identity probe returned an invalid payload; explicit rebind is required', { stdoutBytes: stdout.byteLength }, { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** A ready binding and the single frozen subprocess policy that established it. */
|
|
140
|
+
export class MarivoEnvironment {
|
|
141
|
+
binding;
|
|
142
|
+
subprocessPolicy;
|
|
143
|
+
#failed = false;
|
|
144
|
+
constructor(binding, subprocessPolicy) {
|
|
145
|
+
this.binding = Object.freeze({ ...binding });
|
|
146
|
+
this.subprocessPolicy = subprocessPolicy;
|
|
147
|
+
}
|
|
148
|
+
get status() {
|
|
149
|
+
return this.#failed ? 'failed' : 'ready';
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Execute one Python program after checking the bound import identity in the same process.
|
|
153
|
+
* Identity mismatch permanently fails this binding; ordinary program failures do not.
|
|
154
|
+
*/
|
|
155
|
+
async runChecked(request) {
|
|
156
|
+
if (this.#failed) {
|
|
157
|
+
throw new MarivoEnvironmentError('binding-failed', 'Marivo Environment Binding has failed; explicit rebind is required', { fingerprint: this.binding.fingerprint });
|
|
158
|
+
}
|
|
159
|
+
if (request.program.trim() === '')
|
|
160
|
+
throw new TypeError('checked Python program must not be empty');
|
|
161
|
+
const rawResult = await this.subprocessPolicy.run({
|
|
162
|
+
executable: this.binding.pythonExecutable,
|
|
163
|
+
args: [
|
|
164
|
+
'-c',
|
|
165
|
+
`${CHECKED_IDENTITY_PRELUDE}\n${request.program}`,
|
|
166
|
+
this.binding.pythonExecutable,
|
|
167
|
+
this.binding.marivoVersion,
|
|
168
|
+
this.binding.packagePath,
|
|
169
|
+
...(request.args ?? []),
|
|
170
|
+
],
|
|
171
|
+
...(request.environmentOverlay === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { environmentOverlay: request.environmentOverlay }),
|
|
174
|
+
...(request.limits === undefined ? {} : { limits: request.limits }),
|
|
175
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
176
|
+
});
|
|
177
|
+
const result = redactSubprocessOutput(rawResult, request.environmentOverlay);
|
|
178
|
+
if (result.exitCode === 78) {
|
|
179
|
+
this.#failed = true;
|
|
180
|
+
throw new MarivoEnvironmentError('binding-identity-mismatch', 'Marivo import identity changed; explicit rebind is required', {
|
|
181
|
+
fingerprint: this.binding.fingerprint,
|
|
182
|
+
exitCode: result.exitCode,
|
|
183
|
+
stderr: result.stderr.toString('utf8').slice(0, 2_000),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
/** Recheck and return the exact bound import identity. */
|
|
189
|
+
async assertImportIdentity(signal) {
|
|
190
|
+
const result = await this.runChecked({
|
|
191
|
+
program: IMPORT_IDENTITY_PROGRAM,
|
|
192
|
+
limits: IDENTITY_LIMITS,
|
|
193
|
+
signal,
|
|
194
|
+
});
|
|
195
|
+
if (result.exitCode !== 0) {
|
|
196
|
+
this.#failed = true;
|
|
197
|
+
throw new MarivoEnvironmentError('binding-identity-mismatch', 'Marivo import identity changed; explicit rebind is required', {
|
|
198
|
+
fingerprint: this.binding.fingerprint,
|
|
199
|
+
exitCode: result.exitCode,
|
|
200
|
+
stderr: result.stderr.toString('utf8').slice(0, 2_000),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const identity = parseImportIdentity(result.stdout);
|
|
205
|
+
if (normalizeAbsolute(identity.pythonExecutable) !==
|
|
206
|
+
normalizeAbsolute(this.binding.pythonExecutable) ||
|
|
207
|
+
identity.marivoVersion !== this.binding.marivoVersion ||
|
|
208
|
+
normalizeAbsolute(identity.packagePath) !== normalizeAbsolute(this.binding.packagePath))
|
|
209
|
+
throw new Error('identity values differ from binding');
|
|
210
|
+
return identity;
|
|
211
|
+
}
|
|
212
|
+
catch (cause) {
|
|
213
|
+
this.#failed = true;
|
|
214
|
+
if (cause instanceof MarivoEnvironmentError)
|
|
215
|
+
throw cause;
|
|
216
|
+
throw new MarivoEnvironmentError('binding-identity-mismatch', 'Marivo import identity changed; explicit rebind is required', { fingerprint: this.binding.fingerprint }, { cause });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Build an identity-checked Help runner directly from an admitted shared Runtime. */
|
|
221
|
+
export function createSharedMarivoRuntimeRunner(runtime, options = {}) {
|
|
222
|
+
const subprocessPolicy = new FixedSubprocessPolicy(runtime.runtimeRoot, options.environment);
|
|
223
|
+
const partialBinding = {
|
|
224
|
+
projectRoot: runtime.runtimeRoot,
|
|
225
|
+
pythonExecutable: runtime.pythonExecutable,
|
|
226
|
+
marivoVersion: runtime.marivoVersion,
|
|
227
|
+
packagePath: runtime.packagePath,
|
|
228
|
+
subprocessPolicyId: subprocessPolicy.id,
|
|
229
|
+
};
|
|
230
|
+
return new MarivoEnvironment({ ...partialBinding, fingerprint: fingerprint(partialBinding) }, subprocessPolicy);
|
|
231
|
+
}
|
|
232
|
+
/** Resolve, probe, and establish one Marivo Environment Binding. */
|
|
233
|
+
export async function bindMarivoEnvironment(config, options = {}) {
|
|
234
|
+
const projectRoot = await assertProjectRoot(config.projectRoot);
|
|
235
|
+
const pythonExecutable = await resolvePythonExecutable(projectRoot, config.pythonExecutable);
|
|
236
|
+
const subprocessPolicy = new FixedSubprocessPolicy(projectRoot, options.environment);
|
|
237
|
+
const result = await subprocessPolicy.run({
|
|
238
|
+
executable: pythonExecutable,
|
|
239
|
+
args: ['-m', 'marivo', 'doctor', '--project-root', projectRoot, '--format', 'json'],
|
|
240
|
+
// Admission is observational. Keep Marivo's default telemetry from creating
|
|
241
|
+
// .marivo/telemetry merely because DSH resolved a Workspace binding.
|
|
242
|
+
environmentOverlay: { MARIVO_TELEMETRY: 'off' },
|
|
243
|
+
limits: DOCTOR_LIMITS,
|
|
244
|
+
signal: options.signal,
|
|
245
|
+
});
|
|
246
|
+
let report;
|
|
247
|
+
try {
|
|
248
|
+
report = parseDoctorReport(result.stdout);
|
|
249
|
+
}
|
|
250
|
+
catch (cause) {
|
|
251
|
+
if (cause instanceof MarivoEnvironmentError && cause.code === 'doctor-json-invalid') {
|
|
252
|
+
throw new MarivoEnvironmentError(cause.code, cause.message, {
|
|
253
|
+
...cause.details,
|
|
254
|
+
exitCode: result.exitCode,
|
|
255
|
+
stderr: result.stderr.toString('utf8').slice(0, 2_000),
|
|
256
|
+
}, { cause });
|
|
257
|
+
}
|
|
258
|
+
throw cause;
|
|
259
|
+
}
|
|
260
|
+
admitDoctorReport(report, projectRoot, pythonExecutable);
|
|
261
|
+
const partialBinding = {
|
|
262
|
+
projectRoot,
|
|
263
|
+
pythonExecutable,
|
|
264
|
+
marivoVersion: report.marivo.version,
|
|
265
|
+
packagePath: normalizeAbsolute(report.marivo.package_path),
|
|
266
|
+
subprocessPolicyId: subprocessPolicy.id,
|
|
267
|
+
};
|
|
268
|
+
const binding = {
|
|
269
|
+
...partialBinding,
|
|
270
|
+
fingerprint: fingerprint(partialBinding),
|
|
271
|
+
};
|
|
272
|
+
return new MarivoEnvironment(binding, subprocessPolicy);
|
|
273
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { MarivoEnvironmentError } from "./errors.js";
|
|
3
|
+
const DOCTOR_STATUSES = new Set(['ok', 'info', 'warning', 'fail', 'skipped']);
|
|
4
|
+
const OVERALL_STATUSES = new Set(['ok', 'warning', 'fail']);
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function requireString(record, key) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
11
|
+
throw new TypeError(`${key} must be a non-empty string`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function parseCheck(value) {
|
|
16
|
+
if (!isRecord(value))
|
|
17
|
+
throw new TypeError('doctor check must be an object');
|
|
18
|
+
const status = requireString(value, 'status');
|
|
19
|
+
if (!DOCTOR_STATUSES.has(status))
|
|
20
|
+
throw new TypeError(`unsupported doctor check status: ${status}`);
|
|
21
|
+
const details = value.details;
|
|
22
|
+
if (details !== undefined && !isRecord(details))
|
|
23
|
+
throw new TypeError('doctor check details must be an object');
|
|
24
|
+
return {
|
|
25
|
+
id: requireString(value, 'id'),
|
|
26
|
+
status,
|
|
27
|
+
summary: requireString(value, 'summary'),
|
|
28
|
+
...(details === undefined ? {} : { details }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function parseSection(value) {
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
throw new TypeError('doctor section must be an object');
|
|
34
|
+
const status = requireString(value, 'status');
|
|
35
|
+
if (!DOCTOR_STATUSES.has(status))
|
|
36
|
+
throw new TypeError(`unsupported doctor section status: ${status}`);
|
|
37
|
+
if (!Array.isArray(value.checks))
|
|
38
|
+
throw new TypeError('doctor section checks must be an array');
|
|
39
|
+
return {
|
|
40
|
+
id: requireString(value, 'id'),
|
|
41
|
+
status,
|
|
42
|
+
checks: value.checks.map(parseCheck),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Parse the complete doctor stdout. Non-zero process exit is deliberately handled elsewhere. */
|
|
46
|
+
export function parseDoctorReport(stdout) {
|
|
47
|
+
let value;
|
|
48
|
+
try {
|
|
49
|
+
value = JSON.parse(stdout.toString('utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
throw new MarivoEnvironmentError('doctor-json-invalid', 'marivo doctor did not return valid JSON', { stdoutBytes: stdout.byteLength }, { cause });
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
if (!isRecord(value))
|
|
56
|
+
throw new TypeError('doctor report must be an object');
|
|
57
|
+
const status = requireString(value, 'status');
|
|
58
|
+
if (!OVERALL_STATUSES.has(status))
|
|
59
|
+
throw new TypeError(`unsupported doctor status: ${status}`);
|
|
60
|
+
const marivo = value.marivo;
|
|
61
|
+
if (!isRecord(marivo))
|
|
62
|
+
throw new TypeError('marivo must be an object');
|
|
63
|
+
if (!Array.isArray(value.sections))
|
|
64
|
+
throw new TypeError('sections must be an array');
|
|
65
|
+
return {
|
|
66
|
+
status,
|
|
67
|
+
project_root: requireString(value, 'project_root'),
|
|
68
|
+
python_executable: requireString(value, 'python_executable'),
|
|
69
|
+
marivo: {
|
|
70
|
+
version: requireString(marivo, 'version'),
|
|
71
|
+
package_path: requireString(marivo, 'package_path'),
|
|
72
|
+
},
|
|
73
|
+
sections: value.sections.map(parseSection),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw new MarivoEnvironmentError('doctor-json-invalid', `marivo doctor JSON has an invalid shape: ${cause.message}`, {}, { cause });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function normalizeAbsolute(value) {
|
|
81
|
+
return path.normalize(path.resolve(value));
|
|
82
|
+
}
|
|
83
|
+
function findCheck(report, id) {
|
|
84
|
+
for (const section of report.sections) {
|
|
85
|
+
const match = section.checks.find((check) => check.id === id);
|
|
86
|
+
if (match !== undefined)
|
|
87
|
+
return match;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function admissionFailure(check, reason) {
|
|
92
|
+
throw new MarivoEnvironmentError('doctor-admission-failed', `Marivo disclosure admission failed at ${check}: ${reason}`, { check, reason });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate disclosure-specific admission checks without treating the top-level doctor status as
|
|
96
|
+
* an admission decision.
|
|
97
|
+
*/
|
|
98
|
+
export function admitDoctorReport(report, expectedProjectRoot, expectedPythonExecutable) {
|
|
99
|
+
if (normalizeAbsolute(report.project_root) !== normalizeAbsolute(expectedProjectRoot)) {
|
|
100
|
+
admissionFailure('project_root', `reported ${report.project_root}`);
|
|
101
|
+
}
|
|
102
|
+
if (normalizeAbsolute(report.python_executable) !== normalizeAbsolute(expectedPythonExecutable)) {
|
|
103
|
+
admissionFailure('python_executable', `reported ${report.python_executable}`);
|
|
104
|
+
}
|
|
105
|
+
if (report.marivo.version.length === 0)
|
|
106
|
+
admissionFailure('marivo.version', 'missing');
|
|
107
|
+
if (!path.isAbsolute(report.marivo.package_path)) {
|
|
108
|
+
admissionFailure('marivo.package_path', 'must be absolute');
|
|
109
|
+
}
|
|
110
|
+
for (const id of ['installation.python', 'installation.marivo']) {
|
|
111
|
+
const check = findCheck(report, id);
|
|
112
|
+
if (check === undefined)
|
|
113
|
+
admissionFailure(id, 'check is missing');
|
|
114
|
+
if (check.status !== 'ok')
|
|
115
|
+
admissionFailure(id, `status is ${check.status}`);
|
|
116
|
+
}
|
|
117
|
+
const manifest = findCheck(report, 'project.marivo_toml');
|
|
118
|
+
if (manifest === undefined)
|
|
119
|
+
admissionFailure('project.marivo_toml', 'check is missing');
|
|
120
|
+
if (manifest.status !== 'ok' && manifest.status !== 'info') {
|
|
121
|
+
admissionFailure('project.marivo_toml', `status is ${manifest.status}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Stable failure envelope for config, subprocess, doctor, and identity errors. */
|
|
2
|
+
export class MarivoEnvironmentError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details = {}, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = 'MarivoEnvironmentError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { bindMarivoEnvironment, createSharedMarivoRuntimeRunner, MarivoEnvironment, } from "./binding.js";
|
|
2
|
+
export { admitDoctorReport, parseDoctorReport } from "./doctor.js";
|
|
3
|
+
export { MarivoEnvironmentError } from "./errors.js";
|
|
4
|
+
export { DEFAULT_SHARED_RUNTIME_INSTALL_TIMEOUT_MS, ensureSharedMarivoRuntime, SHARED_MARIVO_PACKAGE_SPEC, SHARED_PYTHON_SPEC, } from "./runtime.js";
|
|
5
|
+
export { resolveMarivoBridgeSource, resolveMarivoEnvironmentSource, } from "./source.js";
|
|
6
|
+
export { DEFAULT_SUBPROCESS_LIMITS, FixedSubprocessPolicy, MARIVO_PERSIST_CREDENTIALS_DISABLED, MARIVO_PERSIST_CREDENTIALS_ENV, SUBPROCESS_POLICY_ID, } from "./subprocess.js";
|
|
7
|
+
export { MarivoWorkspaceEnvironmentManager } from "./workspace.js";
|