@qvac/harness 0.8.0 → 0.8.5

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 (43) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist-lib/lib/docx/index.d.ts +1 -1
  3. package/dist-lib/lib/docx/index.js +16 -24
  4. package/dist-lib/lib/office/extract.d.ts +9 -0
  5. package/dist-lib/lib/office/extract.js +19 -0
  6. package/dist-lib/lib/office/index.d.ts +1 -1
  7. package/dist-lib/lib/office/index.js +74 -19
  8. package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
  9. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
  10. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
  11. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
  12. package/dist-lib/lib/tools/exec.js +9 -20
  13. package/dist-lib/lib/tools/office/ops.d.ts +13 -0
  14. package/dist-lib/lib/tools/office/ops.js +100 -0
  15. package/dist-lib/lib/tools/office.d.ts +12 -0
  16. package/dist-lib/lib/tools/office.js +486 -0
  17. package/package.json +10 -6
  18. package/wheels/manifest.json +1 -160
  19. package/worker/harness-sidecar.mjs +3 -1
  20. package/dist-lib/lib/office/pptx.d.ts +0 -1
  21. package/dist-lib/lib/office/pptx.js +0 -26
  22. package/dist-lib/lib/office/python.d.ts +0 -8
  23. package/dist-lib/lib/office/python.js +0 -32
  24. package/dist-lib/lib/office/xlsx.d.ts +0 -1
  25. package/dist-lib/lib/office/xlsx.js +0 -26
  26. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
  27. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
  28. package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  29. package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
  30. package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
  31. package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
  32. package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
  33. package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
  34. package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  35. package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
  36. package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  37. package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
  38. package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
  39. package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
  40. package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
  41. package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
  42. package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  43. package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,47 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.5] - 2026-10-06
12
+
13
+ ### Added
14
+
15
+ - Skill-gated `pptx`, `docx` and `xlsx` tools on the office engine. A call takes
16
+ an `attachmentId` (or `create` options), `ops` that name engine methods with
17
+ named arguments, and an `output` file name; with an `attachmentId` alone it
18
+ returns the file's contents. The first failing op saves nothing and returns
19
+ the engine's error. An edit is never saved under the attached file's own
20
+ name. Saving the same file again in one turn answers with the one already
21
+ attached, and a changed file under that name is refused. The one-line answer
22
+ comes back as `reply`. These are the tools the presentations, word and excel
23
+ skills in `@qvac/skills` 0.1.12 call. (#286)
24
+
25
+ ### Changed
26
+
27
+ - **Breaking:** `@qvac/skills` is now 0.1.12, whose office and pdf skills run on
28
+ the `pptx`, `docx`, `xlsx` and `pdf` tools instead of Python. A host that
29
+ passes its own `skillsDir` needs 0.1.12 as well: older office skills import
30
+ Python libraries this release no longer ships. (#286)
31
+ - `extractOfficeText` and `extractDocxText` read .docx, .xlsx and .pptx in
32
+ process on the engine instead of starting Pyodide, and read at most 1M cells
33
+ across a workbook. Extracted workbook booleans read `true`/`false` instead of
34
+ `True`/`False`. `closeOfficeWorker` and `closeDocxWorker` remain as no-ops.
35
+ (#286)
36
+ - `exec` refuses Python that imports `pptx`, `docx` or `openpyxl`, or declares
37
+ their packages, before it runs, and names the tool (or the skill that grants
38
+ it) to use instead. (#287)
39
+ - **Breaking:** Python run through `exec` asks for approval like any other
40
+ write, including a run that only builds a new PDF. (#289)
41
+
42
+ ### Removed
43
+
44
+ - **Breaking:** the vendored Python wheels for python-pptx, python-docx,
45
+ openpyxl, fpdf2 and pypdf, and their dependencies. Only micropip is still
46
+ seeded; any other package installs from PyPI, which sandbox mode refuses.
47
+ Use the office and `pdf` tools. (#286, #289)
48
+ - The `exec` guidance written for python-pptx, python-docx and fpdf2 code:
49
+ their error hints and source checks, and the shrink-to-fit pass on saved
50
+ decks. (#288, #289)
51
+
11
52
  ## [0.8.0] - 2026-10-05
12
53
 
13
54
  ### Added
@@ -1108,7 +1149,8 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
1108
1149
  - GPU stats are read through `bare-gpu-info`, so every dependency installs from
1109
1150
  the public npm registry. (#12)
1110
1151
 
1111
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...HEAD
1152
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.5...HEAD
1153
+ [0.8.5]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...v0.8.5
1112
1154
  [0.8.0]: https://github.com/tetherto/qvac-harness/compare/v0.7.3...v0.8.0
1113
1155
  [0.7.3]: https://github.com/tetherto/qvac-harness/compare/v0.7.2...v0.7.3
1114
1156
  [0.7.2]: https://github.com/tetherto/qvac-harness/compare/v0.7.1...v0.7.2
@@ -1,5 +1,5 @@
1
1
  import type { AbortSignal } from 'bare-abort-controller';
2
- export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/active.ts';
2
+ export declare function closeDocxWorker(): Promise<void>;
3
3
  export interface DocxExtractOptions {
4
4
  readonly signal?: AbortSignal;
5
5
  }
@@ -1,26 +1,18 @@
1
- import { extractWithPython } from '../office/python.js';
2
- export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/active.js';
3
- const PACKAGES = ['python-docx==1.2.0'];
4
- const EXTRACT = `from docx import Document
5
- from docx.text.paragraph import Paragraph
6
- from pathlib import Path
7
-
8
- def text(container):
9
- for block in container.iter_inner_content():
10
- if isinstance(block, Paragraph):
11
- if block.text.strip():
12
- yield block.text
13
- else:
14
- for row in block.rows:
15
- yield " | ".join(" ".join(text(cell)) for cell in row.cells)
16
-
17
- Path("extracted.txt").write_text("\\n".join(text(Document("input.docx"))), encoding="utf-8")
18
- `;
1
+ import { docx } from '../office/engine/index.js';
2
+ import { cellText, extractWith } from '../office/extract.js';
3
+ export async function closeDocxWorker() { }
4
+ function documentText(bytes) {
5
+ const lines = [];
6
+ for (const block of docx.open(bytes).read().blocks) {
7
+ if (block.type === 'table') {
8
+ lines.push(...block.rows.map((row) => row.map(cellText).join(' | ')));
9
+ }
10
+ else if (block.type === 'paragraph' && block.text.trim()) {
11
+ lines.push(block.text);
12
+ }
13
+ }
14
+ return lines.join('\n');
15
+ }
19
16
  export async function extractDocxText(bytes, fileName, options = {}) {
20
- return extractWithPython(bytes, fileName, {
21
- format: 'DOCX',
22
- packages: PACKAGES,
23
- source: EXTRACT,
24
- signal: options.signal
25
- });
17
+ return extractWith(bytes, { fileName, signal: options.signal, extract: documentText });
26
18
  }
@@ -0,0 +1,9 @@
1
+ import type { AbortSignal } from 'bare-abort-controller';
2
+ export type Extract = (bytes: Uint8Array) => string;
3
+ export interface ExtractJob {
4
+ readonly fileName: string;
5
+ readonly signal: AbortSignal | undefined;
6
+ readonly extract: Extract;
7
+ }
8
+ export declare function extractWith(bytes: Uint8Array, { fileName, signal, extract }: ExtractJob): string;
9
+ export declare function cellText(value: unknown): string;
@@ -0,0 +1,19 @@
1
+ import { errorMessage } from '../guards.js';
2
+ const MAX_OFFICE_BYTES = 20 * 1024 * 1024;
3
+ export function extractWith(bytes, { fileName, signal, extract }) {
4
+ if (bytes.byteLength > MAX_OFFICE_BYTES) {
5
+ throw new Error(`document is too large to extract: ${bytes.byteLength} bytes, limit ${MAX_OFFICE_BYTES}`);
6
+ }
7
+ if (signal?.aborted) {
8
+ throw new Error(`${fileName}: extraction aborted`);
9
+ }
10
+ try {
11
+ return extract(bytes);
12
+ }
13
+ catch (err) {
14
+ throw new Error(`${fileName}: ${errorMessage(err)}`);
15
+ }
16
+ }
17
+ export function cellText(value) {
18
+ return value === null || value === undefined ? '' : String(value).replaceAll('\n', ' ');
19
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AbortSignal } from 'bare-abort-controller';
2
- export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.ts';
2
+ export { closeDocxWorker as closeOfficeWorker } from '../docx/index.ts';
3
3
  export interface OfficeExtractOptions {
4
4
  readonly signal?: AbortSignal | undefined;
5
5
  readonly mimeType?: string | undefined;
@@ -1,14 +1,81 @@
1
1
  import path from 'path';
2
2
  import { extractDocxText } from '../docx/index.js';
3
- import { extractWithPython } from './python.js';
4
- import { XLSX_EXTRACT } from './xlsx.js';
5
- import { PPTX_EXTRACT } from './pptx.js';
6
- export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.js';
3
+ import { pptx, xlsx } from './engine/index.js';
4
+ import { cellText, extractWith } from './extract.js';
5
+ export { closeDocxWorker as closeOfficeWorker } from '../docx/index.js';
7
6
  const OFFICE_MIME_EXTENSIONS = {
8
7
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
9
8
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
10
9
  'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx'
11
10
  };
11
+ const EXTRACT_CELL_LIMIT = 1_000_000;
12
+ function columnIndex(letters) {
13
+ let index = 0;
14
+ for (const char of letters) {
15
+ index = index * 26 + char.charCodeAt(0) - 64;
16
+ }
17
+ return index;
18
+ }
19
+ function columnName(index) {
20
+ let name = '';
21
+ for (let n = index; n > 0; n = Math.floor((n - 1) / 26)) {
22
+ name = String.fromCharCode(65 + ((n - 1) % 26)) + name;
23
+ }
24
+ return name;
25
+ }
26
+ function trimmedRow(values) {
27
+ const row = [...values];
28
+ while (row.length && !cellText(row.at(-1)).trim()) {
29
+ row.pop();
30
+ }
31
+ return row;
32
+ }
33
+ function workbookText(bytes) {
34
+ const book = xlsx.open(bytes);
35
+ let output = '';
36
+ let budget = EXTRACT_CELL_LIMIT;
37
+ for (const sheet of book.sheets()) {
38
+ if (sheet.kind !== 'worksheet' || budget <= 0)
39
+ continue;
40
+ const { range, rows, formulas } = book.read(sheet.index, { limit: budget });
41
+ budget -= rows.length * (rows[0]?.length ?? 0);
42
+ const [, letters = 'A', digits = '1'] = /^([A-Z]+)(\d+)/.exec(range ?? 'A1') ?? [];
43
+ const firstColumn = columnIndex(letters);
44
+ const firstRow = Number(digits);
45
+ const lines = rows
46
+ .map((row, r) => trimmedRow(row.map((value, c) => value ?? formulas[`${columnName(firstColumn + c)}${firstRow + r}`] ?? null)))
47
+ .filter((row) => row.length)
48
+ .map((row) => row.map(cellText).join(' | '));
49
+ if (lines.length) {
50
+ output += `Sheet: ${sheet.name}\n${lines.join('\n')}\n`;
51
+ }
52
+ }
53
+ return output;
54
+ }
55
+ function shapeBlocks(shape) {
56
+ if (shape.paragraphs) {
57
+ const text = shape.paragraphs.map((paragraph) => paragraph.text).join('\n');
58
+ return text.trim() ? [text] : [];
59
+ }
60
+ return (shape.rows ?? [])
61
+ .map((row) => row.map(cellText))
62
+ .filter((cells) => cells.some((cell) => cell.trim()))
63
+ .map((cells) => cells.join(' | '));
64
+ }
65
+ function deckText(bytes) {
66
+ let output = '';
67
+ for (const slide of pptx.open(bytes).read()) {
68
+ const blocks = slide.shapes.flatMap(shapeBlocks);
69
+ if (blocks.length) {
70
+ output += `Slide ${slide.index + 1}\n${blocks.join('\n')}\n`;
71
+ }
72
+ }
73
+ return output;
74
+ }
75
+ const EXTRACTORS = new Map([
76
+ ['.xlsx', workbookText],
77
+ ['.pptx', deckText]
78
+ ]);
12
79
  export async function extractOfficeText(bytes, fileName, options = {}) {
13
80
  const mimeType = options.mimeType?.split(';')[0]?.trim().toLowerCase();
14
81
  const suffix = path.extname(fileName).toLowerCase();
@@ -18,21 +85,9 @@ export async function extractOfficeText(bytes, fileName, options = {}) {
18
85
  if (extension === '.docx') {
19
86
  return extractDocxText(bytes, fileName, options);
20
87
  }
21
- if (extension === '.xlsx') {
22
- return extractWithPython(bytes, fileName, {
23
- format: 'XLSX',
24
- packages: ['openpyxl==3.1.5'],
25
- source: XLSX_EXTRACT,
26
- signal: options.signal
27
- });
28
- }
29
- if (extension === '.pptx') {
30
- return extractWithPython(bytes, fileName, {
31
- format: 'PPTX',
32
- packages: ['python-pptx==1.0.2'],
33
- source: PPTX_EXTRACT,
34
- signal: options.signal
35
- });
88
+ const extract = EXTRACTORS.get(extension ?? '');
89
+ if (extract) {
90
+ return extractWith(bytes, { fileName, signal: options.signal, extract });
36
91
  }
37
92
  throw new Error(`Unsupported Office document: ${fileName}`);
38
93
  }
@@ -20,12 +20,10 @@ export const MODULE_DISTRIBUTIONS = {
20
20
  cvxpy: 'cvxpy-base',
21
21
  dateutil: 'python-dateutil',
22
22
  dns: 'dnspython',
23
- docx: 'python-docx',
24
23
  erfa: 'pyerfa',
25
24
  fitz: 'PyMuPDF',
26
25
  flint: 'python-flint',
27
26
  flirt: 'python-flirt',
28
- fpdf: 'fpdf2',
29
27
  google: 'protobuf',
30
28
  gridfs: 'pymongo',
31
29
  isympy: 'sympy',
@@ -33,7 +31,6 @@ export const MODULE_DISTRIBUTIONS = {
33
31
  mpl_toolkits: 'matplotlib',
34
32
  mypyc: 'mypy',
35
33
  nacl: 'pynacl',
36
- pptx: 'python-pptx',
37
34
  pygame: 'pygame-ce',
38
35
  pylab: 'matplotlib',
39
36
  pysat: 'python-sat',
@@ -1,8 +1,7 @@
1
- import type { ExecAttachment } from '../../runner.ts';
2
1
  import type { ExecutionAdapter } from '../adapter.ts';
3
2
  import { type ExecInput } from './attachments.ts';
4
3
  import { type ExecError } from './output-policy.ts';
5
- import { type LoadPyodideFn, type PythonRunResult } from './pyodide.ts';
4
+ import { type LoadPyodideFn } from './pyodide.ts';
6
5
  export declare function isPythonLanguage(language: unknown): boolean;
7
6
  export declare function runsThroughPyodide(language: unknown, disablePyodide: boolean): boolean;
8
7
  export declare function unwrapPythonSource(command: string): string | null;
@@ -20,70 +19,28 @@ export interface PipFromSourceHintOptions {
20
19
  export declare function pipFromSourceHint(stderr: string, source: string, { packages, sandbox }?: PipFromSourceHintOptions): string | null;
21
20
  export declare function stringLiteralProgramHint(source: string): string | null;
22
21
  export declare function checkCollapsedCompound(source: string): string | null;
23
- export declare function mangledPptxApiHint(source: string): string | null;
24
- export declare function wrongRgbColorHint(source: string): string | null;
25
- export declare function fpdfSizeKeywordHint(source: string): string | null;
26
- export declare function fpdfKeywordErrorHint(stderr: string): string | null;
27
- export declare function normalizeFpdfSource(source: string): string;
28
- export declare function fpdfTextCallHint(source: string): string | null;
29
- export declare function fpdfTextErrorHint(stderr: string): string | null;
30
- export declare function fpdfNonLatin1Hint(source: string): string | null;
31
- export declare function fpdfUnicodeErrorHint(stderr: string): string | null;
32
- export declare function fpdfHorizontalSpaceHint(stderr: string): string | null;
33
- export declare function rgbColorImportHint(stderr: string): string | null;
34
22
  export declare function checkPythonSource(command: string): string | null;
35
23
  export declare function syntaxErrorHint(stderr: string, source: string): string | null;
36
- export declare function checkPptxBuildSource(source: string, outputs: readonly string[] | undefined, packages?: readonly string[] | undefined): string | null;
37
- export declare function tripleQuoteAt(source: string, index: number): string | null;
38
- export declare function isEscaped(source: string, index: number): boolean;
39
- export declare function checkBareLengthArgs(source: string, packages?: readonly string[] | undefined): string | null;
40
- export declare function checkPdfBuildSource(source: string, outputs: readonly string[] | undefined, packages?: readonly string[] | undefined): string | null;
41
- export declare function stripCommentsAndDocstrings(source: string): string;
42
- export declare const PDF_READ: RegExp;
43
- export declare const OFFICE_READ: RegExp;
44
- export declare function checkUnstagedRead(source: string, inputs: readonly Pick<ExecInput, 'path'>[] | undefined): string | null;
45
24
  export declare function missingFileHint(stderr: string): string | null;
46
25
  export declare function parsePackages(value: unknown): string[] | ExecError | undefined;
47
26
  export declare function parseInputs(value: unknown): ExecInput[] | ExecError | undefined;
48
27
  export declare function parseOutputs(value: unknown): string[] | ExecError | undefined;
49
28
  export declare function parseWorkdir(value: unknown): string | ExecError | undefined;
29
+ export interface OfficeImportContext {
30
+ readonly packages: readonly string[] | undefined;
31
+ readonly toolNames: readonly string[] | undefined;
32
+ }
33
+ export declare function officeModuleHint(stderr: string, toolNames: readonly string[] | undefined): string | null;
34
+ export declare function officeImportError(source: string, { packages, toolNames }: OfficeImportContext): string | null;
50
35
  export interface MissingModuleHintOptions {
51
36
  readonly packages?: readonly string[] | undefined;
52
37
  readonly sandbox?: boolean;
53
38
  readonly network?: boolean;
54
39
  }
55
40
  export declare function missingModuleHint(stderr: string, { packages, sandbox, network }?: MissingModuleHintOptions): string | null;
56
- export declare function nonePlaceholderHint(stderr: string): string | null;
57
- export declare function slideShapesHint(stderr: string): string | null;
58
- export declare function shapeFontHint(stderr: string): string | null;
59
- export declare function shapesBackgroundHint(stderr: string): string | null;
60
- export declare function paragraphFillHint(stderr: string): string | null;
61
- export declare function readOnlyPropertyHint(stderr: string): string | null;
62
- export declare function paragraphIndexHint(stderr: string, outputs: readonly string[]): string | null;
63
41
  export declare function shellNameHint(stderr: string): string | null;
64
42
  export declare function noProcessHint(stderr: string, sandbox?: boolean): string | null;
65
43
  export declare function noNetworkHint(stderr: string, tools?: readonly string[], network?: boolean): string | null;
66
44
  export declare function nativeWheelHint(stderr: string): string | null;
67
45
  export declare function packageFetchFailedHint(stderr: string): string | null;
68
- export declare const IMAGE_INPUT: RegExp;
69
- export declare function pptxHasEmbeddedMedia(bytes: Uint8Array): boolean;
70
- export declare function softFailedDeckHint(stdout: string, exitCode: number, source: string): string | null;
71
- export interface DeckImagesOptions {
72
- readonly inputs: readonly {
73
- path: string;
74
- }[] | undefined;
75
- readonly outputs: readonly string[] | undefined;
76
- readonly source: string;
77
- }
78
- export declare function stagedImagesMissingFromDeck(result: Pick<PythonRunResult, 'exitCode' | 'files'>, { inputs, outputs, source }: DeckImagesOptions): string | null;
79
- export declare function checkStagedImagesUsed(source: string, inputs: readonly Pick<ExecInput, 'path'>[] | undefined, outputs: readonly string[] | undefined): string | null;
80
- export interface TurnImagesStagedOptions {
81
- readonly inputs: readonly ExecInput[] | undefined;
82
- readonly outputs: readonly string[] | undefined;
83
- readonly turnImages: readonly {
84
- path: string;
85
- }[] | undefined;
86
- }
87
- export declare function checkTurnImagesStaged(source: string, { inputs, outputs, turnImages }: TurnImagesStagedOptions): string | null;
88
- export declare function deckDeliveredStop(attachments: readonly Pick<ExecAttachment, 'fileName'>[] | undefined, exitCode: number, source?: string): string | null;
89
46
  export declare function createPythonAdapter(disabled: boolean, loadPyodide?: LoadPyodideFn, network?: boolean): ExecutionAdapter;