@qvac/harness 0.7.0 → 1.0.0-rc.0

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 (204) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +239 -2
  3. package/dist-lib/index.d.ts +58 -3
  4. package/dist-lib/index.js +736 -239
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  6. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  44. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  46. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  48. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  50. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  52. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  54. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  56. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  58. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  60. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  62. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  64. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  66. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  67. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  68. package/dist-lib/lib/capabilities/completion.js +20 -1301
  69. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  70. package/dist-lib/lib/capabilities/download.js +7 -2
  71. package/dist-lib/lib/capabilities/invoke.js +5 -3
  72. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  73. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  74. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  75. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  76. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  77. package/dist-lib/lib/capabilities/types.js +1 -0
  78. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  79. package/dist-lib/lib/completion-lifecycle.js +126 -0
  80. package/dist-lib/lib/connect.d.ts +10 -3
  81. package/dist-lib/lib/connect.js +329 -23
  82. package/dist-lib/lib/contract-types.d.ts +19 -2
  83. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  84. package/dist-lib/lib/harness-lifecycle.js +129 -0
  85. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  86. package/dist-lib/lib/helpers/detach-json.js +146 -0
  87. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  88. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  89. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  90. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  91. package/dist-lib/lib/helpers/race-abort.js +2 -7
  92. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  93. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  94. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  95. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  96. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  97. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  98. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  99. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  101. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  102. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  103. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  105. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  106. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  107. package/dist-lib/lib/hooks/pipeline.js +24 -2
  108. package/dist-lib/lib/hooks/run.d.ts +2 -1
  109. package/dist-lib/lib/hooks/run.js +19 -7
  110. package/dist-lib/lib/hooks/types.d.ts +17 -6
  111. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  112. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  114. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  116. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  118. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  119. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  120. package/dist-lib/lib/machine/completion/context.js +44 -0
  121. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  122. package/dist-lib/lib/machine/completion/contract.js +1 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  124. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  125. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  126. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  127. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  128. package/dist-lib/lib/machine/completion/machine.js +511 -0
  129. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  130. package/dist-lib/lib/machine/completion/ports.js +224 -0
  131. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  132. package/dist-lib/lib/machine/completion/rules.js +115 -0
  133. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  134. package/dist-lib/lib/machine/completion/types.js +1 -0
  135. package/dist-lib/lib/machine/engine.d.ts +93 -0
  136. package/dist-lib/lib/machine/engine.js +483 -0
  137. package/dist-lib/lib/machine/errors.d.ts +44 -0
  138. package/dist-lib/lib/machine/errors.js +373 -0
  139. package/dist-lib/lib/machine/guards.d.ts +2 -0
  140. package/dist-lib/lib/machine/guards.js +6 -0
  141. package/dist-lib/lib/machine/index.d.ts +20 -0
  142. package/dist-lib/lib/machine/index.js +11 -0
  143. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  144. package/dist-lib/lib/machine/replay-output.js +50 -0
  145. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  146. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  147. package/dist-lib/lib/models/model-handle.d.ts +5 -1
  148. package/dist-lib/lib/models/model-handle.js +8 -3
  149. package/dist-lib/lib/models/model-list.d.ts +5 -3
  150. package/dist-lib/lib/models/model-pool.d.ts +18 -7
  151. package/dist-lib/lib/models/model-pool.js +126 -38
  152. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  153. package/dist-lib/lib/run-log/history-codec.js +973 -0
  154. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  155. package/dist-lib/lib/run-log/patch.js +93 -0
  156. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  157. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  158. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  159. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  160. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  161. package/dist-lib/lib/run-log/protocol.js +1056 -0
  162. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  163. package/dist-lib/lib/run-log/replay.js +129 -0
  164. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  165. package/dist-lib/lib/run-log/run-journal.js +783 -0
  166. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  167. package/dist-lib/lib/run-log-inspect.js +627 -0
  168. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  169. package/dist-lib/lib/run-log-protocol.js +1 -0
  170. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  171. package/dist-lib/lib/run-log-redact.js +1107 -0
  172. package/dist-lib/lib/serve.js +308 -12
  173. package/dist-lib/lib/spawn.js +3 -2
  174. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  175. package/dist-lib/lib/tools/exec.d.ts +2 -1
  176. package/dist-lib/lib/tools/exec.js +57 -2
  177. package/dist-lib/lib/tools/generate-image.js +1 -1
  178. package/dist-lib/lib/tools/generate-music.js +1 -1
  179. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  180. package/dist-lib/lib/tools/http-request.js +2 -1
  181. package/dist-lib/lib/tools/mcp-call.js +8 -1
  182. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  183. package/dist-lib/lib/tools/operations/builders.js +1 -0
  184. package/dist-lib/lib/tools/operations/compile.js +11 -1
  185. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  186. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  187. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  188. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  189. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  190. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  191. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  192. package/dist-lib/lib/wire.d.ts +5 -0
  193. package/dist-lib/lib/wire.js +128 -5
  194. package/dist-lib/schema/harness.js +22 -6
  195. package/dist-lib/spec/hrpc/messages.js +100 -18
  196. package/dist-lib/spec/hyperschema/index.js +100 -18
  197. package/dist-lib/spec/hyperschema/schema.json +74 -1
  198. package/dist-lib/spec/hyperschema/types.d.ts +11 -3
  199. package/package.json +19 -1
  200. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  201. package/patches/bare-stow@0.1.5.patch +68 -0
  202. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  203. package/patches/localwatch@1.4.3.patch +13 -0
  204. package/patches/pyodide@314.0.2.patch +144 -0
@@ -1,5 +1,5 @@
1
- import { AbortController } from 'bare-abort-controller';
2
1
  import b4a from 'b4a';
2
+ import { AbortController } from 'bare-abort-controller';
3
3
  import fs from 'fs';
4
4
  import { settleOnClose } from './rpc-session.js';
5
5
  import { pipeline, Readable } from 'streamx';
@@ -8,7 +8,8 @@ import HRPC from '../spec/hrpc/index.js';
8
8
  import { isCapability } from './capabilities/invoke.js';
9
9
  import { CUSTOM_TASK_TYPES } from './models/model-catalog.js';
10
10
  import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
11
- import { errorMessage } from './guards.js';
11
+ import { validateEngineBoot } from './run-log-protocol.js';
12
+ import { errorMessage, isRecord } from './guards.js';
12
13
  import { hlog } from './log.js';
13
14
  import { pushQueue } from './queue.js';
14
15
  import { TextCoalescer } from './helpers/text-coalescer.js';
@@ -30,6 +31,168 @@ const WIRE_HIGH_WATER_MARK = 64;
30
31
  const TEXT_COALESCE_CAP = 8192;
31
32
  const TEXT_COALESCE_MS = 60;
32
33
  function noop() { }
34
+ function decodeDigestKey(value) {
35
+ if (typeof value !== 'string' || !/^[0-9a-f]{64}$/i.test(value)) {
36
+ return undefined;
37
+ }
38
+ return b4a.from(value, 'hex');
39
+ }
40
+ function isEngineBoot(value) {
41
+ if (!isEngineBootEnvelope(value)) {
42
+ return false;
43
+ }
44
+ try {
45
+ validateEngineBoot(value);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ function isEngineBootEnvelope(value) {
53
+ if (!isRecord(value)) {
54
+ return false;
55
+ }
56
+ const counter = (name) => typeof value[name] === 'number' && Number.isSafeInteger(value[name]) && value[name] >= 0;
57
+ const metadata = value.metadata;
58
+ const hasMetadata = metadata === null ||
59
+ (isRecord(metadata) && metadata.version === 5 && metadata.type === 'metadata');
60
+ return (value.protocolVersion === 1 &&
61
+ counter('length') &&
62
+ counter('nextSeq') &&
63
+ counter('deltasSinceCheckpoint') &&
64
+ counter('unredactedLength') &&
65
+ (value.machineVersion === null || typeof value.machineVersion === 'number') &&
66
+ (value.xstateVersion === null || typeof value.xstateVersion === 'string') &&
67
+ hasMetadata &&
68
+ 'eventState' in value &&
69
+ (value.snapshotSavedAt === undefined || typeof value.snapshotSavedAt === 'number') &&
70
+ (value.firstSavedAt === undefined || typeof value.firstSavedAt === 'number') &&
71
+ (value.redactedPendingArgs === null || typeof value.redactedPendingArgs === 'boolean') &&
72
+ typeof value.restoredRedacted === 'boolean' &&
73
+ Array.isArray(value.forkBeforeWrite) &&
74
+ Array.isArray(value.receipts) &&
75
+ Array.isArray(value.intentArgs));
76
+ }
77
+ const EMPTY_IDENTITY_FIELDS = new Set([
78
+ 'model',
79
+ 'cacheKey',
80
+ 'thinkingDefault',
81
+ 'agent.id',
82
+ 'agent.name',
83
+ 'agent.bio'
84
+ ]);
85
+ function startPayloadFrom(value) {
86
+ if (!isRecord(value)) {
87
+ return {};
88
+ }
89
+ const payload = {};
90
+ if (value.resume === null || typeof value.resume === 'boolean') {
91
+ payload.resume = value.resume;
92
+ }
93
+ if (value.recoveryEvents === null || typeof value.recoveryEvents === 'boolean') {
94
+ payload.recoveryEvents = value.recoveryEvents;
95
+ }
96
+ if (Object.hasOwn(value, 'explicitEmptyIdentity')) {
97
+ if (!Array.isArray(value.explicitEmptyIdentity) ||
98
+ value.explicitEmptyIdentity.length > EMPTY_IDENTITY_FIELDS.size) {
99
+ throw new TypeError('Invalid explicit-empty identity marker');
100
+ }
101
+ const fields = new Set();
102
+ for (const field of value.explicitEmptyIdentity) {
103
+ if (typeof field !== 'string' || !EMPTY_IDENTITY_FIELDS.has(field)) {
104
+ throw new TypeError('Invalid explicit-empty identity marker');
105
+ }
106
+ fields.add(field);
107
+ }
108
+ if (fields.size) {
109
+ payload.explicitEmptyIdentity = [...fields];
110
+ }
111
+ }
112
+ if (Array.isArray(value.resolutions)) {
113
+ payload.resolutions = value.resolutions.map((resolution) => {
114
+ if (!isRecord(resolution) || typeof resolution.operationId !== 'string') {
115
+ throw new TypeError('Invalid run recovery resolution');
116
+ }
117
+ if (resolution.decision === 'retry') {
118
+ return { operationId: resolution.operationId, decision: 'retry' };
119
+ }
120
+ if (resolution.decision === 'result' && 'result' in resolution) {
121
+ return {
122
+ operationId: resolution.operationId,
123
+ decision: 'result',
124
+ result: resolution.result
125
+ };
126
+ }
127
+ if (resolution.decision === 'skip') {
128
+ return { operationId: resolution.operationId, decision: 'skip' };
129
+ }
130
+ throw new TypeError('Invalid run recovery resolution');
131
+ });
132
+ }
133
+ if (value.runLog === null) {
134
+ payload.runLog = null;
135
+ }
136
+ else if (value.runLog) {
137
+ const runLog = isRecord(value.runLog) ? value.runLog : {};
138
+ payload.runLog = {
139
+ boot: runLog.boot,
140
+ digestKey: runLog.digestKey,
141
+ hostAppendTimeoutMs: typeof runLog.hostAppendTimeoutMs === 'number' ? runLog.hostAppendTimeoutMs : undefined
142
+ };
143
+ }
144
+ return payload;
145
+ }
146
+ function assertEmptyIdentityMarkersMatchFrame(frame, fields) {
147
+ for (const field of fields) {
148
+ let value;
149
+ switch (field) {
150
+ case 'model':
151
+ value = frame.model;
152
+ break;
153
+ case 'cacheKey':
154
+ value = frame.cacheKey;
155
+ break;
156
+ case 'thinkingDefault':
157
+ value = frame.thinkingDefault;
158
+ break;
159
+ case 'agent.id':
160
+ if (frame.agent === null || frame.agent === undefined) {
161
+ throw new TypeError('Explicit-empty agent identity requires an agent frame');
162
+ }
163
+ value = frame.agent?.id;
164
+ break;
165
+ case 'agent.name':
166
+ if (frame.agent === null || frame.agent === undefined) {
167
+ throw new TypeError('Explicit-empty agent identity requires an agent frame');
168
+ }
169
+ value = frame.agent?.name;
170
+ break;
171
+ case 'agent.bio':
172
+ if (frame.agent === null || frame.agent === undefined) {
173
+ throw new TypeError('Explicit-empty agent identity requires an agent frame');
174
+ }
175
+ value = frame.agent?.bio;
176
+ break;
177
+ }
178
+ if (value !== null && value !== undefined && value !== '') {
179
+ throw new TypeError('Conflicting explicit-empty identity marker');
180
+ }
181
+ }
182
+ }
183
+ function normalizeAgentKnobs(agent) {
184
+ const normalized = { ...agent };
185
+ if (normalized.id === null || normalized.id === undefined) {
186
+ delete normalized.id;
187
+ }
188
+ if (normalized.name === null || normalized.name === undefined) {
189
+ delete normalized.name;
190
+ }
191
+ if (normalized.bio === null || normalized.bio === undefined) {
192
+ delete normalized.bio;
193
+ }
194
+ return normalized;
195
+ }
33
196
  function tick() {
34
197
  return new Promise((resolve) => setTimeout(resolve, 0));
35
198
  }
@@ -270,7 +433,7 @@ function modelEvents(harness) {
270
433
  catalogChanged,
271
434
  ...loaded.map((entry) => `${entry.model}:${entry.refs}`)
272
435
  ].join('\0');
273
- if (key === last) {
436
+ if (key === last && !catalogChanged) {
274
437
  return;
275
438
  }
276
439
  last = key;
@@ -358,10 +521,13 @@ function completion(harness) {
358
521
  const pendingRag = new Map();
359
522
  const pendingQuestion = new Map();
360
523
  const pendingDrive = new Map();
524
+ const pendingRunLog = new Map();
361
525
  const controller = new AbortController();
362
526
  let calls = 0;
363
527
  let started = false;
528
+ let clientGone = false;
364
529
  const gone = () => {
530
+ clientGone = true;
365
531
  controller.abort('client gone');
366
532
  for (const resolve of pending.values()) {
367
533
  resolve(false);
@@ -391,6 +557,10 @@ function completion(harness) {
391
557
  reject(new Error('client gone'));
392
558
  }
393
559
  pendingDrive.clear();
560
+ for (const { reject } of pendingRunLog.values()) {
561
+ reject(new Error('client gone'));
562
+ }
563
+ pendingRunLog.clear();
394
564
  events.end();
395
565
  };
396
566
  duplex.on('close', gone);
@@ -501,15 +671,82 @@ function completion(harness) {
501
671
  waiter.resolve(frame.payload ?? {});
502
672
  return;
503
673
  }
674
+ if (frame.type === 'pause') {
675
+ controller.abort('paused');
676
+ return;
677
+ }
678
+ if (frame.type === 'run-log-result') {
679
+ if (typeof frame.callId !== 'string') {
680
+ return;
681
+ }
682
+ const waiter = pendingRunLog.get(frame.callId);
683
+ pendingRunLog.delete(frame.callId);
684
+ if (!waiter) {
685
+ return;
686
+ }
687
+ if (frame.message) {
688
+ return waiter.reject(new Error(frame.message));
689
+ }
690
+ waiter.resolve();
691
+ return;
692
+ }
504
693
  if (started || (frame.type !== 'start' && frame.type !== 'invoke')) {
505
694
  return;
506
695
  }
507
696
  started = true;
697
+ let startPayload;
698
+ try {
699
+ startPayload = startPayloadFrom(frame.payload);
700
+ assertEmptyIdentityMarkersMatchFrame(frame, startPayload.explicitEmptyIdentity ?? []);
701
+ }
702
+ catch (err) {
703
+ events.push({ type: 'error', message: errorMessage(err) });
704
+ events.push({ type: 'done' });
705
+ events.end();
706
+ return;
707
+ }
708
+ const explicitEmptyIdentity = new Set(startPayload.explicitEmptyIdentity ?? []);
709
+ const runLog = startPayload.runLog;
710
+ const resuming = startPayload.resume === true ||
711
+ Boolean(isEngineBoot(runLog?.boot) && runLog.boot.metadata !== null);
712
+ const requestDigestKey = runLog ? decodeDigestKey(runLog.digestKey) : undefined;
713
+ const agent = normalizeAgentKnobs(frame.agent ?? {});
714
+ if ((frame.agent?.id === null || frame.agent?.id === undefined) &&
715
+ explicitEmptyIdentity.has('agent.id')) {
716
+ agent.id = '';
717
+ }
718
+ if ((frame.agent?.name === null || frame.agent?.name === undefined) &&
719
+ explicitEmptyIdentity.has('agent.name')) {
720
+ agent.name = '';
721
+ }
722
+ if ((frame.agent?.bio === null || frame.agent?.bio === undefined) &&
723
+ explicitEmptyIdentity.has('agent.bio')) {
724
+ agent.bio = '';
725
+ }
726
+ const onStep = runLog
727
+ ? (step) => {
728
+ if (clientGone) {
729
+ return Promise.reject(new Error('client gone'));
730
+ }
731
+ return new Promise((resolve, reject) => {
732
+ const callId = String(calls++);
733
+ pendingRunLog.set(callId, { resolve, reject });
734
+ events.push({ type: 'run-log-append', callId, payload: step });
735
+ });
736
+ }
737
+ : undefined;
508
738
  const context = {
739
+ runId: frame.runId || undefined,
740
+ resume: resuming,
741
+ recovery: startPayload.resolutions ?? undefined,
742
+ boot: isEngineBoot(runLog?.boot) ? runLog.boot : undefined,
743
+ onStep,
744
+ requestDigestKey,
745
+ hostAppendTimeoutMs: runLog?.hostAppendTimeoutMs,
509
746
  model: frame.model ?? '',
510
- agent: frame.agent ?? {},
511
- cacheKey: frame.cacheKey ?? undefined,
512
- thinkingDefault: frame.thinkingDefault ?? undefined,
747
+ agent,
748
+ cacheKey: frame.cacheKey ?? (explicitEmptyIdentity.has('cacheKey') ? '' : undefined),
749
+ thinkingDefault: frame.thinkingDefault ?? (explicitEmptyIdentity.has('thinkingDefault') ? '' : undefined),
513
750
  generationParams: frame.generationParams ?? undefined,
514
751
  responseFormat: frame.responseFormat ?? undefined,
515
752
  captureInferenceCalls: frame.captureInferenceCalls ?? undefined,
@@ -546,7 +783,7 @@ function completion(harness) {
546
783
  approve: (call) => new Promise((resolve) => {
547
784
  const callId = String(calls++);
548
785
  pending.set(callId, resolve);
549
- events.push({
786
+ const frame = {
550
787
  type: 'approval-ask',
551
788
  callId,
552
789
  name: call.name,
@@ -555,7 +792,11 @@ function completion(harness) {
555
792
  effect: call.effect,
556
793
  intent: call.intent ?? undefined,
557
794
  display: call.display ?? undefined
558
- });
795
+ };
796
+ if (call.callId !== undefined || call.round !== undefined) {
797
+ frame.payload = { callId: call.callId, round: call.round };
798
+ }
799
+ events.push(frame);
559
800
  }),
560
801
  attach: async (input) => {
561
802
  const staged = await stageFile('attach', input.bytes);
@@ -613,19 +854,62 @@ function completion(harness) {
613
854
  });
614
855
  })
615
856
  };
857
+ if (resuming) {
858
+ for (const key of [
859
+ 'model',
860
+ 'agent',
861
+ 'messages',
862
+ 'cacheKey',
863
+ 'thinkingDefault',
864
+ 'generationParams',
865
+ 'responseFormat'
866
+ ]) {
867
+ const value = frame[key];
868
+ const explicitEmpty = (key === 'model' &&
869
+ (frame.model === null || frame.model === undefined) &&
870
+ explicitEmptyIdentity.has('model')) ||
871
+ (key === 'cacheKey' &&
872
+ (frame.cacheKey === null || frame.cacheKey === undefined) &&
873
+ explicitEmptyIdentity.has('cacheKey')) ||
874
+ (key === 'thinkingDefault' &&
875
+ (frame.thinkingDefault === null || frame.thinkingDefault === undefined) &&
876
+ explicitEmptyIdentity.has('thinkingDefault'));
877
+ if ((value === null || value === undefined || value === '') && !explicitEmpty) {
878
+ const mutableContext = context;
879
+ delete mutableContext[key];
880
+ }
881
+ }
882
+ }
616
883
  void (async () => {
617
884
  let produced = 0;
618
885
  let frames = 0;
886
+ const textIdentity = {};
619
887
  const text = new TextCoalescer((type, merged) => {
620
888
  frames++;
621
- return events.push({ type, text: merged });
889
+ return events.push({ type, text: merged, ...textIdentity });
622
890
  }, { cap: TEXT_COALESCE_CAP, holdMs: TEXT_COALESCE_MS });
623
891
  try {
892
+ if (runLog && !requestDigestKey) {
893
+ throw new Error('The host did not provide a valid run-log digest key');
894
+ }
895
+ if (runLog?.boot &&
896
+ (!isEngineBoot(runLog.boot) ||
897
+ (runLog.boot.metadata !== null && runLog.boot.metadata.runId !== frame.runId))) {
898
+ throw new Error('The host did not provide a valid run-log boot');
899
+ }
900
+ if (resuming && !onStep && !(await harness.capability?.())?.completionRecovery) {
901
+ throw new Error('This peer does not support persistent completion recovery');
902
+ }
624
903
  for await (const event of runFrame(harness, frame, context)) {
625
904
  produced++;
626
- const wire = toWire(event);
627
- if ((wire.type === 'content' || wire.type === 'thinking') && wire.text) {
628
- await text.add(wire.type, wire.text);
905
+ if (!startPayload.recoveryEvents &&
906
+ ['run-started', 'run-resumed', 'round-reset', 'recovery-required'].includes(event.type)) {
907
+ continue;
908
+ }
909
+ const wire = !startPayload.recoveryEvents && event.type === 'paused'
910
+ ? { type: 'aborted' }
911
+ : toWire(event);
912
+ if (await accumulateTextFrame(wire, textIdentity, text)) {
629
913
  continue;
630
914
  }
631
915
  await text.flush();
@@ -650,6 +934,18 @@ function completion(harness) {
650
934
  hlog('wire handed off');
651
935
  };
652
936
  }
937
+ async function accumulateTextFrame(wire, identity, text) {
938
+ if ((wire.type !== 'content' && wire.type !== 'thinking') || !wire.text) {
939
+ return false;
940
+ }
941
+ if (identity.runId !== wire.runId || identity.round !== wire.round) {
942
+ await text.flush();
943
+ }
944
+ identity.runId = wire.runId;
945
+ identity.round = wire.round;
946
+ await text.add(wire.type, wire.text);
947
+ return true;
948
+ }
653
949
  function downloadStream(harness, req) {
654
950
  const controller = new AbortController();
655
951
  const stream = new Readable({
@@ -47,7 +47,8 @@ export function spawnHarness(opts) {
47
47
  });
48
48
  return connectHarness(async () => {
49
49
  const { default: Sidecar } = await import('bare-sidecar');
50
- const { wrap } = await import('bare-stow/host');
50
+ const stowHost = await import('bare-stow/host');
51
+ const IPC = Reflect.get(stowHost, 'IPC');
51
52
  const sidecar = new Sidecar(opts.entry, [
52
53
  opts.skillsDir ?? '',
53
54
  JSON.stringify(opts.sdkStorage),
@@ -63,7 +64,7 @@ export function spawnHarness(opts) {
63
64
  ]);
64
65
  sidecar.stdout.on('data', (data) => logger?.info('harness', data.toString().trimEnd()));
65
66
  sidecar.stderr.on('data', (data) => logger?.warn('harness', data.toString().trimEnd()));
66
- const ipc = wrap(sidecar);
67
+ const ipc = new IPC(sidecar);
67
68
  const died = new Promise((resolve) => {
68
69
  sidecar.once('exit', (code) => resolve(`harness exited (${code}) before ready`));
69
70
  });
@@ -1,21 +1 @@
1
- export interface ToolCallDisplay {
2
- readonly label: string;
3
- readonly description?: string;
4
- readonly iconEmoji?: string;
5
- }
6
- export type ToolExecutionOutcome = {
7
- readonly kind: 'success';
8
- } | {
9
- readonly kind: 'error';
10
- readonly message?: string;
11
- readonly error?: unknown;
12
- } | {
13
- readonly kind: 'denied';
14
- readonly reason?: string;
15
- } | {
16
- readonly kind: 'blocked';
17
- readonly reason?: string;
18
- } | {
19
- readonly kind: 'guidance';
20
- readonly message: string;
21
- };
1
+ export type { ToolCallDisplay, ToolExecutionOutcome } from './machine/index.ts';
@@ -7,6 +7,7 @@ import type { CredentialStoreFn } from './skills/auth-resolver.ts';
7
7
  import { type SkillSettingsFn } from './skills/skill-settings.ts';
8
8
  export type { ExecResult, KillProcess, SpawnFn } from './exec/runner.ts';
9
9
  export interface ExecToolOptions {
10
+ readonly policyVersion?: string;
10
11
  readonly spawn?: SpawnFn;
11
12
  readonly killProcess?: KillProcess;
12
13
  readonly loadPyodide?: LoadPyodideFn;
@@ -23,4 +24,4 @@ export interface ScriptRun {
23
24
  trusted: boolean;
24
25
  }
25
26
  export declare function resolveScriptRun(rawArgs: Record<string, unknown>, skillsDir: string | undefined): ScriptRun | ExecError | null;
26
- export declare function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
27
+ export declare function createExecTool({ policyVersion, spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
@@ -1,5 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { createHash } from 'crypto';
4
+ import b4a from 'b4a';
3
5
  import { AbortedError } from '../helpers/race-abort.js';
4
6
  import { checkPdfBuildSource, checkBareLengthArgs, checkPptxBuildSource, checkPythonSource, checkUnstagedRead, createPythonAdapter, isPythonLanguage, parseInputs, parseOutputs, parsePackages, parseWorkdir, pinned, runsThroughPyodide, sandboxInstallError, unwrapPythonSource } from './exec/adapters/python/python-adapter.js';
5
7
  import { pythonCreateEffect } from './exec/adapters/python/create-effect.js';
@@ -223,7 +225,39 @@ export function resolveScriptRun(rawArgs, skillsDir) {
223
225
  }
224
226
  let source;
225
227
  try {
226
- source = fs.readFileSync(path.join(skillsDir, skill, script), 'utf8');
228
+ const scriptPath = path.join(skillsDir, skill, script);
229
+ const regularPath = () => {
230
+ let current = skillsDir;
231
+ for (const segment of segments) {
232
+ current = path.join(current, segment);
233
+ if (fs.lstatSync(current).isSymbolicLink()) {
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ };
239
+ if (!regularPath()) {
240
+ return { error: `script not found in skill ${skill}: ${script}` };
241
+ }
242
+ const before = fs.statSync(scriptPath);
243
+ if (!before.isFile()) {
244
+ return { error: `script not found in skill ${skill}: ${script}` };
245
+ }
246
+ const fd = fs.openSync(scriptPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
247
+ try {
248
+ const opened = fs.fstatSync(fd);
249
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
250
+ return { error: `script not found in skill ${skill}: ${script}` };
251
+ }
252
+ source = readScriptSource(fd);
253
+ const current = fs.statSync(scriptPath);
254
+ if (!regularPath() || current.dev !== opened.dev || current.ino !== opened.ino) {
255
+ return { error: `script not found in skill ${skill}: ${script}` };
256
+ }
257
+ }
258
+ finally {
259
+ fs.closeSync(fd);
260
+ }
227
261
  }
228
262
  catch {
229
263
  return { error: `script not found in skill ${skill}: ${script}` };
@@ -236,6 +270,18 @@ export function resolveScriptRun(rawArgs, skillsDir) {
236
270
  const command = `import sys\nsys.argv = ${argv}\n${source}`;
237
271
  return { args: { ...rawArgs, language: 'python', command, scriptArgs }, trusted: true };
238
272
  }
273
+ function readScriptSource(fd) {
274
+ const chunks = [];
275
+ while (true) {
276
+ const buffer = b4a.allocUnsafe(8192);
277
+ const length = fs.readSync(fd, buffer);
278
+ if (length === 0) {
279
+ break;
280
+ }
281
+ chunks.push(buffer.subarray(0, length));
282
+ }
283
+ return b4a.toString(b4a.concat(chunks), 'utf8');
284
+ }
239
285
  const SCRIPT_FILE_ARG = /^[\w][\w.-]*\.(pdf|txt|md|csv|docx|xlsx|pptx|png|jpe?g)$/i;
240
286
  function stagingGap(rawArgs, scriptArgs) {
241
287
  const inputs = parseInputs(rawArgs.inputs);
@@ -301,11 +347,20 @@ async function skillsEnv(skillSettings, ctx) {
301
347
  }
302
348
  return Object.keys(env).length > 0 ? env : undefined;
303
349
  }
304
- export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
350
+ export function createExecTool({ policyVersion, spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
305
351
  const adapters = [createPythonAdapter(disablePyodide, loadPyodide, pythonNetwork)];
306
352
  const fallback = createShellAdapter({ spawn, killProcess, loadRuntime: bareRuntime });
307
353
  return {
308
354
  name: 'exec',
355
+ policyVersion: createHash('sha256')
356
+ .update(JSON.stringify({
357
+ version: 1,
358
+ policyVersion: policyVersion ?? null,
359
+ disablePyodide,
360
+ deniedRoots: [...(deniedRoots ?? [])].sort(),
361
+ skillsDir: skillsDir ?? null
362
+ }))
363
+ .digest('hex'),
309
364
  toolset: 'exec',
310
365
  description: "Execute a shell command on the user's device, or Python via in-process Pyodide. " +
311
366
  'One program per call: chaining, pipes, redirection and shell expansion are rejected. ' +
@@ -110,7 +110,7 @@ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBefore
110
110
  }
111
111
  });
112
112
  try {
113
- const modelId = await Promise.race([loads.track(pending), stall.promise]);
113
+ const modelId = await Promise.race([loads.track(pending, sdk), stall.promise]);
114
114
  onLoaded?.(diffusionProgressName(set));
115
115
  return { modelId, prediction: set.prediction };
116
116
  }
@@ -118,7 +118,7 @@ export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads =
118
118
  modelType: 'audiogen',
119
119
  modelConfig,
120
120
  onProgress: onProgress && ((progress) => onProgress(musicProgressName(stages.dit), progress))
121
- }));
121
+ }), sdk);
122
122
  return { modelId };
123
123
  })().catch((err) => {
124
124
  loading = null;
@@ -12,9 +12,10 @@ export type FetchFn = (url: URL, init: FetchInit) => Promise<{
12
12
  }>;
13
13
  export type { CredentialAllowList, CredentialAllowListFn, CredentialRecord, CredentialStoreFn, CredentialStoreValue } from './skills/auth-resolver.ts';
14
14
  export interface HttpRequestToolOptions {
15
+ policyVersion?: string;
15
16
  fetch?: FetchFn;
16
17
  credentialStore?: CredentialStoreFn;
17
18
  credentialAllowList?: CredentialAllowListFn;
18
19
  resolveHost?: HostResolver;
19
20
  }
20
- export declare function createHttpRequestTool({ fetch: fetchImpl, credentialStore, credentialAllowList, resolveHost }?: HttpRequestToolOptions): ToolDefinition;
21
+ export declare function createHttpRequestTool({ policyVersion, fetch: fetchImpl, credentialStore, credentialAllowList, resolveHost }?: HttpRequestToolOptions): ToolDefinition;
@@ -10,9 +10,10 @@ import { parseAuth, resolveBearerHeader, resolveRedirectBearerHeader } from './s
10
10
  import { recordCredentialHealth } from './skills/credential-health.js';
11
11
  import { resolveOutboundUrl } from './url-guard.js';
12
12
  const MAX_REDIRECTS = 5;
13
- export function createHttpRequestTool({ fetch: fetchImpl, credentialStore, credentialAllowList, resolveHost = lookupHost } = {}) {
13
+ export function createHttpRequestTool({ policyVersion, fetch: fetchImpl, credentialStore, credentialAllowList, resolveHost = lookupHost } = {}) {
14
14
  return {
15
15
  name: 'http_request',
16
+ policyVersion,
16
17
  toolset: 'http-request',
17
18
  description: 'Make an outbound HTTP or HTTPS request and return the response status and body (truncated) as text. Authorization/Cookie headers are stripped before sending. Stored skill credentials are attached automatically for their allow-listed hosts; `auth` is only needed to force a specific credential key. For APIs and text responses only: a URL that serves a file — PDF, .docx, .xlsx, an image, any binary — comes back as truncated garbage here. Use web_fetch for those; it downloads and attaches the file.',
18
19
  parameters: {
@@ -3,8 +3,9 @@ import { AbortController } from 'bare-abort-controller';
3
3
  import { MCP_CALL_TOOL_NAME } from '../capabilities/tool-names.js';
4
4
  import { lookupHost } from '../dns-lookup.js';
5
5
  import { AbortedError } from '../helpers/race-abort.js';
6
+ import { writeResult } from '../helpers/run-outcome.js';
6
7
  import { isRedirectStatus, parseObjectArg } from './http-request/request.js';
7
- import { cacheSession, deleteSession, establishSession, flattenMcpResponse, getSession, isNotification, needsSession, normalizeMethod, post, readBody, sessionKey, TIMEOUT_MS } from './mcp/protocol.js';
8
+ import { cacheSession, deleteSession, establishSession, flattenMcpResponse, getSession, isMetadataMethod, isNotification, needsSession, normalizeMethod, post, readBody, sessionKey, TIMEOUT_MS } from './mcp/protocol.js';
8
9
  import { parseAuth, resolveBearerHeader } from './skills/auth-resolver.js';
9
10
  import { recordCredentialHealth } from './skills/credential-health.js';
10
11
  import { resolveOutboundUrl } from './url-guard.js';
@@ -75,6 +76,12 @@ export function createMcpCallTool({ fetch: fetchImpl, credentialStore, credentia
75
76
  return null;
76
77
  }
77
78
  },
79
+ landedWrite(args, output) {
80
+ if (isMetadataMethod(args.method)) {
81
+ return null;
82
+ }
83
+ return writeResult(output);
84
+ },
78
85
  async execute(args, ctx) {
79
86
  const rawParams = parseMcpParams(args.params);
80
87
  if (!rawParams.ok) {
@@ -12,6 +12,7 @@ export type BuilderOutcome = {
12
12
  export interface OperationBuilder {
13
13
  build: (args: Record<string, unknown>) => BuilderOutcome;
14
14
  }
15
+ export declare const BUILDER_POLICY_VERSION = 1;
15
16
  export declare const BUILDER_NAMES: readonly ['gmail-send', 'gmail-draft', 'gmail-get', 'calendar-meet-event', 'calendar-add-meet', 'docs-create', 'docs-append-text', 'sheets-create', 'sheets-write-values', 'sheets-append-values', 'drive-create-folder', 'drive-trash', 'notion-create-page', 'notion-insert-content'];
16
17
  export type BuilderName = (typeof BUILDER_NAMES)[number];
17
18
  export declare const BUILDERS: Record<BuilderName, OperationBuilder>;
@@ -2,6 +2,7 @@ import { randomBytes } from 'crypto';
2
2
  import b4a from 'b4a';
3
3
  import { z } from 'zod';
4
4
  import { buildGmailMessage, GmailEnvelopeSchema } from './gmail-message.js';
5
+ export const BUILDER_POLICY_VERSION = 1;
5
6
  export const BUILDER_NAMES = [
6
7
  'gmail-send',
7
8
  'gmail-draft',