mixdog 1.3.4 → 1.3.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 (197) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +2 -1
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/bot-hops.mjs +62 -2
  128. package/src/runtime/shared/config.mjs +49 -22
  129. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  130. package/src/runtime/shared/connector-catalog.mjs +16 -8
  131. package/src/runtime/shared/developer-options.mjs +32 -1
  132. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  133. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  134. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  135. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  136. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  137. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  138. package/src/runtime/shared/provider-accounts.mjs +224 -58
  139. package/src/runtime/shared/provider-api-key.mjs +19 -8
  140. package/src/runtime/shared/routine-trigger.mjs +42 -0
  141. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  142. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  143. package/src/runtime/shared/user-data-guard.mjs +30 -2
  144. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  145. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  146. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  147. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  148. package/src/session-runtime/boot/begin.mjs +3 -2
  149. package/src/session-runtime/boot/tools.mjs +2 -0
  150. package/src/session-runtime/bot-chain.mjs +32 -0
  151. package/src/session-runtime/bot-status.mjs +7 -4
  152. package/src/session-runtime/channel-config-api.mjs +63 -17
  153. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  154. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  155. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  156. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  157. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  158. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  159. package/src/session-runtime/owned-automation-run.mjs +33 -12
  160. package/src/session-runtime/provider-auth/login.mjs +7 -0
  161. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  162. package/src/session-runtime/routine-home-turn.mjs +33 -6
  163. package/src/session-runtime/schedule-session-run.mjs +7 -3
  164. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  165. package/src/session-runtime/services/channel-admin.mjs +66 -18
  166. package/src/session-runtime/services/channel-worker.mjs +1 -1
  167. package/src/session-runtime/services/provider-admin.mjs +128 -25
  168. package/src/session-runtime/services/schedule-access.mjs +14 -0
  169. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  170. package/src/session-runtime/session-create/session-options.mjs +7 -4
  171. package/src/session-runtime/settings-system-api.mjs +16 -1
  172. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  173. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  174. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  175. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  176. package/src/session-runtime/task-session-route.mjs +99 -0
  177. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  178. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  179. package/src/session-runtime/tool-surface.mjs +4 -0
  180. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  181. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  182. package/src/session-runtime/workspace/transitions.mjs +10 -2
  183. package/src/standalone/daemon-session-control.mjs +88 -20
  184. package/src/standalone/daemon.mjs +8 -0
  185. package/src/standalone/session-protocol.mjs +5 -0
  186. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  187. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  188. package/src/standalone/session-service.mjs +13 -0
  189. package/src/standalone/session-tool-output.mjs +8 -5
  190. package/src/tui/app/model-options.mjs +2 -0
  191. package/src/tui/dist/index.mjs +24 -2
  192. package/src/tui/session/notice-surface.mjs +17 -1
  193. package/src/tui/session/session-api/integrations.mjs +1 -0
  194. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  195. package/src/tui/session/session-api/settings.mjs +14 -2
  196. package/src/tui/session/turn-stream.mjs +17 -1
  197. package/src/tui/session/turn.mjs +22 -2
@@ -0,0 +1,469 @@
1
+ // Opening an encrypted Office Open XML package with its password, exactly as [MS-OFFCRYPTO] 2.3.4 specifies it.
2
+ //
3
+ // The \EncryptionInfo stream says how the package was encrypted and carries a password verifier: agile encryption
4
+ // (2.3.4.10) with an XML descriptor, or standard encryption (2.3.4.5) with a binary header. The verifier decides
5
+ // whether a password is right before anything else happens (2.3.4.9, 2.3.4.13); for agile encryption the
6
+ // descriptor's HMAC then has to prove \EncryptedPackage intact (2.3.4.14), and only then does the key decrypt it
7
+ // back into the plain package (2.3.4.15). Anything outside those two schemes (extensible encryption, a cipher or
8
+ // chaining mode the spec allows but Office does not write) is "unsupported", never a guess. The password hashing
9
+ // (up to 10,000,000 rounds) runs on a worker thread that a cancelled request ends.
10
+ import { createDecipheriv, createHash, createHmac, timingSafeEqual } from 'node:crypto';
11
+ import { Worker } from 'node:worker_threads';
12
+ import { SaxesParser } from 'saxes';
13
+ import { workerExecArgv } from '../../shared/worker-exec-argv.mjs';
14
+
15
+ // The agile descriptor is XML, read with saxes (the namespace-aware parser jsdom also parses XML with). saxes
16
+ // resolves no external entity and knows only the predefined ones; a DTD is refused outright (descriptorFields).
17
+
18
+ const ENCRYPTION_NAMESPACE = 'http://schemas.microsoft.com/office/2006/encryption';
19
+ const PASSWORD_KEY_ENCRYPTOR = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
20
+ // 2.3.4.13: the block keys of the verifier input, the verifier hash and the encrypted key.
21
+ const VERIFIER_INPUT_BLOCK = Buffer.from([0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79]);
22
+ const VERIFIER_HASH_BLOCK = Buffer.from([0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e]);
23
+ const ENCRYPTED_KEY_BLOCK = Buffer.from([0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6]);
24
+ // 2.3.4.14: the block keys of the HMAC key and the HMAC value.
25
+ const INTEGRITY_KEY_BLOCK = Buffer.from([0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, 0xf6]);
26
+ const INTEGRITY_VALUE_BLOCK = Buffer.from([0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, 0x33]);
27
+ // 2.3.4.10: spinCount MUST NOT exceed 10,000,000.
28
+ const MAX_SPIN_COUNT = 10_000_000;
29
+ // 2.3.4.7: standard encryption hashes the password 50,000 times.
30
+ const STANDARD_SPIN_COUNT = 50_000;
31
+ const SEGMENT_LENGTH = 4096;
32
+ // 2.3.4.10 HashAlgorithm: the values the table defines, by their OpenSSL names. "SHA1" is not in the table; it is
33
+ // the name Windows resolves SHA-1 by (as it resolves SHA256, SHA384 and SHA512), which footnote <21> allows: "any
34
+ // algorithm that can be resolved by name by the underlying operating system can be used for hashing". A defined
35
+ // algorithm this runtime cannot compute (MD2, RIPEMD-128, ...) leaves the document unsupported.
36
+ const HASH_ALGORITHMS = new Map([
37
+ ['SHA-1', 'sha1'],
38
+ ['SHA1', 'sha1'],
39
+ ['SHA256', 'sha256'],
40
+ ['SHA384', 'sha384'],
41
+ ['SHA512', 'sha512'],
42
+ ['MD5', 'md5'],
43
+ ['MD4', 'md4'],
44
+ ['MD2', 'md2'],
45
+ ['RIPEMD-128', 'ripemd128'],
46
+ ['RIPEMD-160', 'ripemd160'],
47
+ ['WHIRLPOOL', 'whirlpool'],
48
+ ]);
49
+ const AES_ALGORITHMS = new Map([
50
+ [0x660e, 128],
51
+ [0x660f, 192],
52
+ [0x6610, 256],
53
+ ]);
54
+ const STANDARD_FLAG_CRYPTOAPI = 0x04;
55
+ const STANDARD_FLAG_AES = 0x20;
56
+
57
+ const hash = (algorithm, ...parts) => {
58
+ const digest = createHash(algorithm);
59
+ for (const part of parts) digest.update(part);
60
+ return digest.digest();
61
+ };
62
+ const uint32 = (value) => {
63
+ const bytes = Buffer.alloc(4);
64
+ bytes.writeUInt32LE(value >>> 0, 0);
65
+ return bytes;
66
+ };
67
+ // Truncated to, or padded with 0x36 up to, `length` bytes (2.3.4.11, 2.3.4.12).
68
+ const fitted = (bytes, length) =>
69
+ bytes.length >= length
70
+ ? bytes.subarray(0, length)
71
+ : Buffer.concat([bytes, Buffer.alloc(length - bytes.length, 0x36)]);
72
+
73
+ function decrypt(mode, keyBits, key, iv, data) {
74
+ const decipher = createDecipheriv(`aes-${keyBits}-${mode}`, key, iv);
75
+ decipher.setAutoPadding(false);
76
+ return Buffer.concat([decipher.update(data), decipher.final()]);
77
+ }
78
+
79
+ // The digest length of an OpenSSL hash, or null when this runtime cannot compute it.
80
+ const digestLengths = new Map();
81
+ function digestLength(algorithm) {
82
+ if (!digestLengths.has(algorithm)) {
83
+ let length = null;
84
+ try {
85
+ length = createHash(algorithm).digest().length;
86
+ } catch {}
87
+ digestLengths.set(algorithm, length);
88
+ }
89
+ return digestLengths.get(algorithm);
90
+ }
91
+
92
+ // [W3C-XSD] lexical forms of the attribute types the descriptor uses.
93
+ const XML_SPACE = /[\t\n\r ]+/g;
94
+ // xs:unsignedInt (whitespace collapsed): decimal digits with an optional "+", at most 4,294,967,295.
95
+ function unsignedInt(value) {
96
+ const text = typeof value === 'string' ? value.replace(XML_SPACE, ' ').trim() : '';
97
+ if (!/^\+?[0-9]+$/.test(text)) return null;
98
+ const number = Number(text);
99
+ return number <= 0xffffffff ? number : null;
100
+ }
101
+ // xs:base64Binary: whole quads of the base64 alphabet (whitespace between them allowed), the last one padded with
102
+ // "=" over the unused bits, which must be zero.
103
+ function base64Binary(value) {
104
+ const text = typeof value === 'string' ? value.replace(XML_SPACE, '') : '';
105
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?$/.test(text)) return null;
106
+ return Buffer.from(text, 'base64');
107
+ }
108
+ // xs:token: whitespace collapsed.
109
+ const token = (value) => (typeof value === 'string' ? value.replace(XML_SPACE, ' ').trim() : null);
110
+
111
+ // The unqualified attributes of an element (the descriptor's schemas set attributeFormDefault="unqualified").
112
+ function unqualified(tag) {
113
+ const values = Object.create(null);
114
+ for (const attribute of Object.values(tag.attributes)) {
115
+ if (attribute.uri === '') values[attribute.local] = attribute.value;
116
+ }
117
+ return values;
118
+ }
119
+
120
+ // CT_Encryption is this sequence: keyData, an optional dataIntegrity, keyEncryptors.
121
+ const ENCRYPTION_SEQUENCE = ['keyData', 'dataIntegrity', 'keyEncryptors'];
122
+
123
+ /**
124
+ * The attributes of keyData, dataIntegrity (null when absent) and the password encryptedKey of an
125
+ * XmlEncryptionDescriptor (2.3.4.10), read by namespace: the root is encryption, its children follow the
126
+ * CT_Encryption sequence, keyEncryptors holds at least one keyEncryptor of exactly one element each, and exactly one
127
+ * of them is the password key encryptor (uri "…/keyEncryptor/password") holding the encryptedKey of that namespace.
128
+ * Throws for a DTD, malformed XML, an encoding other than UTF-8, or any other structure where these fields live.
129
+ */
130
+ function descriptorFields(xml) {
131
+ const parser = new SaxesParser({ xmlns: true, position: false });
132
+ const fields = { keyData: null, dataIntegrity: null, encryptedKey: null };
133
+ const open = [];
134
+ let position = -1;
135
+ let keyEncryptors = 0;
136
+ let passwordKeyEncryptors = 0;
137
+ parser.on('xmldecl', ({ encoding }) => {
138
+ if (encoding !== undefined && encoding.toLowerCase() !== 'utf-8') throw new Error('descriptor is not UTF-8');
139
+ });
140
+ parser.on('doctype', () => {
141
+ throw new Error('a DTD is not allowed in the descriptor');
142
+ });
143
+ parser.on('opentag', (tag) => {
144
+ const parent = open.at(-1);
145
+ const encryption = tag.uri === ENCRYPTION_NAMESPACE;
146
+ let kind;
147
+ if (!parent) {
148
+ if (!encryption || tag.local !== 'encryption') throw new Error('not an encryption descriptor');
149
+ kind = 'encryption';
150
+ } else if (parent.kind === 'encryption') {
151
+ const at = ENCRYPTION_SEQUENCE.indexOf(tag.local);
152
+ if (!encryption || at <= position) throw new Error('unexpected element in encryption');
153
+ position = at;
154
+ kind = tag.local;
155
+ if (kind !== 'keyEncryptors') fields[kind] = unqualified(tag);
156
+ } else if (parent.kind === 'keyEncryptors') {
157
+ if (!encryption || tag.local !== 'keyEncryptor') throw new Error('unexpected element in keyEncryptors');
158
+ keyEncryptors++;
159
+ const password = token(unqualified(tag).uri) === PASSWORD_KEY_ENCRYPTOR;
160
+ if (password) passwordKeyEncryptors++;
161
+ kind = password ? 'passwordKeyEncryptor' : 'keyEncryptor';
162
+ } else if (parent.kind === 'passwordKeyEncryptor' || parent.kind === 'keyEncryptor') {
163
+ if (++parent.children > 1) throw new Error('a keyEncryptor holds one element');
164
+ if (parent.kind === 'passwordKeyEncryptor') {
165
+ if (tag.uri !== PASSWORD_KEY_ENCRYPTOR || tag.local !== 'encryptedKey') throw new Error('not a password key');
166
+ fields.encryptedKey = unqualified(tag);
167
+ kind = 'encryptedKey';
168
+ } else {
169
+ kind = 'other';
170
+ }
171
+ } else if (parent.kind === 'other') {
172
+ kind = 'other';
173
+ } else {
174
+ // keyData, dataIntegrity and encryptedKey are attributes only.
175
+ throw new Error(`${parent.kind} has no child elements`);
176
+ }
177
+ open.push({ kind, children: 0 });
178
+ });
179
+ parser.on('closetag', () => {
180
+ const closed = open.pop();
181
+ if ((closed.kind === 'keyEncryptor' || closed.kind === 'passwordKeyEncryptor') && closed.children !== 1) {
182
+ throw new Error('a keyEncryptor holds one element');
183
+ }
184
+ });
185
+ parser.write(xml).close();
186
+ if (!fields.keyData || position !== 2 || keyEncryptors < 1 || passwordKeyEncryptors !== 1) {
187
+ throw new Error('incomplete encryption descriptor');
188
+ }
189
+ return fields;
190
+ }
191
+
192
+ // One cipher description (keyData, or the password encryptedKey), or null when it is not AES-CBC with a hash this
193
+ // runtime computes, or its values do not match their schema types.
194
+ function agileCipher(values) {
195
+ const keyBits = unsignedInt(values.keyBits);
196
+ const blockSize = unsignedInt(values.blockSize);
197
+ const saltSize = unsignedInt(values.saltSize);
198
+ const hashSize = unsignedInt(values.hashSize);
199
+ const algorithm = HASH_ALGORITHMS.get(values.hashAlgorithm);
200
+ const salt = base64Binary(values.saltValue);
201
+ if (values.cipherAlgorithm !== 'AES' || values.cipherChaining !== 'ChainingModeCBC') return null;
202
+ if (![128, 192, 256].includes(keyBits) || blockSize !== 16 || !algorithm) return null;
203
+ if (digestLength(algorithm) !== hashSize || !salt || saltSize < 1 || salt.length !== saltSize) return null;
204
+ return { keyBits, blockSize, saltSize, hashSize, algorithm, salt };
205
+ }
206
+
207
+ // base64Binary ciphertext of whole cipher blocks holding at least `least` bytes, or null.
208
+ function ciphertext(value, blockSize, least) {
209
+ const bytes = base64Binary(value);
210
+ return bytes && bytes.length >= least && bytes.length % blockSize === 0 ? bytes : null;
211
+ }
212
+
213
+ const wholeBlocks = (length, blockSize) => Math.ceil(length / blockSize) * blockSize;
214
+
215
+ // 2.3.4.14: the encrypted salt (the HMAC key) and the encrypted HMAC (hashSize bytes), each padded with 0x00 to
216
+ // whole blocks. Step 2 makes the salt KeyData.saltSize bytes long. Excel writes hashSize bytes instead (a file it
217
+ // saves holds saltSize 16, hashSize 64 and a 64-byte encryptedHmacKey whose bytes past the 16th are not padding),
218
+ // as Apache POI and msoffcrypto-tool do. The padded length says which of the two a file holds; where both pad to
219
+ // the same size the longer is taken, because the shorter one's further bytes are its 0x00 padding and [RFC2104]
220
+ // keys the HMAC identically with them (that size is at most 64 bytes, within one hash block). A present element
221
+ // that is anything else is malformed: the package is then never rendered.
222
+ function dataIntegrity(values, data) {
223
+ if (!values) return null;
224
+ const hmacKey = base64Binary(values.encryptedHmacKey);
225
+ const hmacValue = base64Binary(values.encryptedHmacValue);
226
+ const saltLengths = [data.saltSize, data.hashSize].filter(
227
+ (length) => hmacKey?.length === wholeBlocks(length, data.blockSize)
228
+ );
229
+ if (!saltLengths.length || hmacValue?.length !== wholeBlocks(data.hashSize, data.blockSize)) {
230
+ return { malformed: true };
231
+ }
232
+ return { hmacKey, hmacValue, saltLength: Math.max(...saltLengths) };
233
+ }
234
+
235
+ function agileDescriptor(info) {
236
+ let fields;
237
+ try {
238
+ fields = descriptorFields(new TextDecoder('utf-8', { fatal: true }).decode(info.subarray(8)));
239
+ } catch {
240
+ return null;
241
+ }
242
+ const values = fields.encryptedKey;
243
+ const data = agileCipher(fields.keyData);
244
+ const key = agileCipher(values);
245
+ const spinCount = unsignedInt(values.spinCount);
246
+ if (!data || !key || spinCount === null || spinCount > MAX_SPIN_COUNT) return null;
247
+ // The password key encryptor MUST use the keyData's cipher (both AES here) and hash algorithm.
248
+ if (key.algorithm !== data.algorithm) return null;
249
+ // 2.3.4.13: the verifier input (saltSize bytes), its hash and the intermediate key, each padded to whole blocks.
250
+ const verifierInput = ciphertext(values.encryptedVerifierHashInput, key.blockSize, key.saltSize);
251
+ const verifierHash = ciphertext(values.encryptedVerifierHashValue, key.blockSize, key.hashSize);
252
+ const encryptedKey = ciphertext(values.encryptedKeyValue, key.blockSize, data.keyBits / 8);
253
+ if (!verifierInput || !verifierHash || !encryptedKey) return null;
254
+ return {
255
+ type: 'agile',
256
+ data,
257
+ key,
258
+ spinCount,
259
+ verifierInput,
260
+ verifierHash,
261
+ encryptedKey,
262
+ integrity: dataIntegrity(fields.dataIntegrity, data),
263
+ };
264
+ }
265
+
266
+ function standardDescriptor(info) {
267
+ if (info.length < 12) return null;
268
+ const headerSize = info.readUInt32LE(8);
269
+ if (headerSize < 32 || 12 + headerSize + 4 > info.length) return null;
270
+ // 2.3.2 EncryptionHeader.Flags: standard encryption sets fCryptoAPI and fAES.
271
+ const flags = info.readUInt32LE(12);
272
+ const keyBits = AES_ALGORITHMS.get(info.readUInt32LE(20));
273
+ const hashAlgorithm = info.readUInt32LE(24);
274
+ if (!(flags & STANDARD_FLAG_CRYPTOAPI) || !(flags & STANDARD_FLAG_AES)) return null;
275
+ if (!keyBits || info.readUInt32LE(28) !== keyBits || ![0, 0x8004].includes(hashAlgorithm)) return null;
276
+ // 2.3.3 EncryptionVerifier: SaltSize (16), Salt, EncryptedVerifier (16), VerifierHashSize (20), and the
277
+ // AES-padded EncryptedVerifierHash (32).
278
+ const at = 12 + headerSize;
279
+ if (info.readUInt32LE(at) !== 16 || at + 4 + 16 + 16 + 4 + 32 > info.length) return null;
280
+ const verifierHashSize = info.readUInt32LE(at + 36);
281
+ if (verifierHashSize !== 20) return null;
282
+ return {
283
+ type: 'standard',
284
+ keyBits,
285
+ salt: info.subarray(at + 4, at + 20),
286
+ verifier: info.subarray(at + 20, at + 36),
287
+ verifierHash: info.subarray(at + 40, at + 72),
288
+ verifierHashSize,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * How an \EncryptionInfo stream encrypts its package, or null when this is not agile or standard encryption with
294
+ * parameters Office writes. The version decides (2.3.4.5, 2.3.4.10): 4.4 with reserved 0x40 is agile, minor 2 with
295
+ * major 2, 3 or 4 is standard; minor 3 (extensible) is unsupported. An agile descriptor's `integrity` is null
296
+ * without a dataIntegrity element and `{ malformed: true }` for one that cannot be checked.
297
+ */
298
+ export function ooxmlEncryption(info) {
299
+ if (!Buffer.isBuffer(info) || info.length < 8) return null;
300
+ const major = info.readUInt16LE(0);
301
+ const minor = info.readUInt16LE(2);
302
+ if (major === 4 && minor === 4) return info.readUInt32LE(4) === 0x40 ? agileDescriptor(info) : null;
303
+ if ([2, 3, 4].includes(major) && minor === 2) return standardDescriptor(info);
304
+ return null;
305
+ }
306
+
307
+ const KEY_WORKER = new URL('./ooxml-key-worker.mjs', import.meta.url);
308
+ // Password derivations that may run at once, each on its own thread; later ones wait for a free one.
309
+ const MAX_KEY_DERIVATIONS = 2;
310
+ let runningDerivations = 0;
311
+ const waitingDerivations = [];
312
+
313
+ /** Password derivations running on their threads and waiting for one (for diagnostics and tests). */
314
+ export const ooxmlKeyDerivations = () => ({ running: runningDerivations, waiting: waitingDerivations.length });
315
+
316
+ function derivationSlot(signal) {
317
+ if (runningDerivations < MAX_KEY_DERIVATIONS) {
318
+ runningDerivations++;
319
+ return Promise.resolve();
320
+ }
321
+ return new Promise((resolve, reject) => {
322
+ const start = () => {
323
+ signal?.removeEventListener('abort', onAbort);
324
+ runningDerivations++;
325
+ resolve();
326
+ };
327
+ const onAbort = () => {
328
+ waitingDerivations.splice(waitingDerivations.indexOf(start), 1);
329
+ reject(signal.reason);
330
+ };
331
+ signal?.addEventListener('abort', onAbort, { once: true });
332
+ waitingDerivations.push(start);
333
+ });
334
+ }
335
+
336
+ function releaseDerivationSlot() {
337
+ runningDerivations--;
338
+ waitingDerivations.shift()?.();
339
+ }
340
+
341
+ // 2.3.4.7 / 2.3.4.11: H0 = H(salt + password), Hn = H(iterator + Hn-1), computed on a worker thread
342
+ // (ooxml-key-worker.mjs). An abort ends the thread and rejects with the signal's reason; the thread's slot is free
343
+ // again once it has exited, however it ends.
344
+ async function iteratedHash(algorithm, salt, password, spinCount, signal) {
345
+ signal?.throwIfAborted();
346
+ await derivationSlot(signal);
347
+ let worker;
348
+ try {
349
+ signal?.throwIfAborted();
350
+ worker = new Worker(KEY_WORKER, {
351
+ execArgv: workerExecArgv(),
352
+ workerData: { algorithm, salt, password: String(password), spinCount },
353
+ });
354
+ } catch (error) {
355
+ releaseDerivationSlot();
356
+ throw error;
357
+ }
358
+ worker.once('exit', releaseDerivationSlot);
359
+ return await new Promise((resolve, reject) => {
360
+ const settle = (callback, value) => {
361
+ signal?.removeEventListener('abort', onAbort);
362
+ callback(value);
363
+ };
364
+ const onAbort = () => {
365
+ void worker.terminate();
366
+ settle(reject, signal.reason);
367
+ };
368
+ signal?.addEventListener('abort', onAbort, { once: true });
369
+ worker.once('message', (value) => settle(resolve, Buffer.from(value.buffer, value.byteOffset, value.byteLength)));
370
+ worker.once('error', (error) => settle(reject, error));
371
+ worker.once('exit', (code) =>
372
+ settle(reject, new Error(`The password derivation ended early (exit code ${code}).`))
373
+ );
374
+ });
375
+ }
376
+
377
+ async function agileKey(descriptor, password, signal) {
378
+ const { key } = descriptor;
379
+ const spun = await iteratedHash(key.algorithm, key.salt, password, descriptor.spinCount, signal);
380
+ const iv = fitted(key.salt, key.blockSize);
381
+ const open = (block, data) =>
382
+ decrypt('cbc', key.keyBits, fitted(hash(key.algorithm, spun, block), key.keyBits / 8), iv, data);
383
+ // 2.3.4.13: the decrypted verifier input, hashed, must equal the decrypted verifier hash.
384
+ const input = open(VERIFIER_INPUT_BLOCK, descriptor.verifierInput).subarray(0, key.saltSize);
385
+ const expected = hash(key.algorithm, input);
386
+ const actual = open(VERIFIER_HASH_BLOCK, descriptor.verifierHash).subarray(0, key.hashSize);
387
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return null;
388
+ // 2.3.4.13: the intermediate key that encrypts the package.
389
+ return open(ENCRYPTED_KEY_BLOCK, descriptor.encryptedKey).subarray(0, descriptor.data.keyBits / 8);
390
+ }
391
+
392
+ // 2.3.4.7: the key derived from the iterated SHA-1 hash, and 2.3.4.9: the verifier check.
393
+ async function standardKey(descriptor, password, signal) {
394
+ const spun = await iteratedHash('sha1', descriptor.salt, password, STANDARD_SPIN_COUNT, signal);
395
+ const final = hash('sha1', spun, uint32(0));
396
+ const derive = (fill) => {
397
+ const buffer = Buffer.alloc(64, fill);
398
+ for (let index = 0; index < final.length; index++) buffer[index] ^= final[index];
399
+ return hash('sha1', buffer);
400
+ };
401
+ const key = Buffer.concat([derive(0x36), derive(0x5c)]).subarray(0, descriptor.keyBits / 8);
402
+ const verifier = decrypt('ecb', descriptor.keyBits, key, null, descriptor.verifier);
403
+ const expected = hash('sha1', verifier);
404
+ const actual = decrypt('ecb', descriptor.keyBits, key, null, descriptor.verifierHash).subarray(
405
+ 0,
406
+ descriptor.verifierHashSize
407
+ );
408
+ return timingSafeEqual(actual, expected) ? key : null;
409
+ }
410
+
411
+ /**
412
+ * The key that decrypts the package when `password` is right, else null. The verifier alone decides; the package
413
+ * is not touched. The password hashing runs off this thread, at most two at once, and an abort of `signal` ends it
414
+ * and rejects with the signal's reason.
415
+ */
416
+ export async function ooxmlPasswordKey(descriptor, password, { signal = null } = {}) {
417
+ return descriptor.type === 'agile'
418
+ ? await agileKey(descriptor, password, signal)
419
+ : await standardKey(descriptor, password, signal);
420
+ }
421
+
422
+ /**
423
+ * 2.3.4.14: whether \EncryptedPackage is the stream the descriptor's HMAC was computed over. The HMAC key (the
424
+ * salt) and value are decrypted with the package key and IVs H(keyData salt + block key) (2.3.4.12), and the HMAC
425
+ * covers the whole stream, its StreamSize field included. Standard encryption and an agile descriptor without
426
+ * dataIntegrity carry nothing to check (the schema makes the element optional); a malformed one never passes.
427
+ */
428
+ export function ooxmlPackageIntact(descriptor, key, encryptedPackage) {
429
+ const { integrity } = descriptor;
430
+ if (descriptor.type !== 'agile' || !integrity) return true;
431
+ if (integrity.malformed) return false;
432
+ const { data } = descriptor;
433
+ const open = (block, value) =>
434
+ decrypt('cbc', data.keyBits, key, fitted(hash(data.algorithm, data.salt, block), data.blockSize), value);
435
+ // The HMAC key is the salt without its block padding (its length read in dataIntegrity), the HMAC the first
436
+ // hashSize bytes.
437
+ const hmacKey = open(INTEGRITY_KEY_BLOCK, integrity.hmacKey).subarray(0, integrity.saltLength);
438
+ const expected = open(INTEGRITY_VALUE_BLOCK, integrity.hmacValue).subarray(0, data.hashSize);
439
+ const actual = createHmac(data.algorithm, hmacKey).update(encryptedPackage).digest();
440
+ return timingSafeEqual(actual, expected);
441
+ }
442
+
443
+ /**
444
+ * 2.3.4.15 (agile) / 2.3.4.4 (standard): the plain package. \EncryptedPackage starts with the 8-byte StreamSize of
445
+ * the plain package; agile ciphertext is 4096-byte segments, each with IV H(keyData salt + segment index), standard
446
+ * ciphertext is one AES-ECB run.
447
+ */
448
+ export function decryptOoxmlPackage(descriptor, key, encryptedPackage) {
449
+ if (encryptedPackage.length < 8) throw new Error('The encrypted package is truncated.');
450
+ const size = Number(encryptedPackage.readBigUInt64LE(0));
451
+ const body = encryptedPackage.subarray(8);
452
+ if (size > body.length) throw new Error('The encrypted package is truncated.');
453
+ let plain;
454
+ if (descriptor.type === 'agile') {
455
+ const { data } = descriptor;
456
+ const parts = [];
457
+ for (let offset = 0, index = 0; offset < body.length; offset += SEGMENT_LENGTH, index++) {
458
+ const segment = body.subarray(offset, offset + SEGMENT_LENGTH);
459
+ const whole = segment.subarray(0, segment.length - (segment.length % data.blockSize));
460
+ const iv = fitted(hash(data.algorithm, data.salt, uint32(index)), data.blockSize);
461
+ parts.push(decrypt('cbc', data.keyBits, key, iv, whole));
462
+ }
463
+ plain = Buffer.concat(parts);
464
+ } else {
465
+ plain = decrypt('ecb', descriptor.keyBits, key, null, body.subarray(0, body.length - (body.length % 16)));
466
+ }
467
+ if (plain.length < size) throw new Error('The encrypted package is truncated.');
468
+ return plain.subarray(0, size);
469
+ }
@@ -0,0 +1,14 @@
1
+ // The password hash iteration of [MS-OFFCRYPTO] 2.3.4.7 (standard) and 2.3.4.11 (agile), on its own thread so up to
2
+ // 10,000,000 hashes never hold the host's event loop; ending the thread cancels it (ooxml-crypto.mjs).
3
+ // H0 = H(salt + password as UTF-16LE), Hn = H(iterator + Hn-1) for iterator 0 .. spinCount - 1.
4
+ import { createHash } from 'node:crypto';
5
+ import { parentPort, workerData } from 'node:worker_threads';
6
+
7
+ const { algorithm, salt, password, spinCount } = workerData;
8
+ let value = createHash(algorithm).update(salt).update(Buffer.from(password, 'utf16le')).digest();
9
+ const iterator = Buffer.alloc(4);
10
+ for (let index = 0; index < spinCount; index++) {
11
+ iterator.writeUInt32LE(index, 0);
12
+ value = createHash(algorithm).update(iterator).update(value).digest();
13
+ }
14
+ parentPort.postMessage(value);
@@ -1,6 +1,6 @@
1
1
  import { basename, dirname, extname, join } from 'node:path';
2
2
  import { homedir, tmpdir } from 'node:os';
3
- import { spawn } from 'node:child_process';
3
+ import { execFile, spawn } from 'node:child_process';
4
4
  import { redactDirectories } from '../../shared/redact-directories.mjs';
5
5
  import { removeDirectoryWithRetry } from '../../shared/remove-directory.mjs';
6
6
  import { constants as fsConstants, rmSync } from 'node:fs';
@@ -146,8 +146,16 @@ function libreOfficeCandidates() {
146
146
  // exits once the conversion finishes. soffice.exe returns early or never,
147
147
  // so a caller awaiting its exit cannot tell when output is ready.
148
148
  'soffice.com',
149
- 'C:\\Program Files\\LibreOffice\\program\\soffice.com',
150
- 'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.com',
149
+ ...new Set([
150
+ join(process.env.ProgramFiles || 'C:\\Program Files', 'LibreOffice', 'program', 'soffice.com'),
151
+ join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'LibreOffice', 'program', 'soffice.com'),
152
+ 'C:\\Program Files\\LibreOffice\\program\\soffice.com',
153
+ 'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.com',
154
+ // A per-user install (the MSI's "just for me" choice).
155
+ ...(process.env.LOCALAPPDATA
156
+ ? [join(process.env.LOCALAPPDATA, 'Programs', 'LibreOffice', 'program', 'soffice.com')]
157
+ : []),
158
+ ]),
151
159
  ];
152
160
  }
153
161
  if (process.platform === 'darwin') {
@@ -169,11 +177,43 @@ function libreOfficeCandidates() {
169
177
  ];
170
178
  }
171
179
 
180
+ // The LibreOffice installer records its `program` folder as the default value
181
+ // of SOFTWARE\LibreOffice\UNO\InstallPath, in HKLM (all users, 64- or 32-bit
182
+ // view) or HKCU (per-user). This covers installs on another drive or in a
183
+ // custom folder. The value's display name is localized, so only the REG_SZ
184
+ // data is read.
185
+ const LIBREOFFICE_REGISTRY_QUERIES = [
186
+ ['HKLM', '/reg:64'],
187
+ ['HKLM', '/reg:32'],
188
+ ['HKCU', '/reg:64'],
189
+ ];
190
+
191
+ /** The `soffice.com` paths named by `reg query ... /ve` output. */
192
+ export function registrySofficePrograms(output) {
193
+ return String(output || '')
194
+ .split(/\r?\n/)
195
+ .map((line) => /REG_(?:EXPAND_)?SZ\s+(.+?)\s*$/.exec(line)?.[1])
196
+ .filter(Boolean)
197
+ .map((directory) => join(directory, 'soffice.com'));
198
+ }
199
+
200
+ function queryRegistry(args) {
201
+ return new Promise((resolve) => {
202
+ execFile('reg.exe', args, { windowsHide: true, timeout: 5000 }, (error, stdout) => resolve(error ? '' : stdout));
203
+ });
204
+ }
205
+
172
206
  async function findLibreOfficeProgram() {
173
- const candidates = libreOfficeCandidates();
174
- for (const candidate of candidates) {
207
+ for (const candidate of libreOfficeCandidates()) {
175
208
  if (await commandExists(candidate)) return candidate;
176
209
  }
210
+ if (process.platform !== 'win32') return '';
211
+ for (const [hive, view] of LIBREOFFICE_REGISTRY_QUERIES) {
212
+ const output = await queryRegistry(['query', `${hive}\\SOFTWARE\\LibreOffice\\UNO\\InstallPath`, '/ve', view]);
213
+ for (const candidate of registrySofficePrograms(output)) {
214
+ if (await commandExists(candidate)) return candidate;
215
+ }
216
+ }
177
217
  return '';
178
218
  }
179
219
 
@@ -20,16 +20,76 @@ export const hopOf = (value) => (Number.isInteger(value) && value > 0 ? value :
20
20
  * depth: bot-descended work resumes where it was, independent work at 0. */
21
21
  export const sendDepth = ({ hop, target } = {}) => Math.max(0, hopOf(hop) - (target === 'session' ? 1 : 0));
22
22
 
23
- /** Stamp the hop of the submission that starts the turn (else 0) on `session`. */
23
+ // The bots of a chain (`botChain`): the agent ids that handed the work on,
24
+ // oldest first, the sender last. A bot's send carries the chain of its turn
25
+ // plus itself in the transcript meta of the message it delivers (like `hop`);
26
+ // the turn that message starts or joins is in that chain (`turnBotChain`), and
27
+ // a send or spawn from it may never reach a bot already in it, nor itself
28
+ // (assertBotChainTarget). A turn the user, a routine or a notification
29
+ // started begins a chain of its own.
30
+ export const botChainOf = (value) =>
31
+ Array.isArray(value) ? [...new Set(value.filter((id) => typeof id === 'string' && id))] : [];
32
+
33
+ // The chain of a submission the runtime takes without transcript meta of its
34
+ // own (a group-room member's turn, submitted by the desktop room host under
35
+ // the send's submission id), recorded by the sender before it is submitted.
36
+ const chainsBySubmission = new Map();
37
+ const MAX_SUBMISSION_CHAINS = 1000;
38
+
39
+ /** Record that submission `submissionId` starts or joins a turn of `chain`. */
40
+ export function rememberSubmissionBotChain(submissionId, chain) {
41
+ const id = String(submissionId || '').trim();
42
+ const bots = botChainOf(chain);
43
+ if (!id || !bots.length) return;
44
+ chainsBySubmission.set(id, bots);
45
+ while (chainsBySubmission.size > MAX_SUBMISSION_CHAINS) {
46
+ chainsBySubmission.delete(chainsBySubmission.keys().next().value);
47
+ }
48
+ }
49
+
50
+ /** The chain a submission's transcript meta carries, else the one recorded for its ids. */
51
+ function submissionBotChain(transcriptMeta) {
52
+ const carried = botChainOf(transcriptMeta?.botChain);
53
+ if (carried.length) return carried;
54
+ const merged = Array.isArray(transcriptMeta?.submissionIds) ? transcriptMeta.submissionIds : [];
55
+ const ids = [transcriptMeta?.checkpointId, ...merged];
56
+ return botChainOf(ids.flatMap((id) => chainsBySubmission.get(String(id || '')) || []));
57
+ }
58
+
59
+ /** Stamp the hop and bot chain of the submission that starts the turn (else 0 and none) on `session`. */
24
60
  export function stampTurnHop(session, transcriptMeta) {
25
61
  session.turnHop = hopOf(transcriptMeta?.hop);
62
+ session.turnBotChain = submissionBotChain(transcriptMeta);
26
63
  }
27
64
 
28
- /** A bot's message steered into the running turn raises the turn's hop. */
65
+ /** A bot's message steered into the running turn raises the turn's hop and joins its chain. */
29
66
  export function raiseTurnHop(session, transcriptMeta) {
30
67
  const hop = hopOf(transcriptMeta?.hop);
31
68
  if (session && hop > hopOf(session.turnHop)) session.turnHop = hop;
69
+ const chain = submissionBotChain(transcriptMeta);
70
+ if (session && chain.length) session.turnBotChain = botChainOf([...turnBotChainOf(session), ...chain]);
32
71
  }
33
72
 
34
73
  /** The hop of the turn `session` is in. */
35
74
  export const turnHopOf = (session) => hopOf(session?.turnHop);
75
+
76
+ /** The bot chain of the turn `session` is in. */
77
+ export const turnBotChainOf = (session) => botChainOf(session?.turnBotChain);
78
+
79
+ /** The one check of every bot-to-bot hand-off (session send, a room seat,
80
+ * a contact agent spawn): `target`, the bot the work would reach, is
81
+ * neither `self` (the bot handing it on) nor already in `chain`. */
82
+ export function assertBotChainTarget(target, { chain = [], self = '' } = {}, prefix = 'session') {
83
+ if (!target) return;
84
+ if (target === self) {
85
+ throw new Error(
86
+ `${prefix}: the contact agent "${target}" cannot hand work to itself; do it yourself or delegate to a sub-agent`
87
+ );
88
+ }
89
+ if (chain.includes(target)) {
90
+ const path = [...chain, ...(self && chain.at(-1) !== self ? [self] : []), target];
91
+ throw new Error(
92
+ `${prefix}: the contact agent "${target}" is already in this delegation chain (${path.join(' → ')}); do not hand the work back, answer or delegate to a sub-agent instead`
93
+ );
94
+ }
95
+ }