@purista/harness 1.0.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 (97) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/agents/index.d.ts +34 -0
  4. package/dist/agents/index.js +301 -0
  5. package/dist/errors/catalog.d.ts +185 -0
  6. package/dist/errors/catalog.js +144 -0
  7. package/dist/errors/harness-error.d.ts +64 -0
  8. package/dist/errors/harness-error.js +58 -0
  9. package/dist/errors/index.d.ts +3 -0
  10. package/dist/errors/index.js +3 -0
  11. package/dist/errors/redaction.d.ts +5 -0
  12. package/dist/errors/redaction.js +64 -0
  13. package/dist/harness/defineHarness.d.ts +640 -0
  14. package/dist/harness/defineHarness.js +176 -0
  15. package/dist/harness/errors.d.ts +62 -0
  16. package/dist/harness/errors.js +67 -0
  17. package/dist/harness/types.d.ts +27 -0
  18. package/dist/harness/types.js +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +12 -0
  21. package/dist/logger/index.d.ts +2 -0
  22. package/dist/logger/index.js +2 -0
  23. package/dist/logger/json-logger.d.ts +31 -0
  24. package/dist/logger/json-logger.js +65 -0
  25. package/dist/logger/logger.d.ts +31 -0
  26. package/dist/logger/logger.js +1 -0
  27. package/dist/models/json.d.ts +6 -0
  28. package/dist/models/json.js +1 -0
  29. package/dist/models/registry.d.ts +112 -0
  30. package/dist/models/registry.js +286 -0
  31. package/dist/models/state.d.ts +64 -0
  32. package/dist/models/state.js +1 -0
  33. package/dist/ports/base-model-provider.d.ts +56 -0
  34. package/dist/ports/base-model-provider.js +343 -0
  35. package/dist/ports/capabilities.d.ts +70 -0
  36. package/dist/ports/capabilities.js +38 -0
  37. package/dist/ports/feedback.d.ts +29 -0
  38. package/dist/ports/feedback.js +1 -0
  39. package/dist/ports/harness-context.d.ts +20 -0
  40. package/dist/ports/harness-context.js +1 -0
  41. package/dist/ports/index.d.ts +6 -0
  42. package/dist/ports/index.js +6 -0
  43. package/dist/ports/model-provider.d.ts +280 -0
  44. package/dist/ports/model-provider.js +1 -0
  45. package/dist/ports/state.d.ts +72 -0
  46. package/dist/ports/state.js +24 -0
  47. package/dist/runtime/durable.d.ts +134 -0
  48. package/dist/runtime/durable.js +185 -0
  49. package/dist/runtime/index.d.ts +2 -0
  50. package/dist/runtime/index.js +2 -0
  51. package/dist/runtime/steps.d.ts +22 -0
  52. package/dist/runtime/steps.js +51 -0
  53. package/dist/sandbox/index.d.ts +111 -0
  54. package/dist/sandbox/index.js +165 -0
  55. package/dist/sessions/index.d.ts +23 -0
  56. package/dist/sessions/index.js +718 -0
  57. package/dist/skills/index.d.ts +8 -0
  58. package/dist/skills/index.js +88 -0
  59. package/dist/state/in-memory.d.ts +35 -0
  60. package/dist/state/in-memory.js +140 -0
  61. package/dist/telemetry/index.d.ts +1 -0
  62. package/dist/telemetry/index.js +1 -0
  63. package/dist/telemetry/shim.d.ts +26 -0
  64. package/dist/telemetry/shim.js +120 -0
  65. package/dist/testing/capabilities.d.ts +11 -0
  66. package/dist/testing/capabilities.js +20 -0
  67. package/dist/testing/fakeModelProvider.d.ts +25 -0
  68. package/dist/testing/fakeModelProvider.js +79 -0
  69. package/dist/testing/feedback.d.ts +10 -0
  70. package/dist/testing/feedback.js +24 -0
  71. package/dist/testing/fixtures/mcp/fake-http-server.d.ts +8 -0
  72. package/dist/testing/fixtures/mcp/fake-http-server.js +95 -0
  73. package/dist/testing/index.d.ts +8 -0
  74. package/dist/testing/index.js +11 -0
  75. package/dist/testing/sandboxContract.d.ts +4 -0
  76. package/dist/testing/sandboxContract.js +74 -0
  77. package/dist/testing/sandboxSnapshot.d.ts +7 -0
  78. package/dist/testing/sandboxSnapshot.js +201 -0
  79. package/dist/testing/stateStoreContract.d.ts +2 -0
  80. package/dist/testing/stateStoreContract.js +109 -0
  81. package/dist/tools/index.d.ts +9 -0
  82. package/dist/tools/index.js +123 -0
  83. package/dist/tools/mcp/http.d.ts +2 -0
  84. package/dist/tools/mcp/http.js +109 -0
  85. package/dist/tools/mcp/index.d.ts +2 -0
  86. package/dist/tools/mcp/index.js +2 -0
  87. package/dist/tools/mcp/runner.d.ts +74 -0
  88. package/dist/tools/mcp/runner.js +238 -0
  89. package/dist/tools/mcp/schema.d.ts +41 -0
  90. package/dist/tools/mcp/schema.js +251 -0
  91. package/dist/tools/mcp/stdio.d.ts +2 -0
  92. package/dist/tools/mcp/stdio.js +122 -0
  93. package/dist/ulid/index.d.ts +6 -0
  94. package/dist/ulid/index.js +35 -0
  95. package/dist/workflows/index.d.ts +8 -0
  96. package/dist/workflows/index.js +26 -0
  97. package/package.json +75 -0
@@ -0,0 +1,251 @@
1
+ import { ValidationError } from '../../errors/index.js';
2
+ const supportedKeywords = new Set([
3
+ '$schema',
4
+ 'additionalProperties',
5
+ 'allOf',
6
+ 'anyOf',
7
+ 'const',
8
+ 'enum',
9
+ 'format',
10
+ 'items',
11
+ 'maximum',
12
+ 'maxItems',
13
+ 'maxLength',
14
+ 'minimum',
15
+ 'minItems',
16
+ 'minLength',
17
+ 'not',
18
+ 'oneOf',
19
+ 'pattern',
20
+ 'properties',
21
+ 'required',
22
+ 'type'
23
+ ]);
24
+ const supportedFormats = new Set(['uri', 'email', 'date-time', 'uuid']);
25
+ const warned = new Set();
26
+ export function validateMcpJsonSchema(opts) {
27
+ const schemaIssues = validateSchemaShape(opts.schema, '');
28
+ if (schemaIssues.length > 0) {
29
+ throw new ValidationError('MCP JSON Schema is malformed.', { where: opts.where, issues: schemaIssues });
30
+ }
31
+ warnUnsupportedKeywords(opts.toolId, opts.schema, '', opts.warn);
32
+ const issues = validateValue(opts.schema, opts.value, '');
33
+ if (issues.length > 0) {
34
+ throw new ValidationError('MCP JSON Schema validation failed.', { where: opts.where, issues });
35
+ }
36
+ if (!isJsonValue(opts.value)) {
37
+ throw new ValidationError('MCP value is not JSON serializable.', { where: opts.where, issues: [{ path: '', message: 'Value must be JSON serializable.' }] });
38
+ }
39
+ return opts.value;
40
+ }
41
+ export function assertMcpJsonSchema(toolId, schema, where, warn) {
42
+ const issues = validateSchemaShape(schema, '');
43
+ if (issues.length > 0)
44
+ throw new ValidationError('MCP JSON Schema is malformed.', { where, issues });
45
+ warnUnsupportedKeywords(toolId, schema, '', warn);
46
+ }
47
+ function validateSchemaShape(schema, pointer) {
48
+ if (!isRecord(schema))
49
+ return [{ path: pointer, message: 'Schema must be an object.' }];
50
+ const issues = [];
51
+ if ('type' in schema && !isValidType(schema.type))
52
+ issues.push({ path: pointerJoin(pointer, 'type'), message: 'Schema type must be a string or string array.', keyword: 'type' });
53
+ if ('properties' in schema && !isRecord(schema.properties))
54
+ issues.push({ path: pointerJoin(pointer, 'properties'), message: 'properties must be an object.', keyword: 'properties' });
55
+ if ('required' in schema && (!Array.isArray(schema.required) || !schema.required.every((value) => typeof value === 'string')))
56
+ issues.push({ path: pointerJoin(pointer, 'required'), message: 'required must be an array of strings.', keyword: 'required' });
57
+ if ('enum' in schema && !Array.isArray(schema.enum))
58
+ issues.push({ path: pointerJoin(pointer, 'enum'), message: 'enum must be an array.', keyword: 'enum' });
59
+ for (const key of ['oneOf', 'anyOf', 'allOf']) {
60
+ if (key in schema && (!Array.isArray(schema[key]) || !schema[key].every(isRecord)))
61
+ issues.push({ path: pointerJoin(pointer, key), message: `${key} must be an array of schemas.`, keyword: key });
62
+ }
63
+ for (const key of ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems']) {
64
+ if (key in schema && typeof schema[key] !== 'number')
65
+ issues.push({ path: pointerJoin(pointer, key), message: `${key} must be a number.`, keyword: key });
66
+ }
67
+ if ('pattern' in schema && typeof schema.pattern !== 'string')
68
+ issues.push({ path: pointerJoin(pointer, 'pattern'), message: 'pattern must be a string.', keyword: 'pattern' });
69
+ if ('format' in schema && typeof schema.format !== 'string')
70
+ issues.push({ path: pointerJoin(pointer, 'format'), message: 'format must be a string.', keyword: 'format' });
71
+ if ('additionalProperties' in schema && typeof schema.additionalProperties !== 'boolean' && !isRecord(schema.additionalProperties)) {
72
+ issues.push({ path: pointerJoin(pointer, 'additionalProperties'), message: 'additionalProperties must be a boolean or schema.', keyword: 'additionalProperties' });
73
+ }
74
+ if (isRecord(schema.properties)) {
75
+ for (const [name, child] of Object.entries(schema.properties))
76
+ issues.push(...validateSchemaShape(child, pointerJoin(pointerJoin(pointer, 'properties'), name)));
77
+ }
78
+ if (isRecord(schema.items))
79
+ issues.push(...validateSchemaShape(schema.items, pointerJoin(pointer, 'items')));
80
+ if (isRecord(schema.not))
81
+ issues.push(...validateSchemaShape(schema.not, pointerJoin(pointer, 'not')));
82
+ if (isRecord(schema.additionalProperties))
83
+ issues.push(...validateSchemaShape(schema.additionalProperties, pointerJoin(pointer, 'additionalProperties')));
84
+ for (const key of ['oneOf', 'anyOf', 'allOf']) {
85
+ if (Array.isArray(schema[key])) {
86
+ ;
87
+ schema[key].forEach((child, index) => {
88
+ issues.push(...validateSchemaShape(child, pointerJoin(pointerJoin(pointer, key), String(index))));
89
+ });
90
+ }
91
+ }
92
+ return issues;
93
+ }
94
+ function validateValue(schema, value, path) {
95
+ const issues = [];
96
+ if ('type' in schema && !matchesType(schema.type, value)) {
97
+ issues.push({ path, message: `Value must match type ${JSON.stringify(schema.type)}.`, keyword: 'type' });
98
+ return issues;
99
+ }
100
+ if ('const' in schema && !deepEqual(value, schema.const))
101
+ issues.push({ path, message: 'Value must equal const.', keyword: 'const' });
102
+ if (Array.isArray(schema.enum) && !schema.enum.some((item) => deepEqual(item, value)))
103
+ issues.push({ path, message: 'Value must match one enum value.', keyword: 'enum' });
104
+ if (Array.isArray(schema.allOf)) {
105
+ for (const child of schema.allOf)
106
+ issues.push(...validateValue(child, value, path));
107
+ }
108
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((child) => validateValue(child, value, path).length === 0))
109
+ issues.push({ path, message: 'Value must match at least one anyOf schema.', keyword: 'anyOf' });
110
+ if (Array.isArray(schema.oneOf) && schema.oneOf.filter((child) => validateValue(child, value, path).length === 0).length !== 1)
111
+ issues.push({ path, message: 'Value must match exactly one oneOf schema.', keyword: 'oneOf' });
112
+ if (isRecord(schema.not) && validateValue(schema.not, value, path).length === 0)
113
+ issues.push({ path, message: 'Value must not match schema.', keyword: 'not' });
114
+ if (typeof value === 'number') {
115
+ if (typeof schema.minimum === 'number' && value < schema.minimum)
116
+ issues.push({ path, message: `Value must be >= minimum ${schema.minimum}.`, keyword: 'minimum' });
117
+ if (typeof schema.maximum === 'number' && value > schema.maximum)
118
+ issues.push({ path, message: `Value must be <= maximum ${schema.maximum}.`, keyword: 'maximum' });
119
+ }
120
+ if (typeof value === 'string') {
121
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength)
122
+ issues.push({ path, message: `Value length must be >= minLength ${schema.minLength}.`, keyword: 'minLength' });
123
+ if (typeof schema.maxLength === 'number' && value.length > schema.maxLength)
124
+ issues.push({ path, message: `Value length must be <= maxLength ${schema.maxLength}.`, keyword: 'maxLength' });
125
+ if (typeof schema.pattern === 'string' && !(new RegExp(schema.pattern).test(value)))
126
+ issues.push({ path, message: 'Value must match pattern.', keyword: 'pattern' });
127
+ if (typeof schema.format === 'string' && supportedFormats.has(schema.format) && !matchesFormat(schema.format, value))
128
+ issues.push({ path, message: `Value must match ${schema.format} format.`, keyword: 'format' });
129
+ }
130
+ if (Array.isArray(value)) {
131
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems)
132
+ issues.push({ path, message: `Array length must be >= minItems ${schema.minItems}.`, keyword: 'minItems' });
133
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems)
134
+ issues.push({ path, message: `Array length must be <= maxItems ${schema.maxItems}.`, keyword: 'maxItems' });
135
+ if (isRecord(schema.items))
136
+ value.forEach((item, index) => issues.push(...validateValue(schema.items, item, pointerJoin(path, String(index)))));
137
+ }
138
+ if (isPlainObject(value))
139
+ validateObject(schema, value, path, issues);
140
+ return issues;
141
+ }
142
+ function validateObject(schema, value, path, issues) {
143
+ const properties = isRecord(schema.properties) ? schema.properties : {};
144
+ if (Array.isArray(schema.required)) {
145
+ for (const required of schema.required) {
146
+ if (!(required in value))
147
+ issues.push({ path: pointerJoin(path, required), message: 'Required property is missing.', keyword: 'required' });
148
+ }
149
+ }
150
+ for (const [name, child] of Object.entries(properties)) {
151
+ if (name in value)
152
+ issues.push(...validateValue(child, value[name], pointerJoin(path, name)));
153
+ }
154
+ for (const key of Object.keys(value)) {
155
+ if (key in properties)
156
+ continue;
157
+ if (schema.additionalProperties === false)
158
+ issues.push({ path: pointerJoin(path, key), message: 'Unknown property is not allowed.', keyword: 'additionalProperties' });
159
+ if (isRecord(schema.additionalProperties))
160
+ issues.push(...validateValue(schema.additionalProperties, value[key], pointerJoin(path, key)));
161
+ }
162
+ }
163
+ function warnUnsupportedKeywords(toolId, schema, pointer, warn) {
164
+ if (!isRecord(schema))
165
+ return;
166
+ for (const [key, value] of Object.entries(schema)) {
167
+ if (!supportedKeywords.has(key))
168
+ emitWarning(toolId, key, pointerJoin(pointer, key), warn);
169
+ if (key === 'format' && typeof value === 'string' && !supportedFormats.has(value))
170
+ emitWarning(toolId, `format:${value}`, pointerJoin(pointer, key), warn);
171
+ }
172
+ if (isRecord(schema.properties))
173
+ for (const [name, child] of Object.entries(schema.properties))
174
+ warnUnsupportedKeywords(toolId, child, pointerJoin(pointerJoin(pointer, 'properties'), name), warn);
175
+ if (isRecord(schema.items))
176
+ warnUnsupportedKeywords(toolId, schema.items, pointerJoin(pointer, 'items'), warn);
177
+ if (isRecord(schema.not))
178
+ warnUnsupportedKeywords(toolId, schema.not, pointerJoin(pointer, 'not'), warn);
179
+ if (isRecord(schema.additionalProperties))
180
+ warnUnsupportedKeywords(toolId, schema.additionalProperties, pointerJoin(pointer, 'additionalProperties'), warn);
181
+ for (const key of ['oneOf', 'anyOf', 'allOf']) {
182
+ if (Array.isArray(schema[key]))
183
+ schema[key].forEach((child, index) => warnUnsupportedKeywords(toolId, child, pointerJoin(pointerJoin(pointer, key), String(index)), warn));
184
+ }
185
+ }
186
+ function emitWarning(toolId, keyword, path, warn) {
187
+ const key = `${toolId}\0${keyword}\0${path}`;
188
+ if (warned.has(key))
189
+ return;
190
+ warned.add(key);
191
+ warn?.({ toolId, keyword, path, message: `Unsupported MCP JSON Schema keyword ${keyword} at ${path}.` });
192
+ }
193
+ function isValidType(value) {
194
+ const valid = new Set(['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']);
195
+ return typeof value === 'string' ? valid.has(value) : Array.isArray(value) && value.every((item) => typeof item === 'string' && valid.has(item));
196
+ }
197
+ function matchesType(type, value) {
198
+ if (Array.isArray(type))
199
+ return type.some((item) => matchesType(item, value));
200
+ switch (type) {
201
+ case 'object': return isPlainObject(value);
202
+ case 'array': return Array.isArray(value);
203
+ case 'string': return typeof value === 'string';
204
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
205
+ case 'integer': return Number.isInteger(value);
206
+ case 'boolean': return typeof value === 'boolean';
207
+ case 'null': return value === null;
208
+ default: return true;
209
+ }
210
+ }
211
+ function matchesFormat(format, value) {
212
+ if (format === 'email')
213
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
214
+ if (format === 'uri') {
215
+ try {
216
+ new URL(value);
217
+ return true;
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
223
+ if (format === 'date-time')
224
+ return !Number.isNaN(Date.parse(value));
225
+ if (format === 'uuid')
226
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
227
+ return true;
228
+ }
229
+ function pointerJoin(base, key) {
230
+ return `${base}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`;
231
+ }
232
+ function isRecord(value) {
233
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
234
+ }
235
+ function isPlainObject(value) {
236
+ return Object.prototype.toString.call(value) === '[object Object]';
237
+ }
238
+ function isJsonValue(value) {
239
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
240
+ return true;
241
+ if (typeof value === 'number')
242
+ return Number.isFinite(value);
243
+ if (Array.isArray(value))
244
+ return value.every(isJsonValue);
245
+ if (isPlainObject(value))
246
+ return Object.values(value).every(isJsonValue);
247
+ return false;
248
+ }
249
+ function deepEqual(left, right) {
250
+ return JSON.stringify(left) === JSON.stringify(right);
251
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpTransportRunner, ResolvedMcpStdioTool } from './runner.js';
2
+ export declare function createStdioMcpTransportRunner(config: ResolvedMcpStdioTool): McpTransportRunner;
@@ -0,0 +1,122 @@
1
+ import { McpProtocolError, OperationTimeoutError, SandboxNoExecutorError } from '../../errors/index.js';
2
+ import { withMcpTimeout } from './runner.js';
3
+ const protocolVersion = '2025-06-18';
4
+ export function createStdioMcpTransportRunner(config) {
5
+ let installPromise;
6
+ async function ensureInstalled(signal) {
7
+ if (!config.install)
8
+ return;
9
+ installPromise ??= runInstall(config, signal);
10
+ return installPromise;
11
+ }
12
+ return {
13
+ async listTools(options) {
14
+ return withMcpTimeout({ ...(options?.signal ? { signal: options.signal } : {}), timeoutMs: options?.timeoutMs ?? config.timeoutMs, scope: 'tool' }, async (signal) => {
15
+ await ensureInstalled(signal);
16
+ const result = await exchange(config, [{ id: 1, method: 'tools/list', params: {} }], signal, options?.timeoutMs);
17
+ return readResult(result, 1, 'list', (value) => {
18
+ if (!isRecord(value) || !Array.isArray(value['tools']))
19
+ return [];
20
+ return value['tools'];
21
+ });
22
+ });
23
+ },
24
+ async callTool(name, input, options) {
25
+ return withMcpTimeout({ ...(options?.signal ? { signal: options.signal } : {}), timeoutMs: options?.timeoutMs ?? config.timeoutMs, scope: 'tool' }, async (signal) => {
26
+ await ensureInstalled(signal);
27
+ const result = await exchange(config, [{ id: 1, method: 'tools/call', params: { name, arguments: input } }], signal, options?.timeoutMs);
28
+ return readResult(result, 1, 'call', (value) => value);
29
+ });
30
+ },
31
+ async close() {
32
+ installPromise = undefined;
33
+ }
34
+ };
35
+ }
36
+ async function runInstall(config, signal) {
37
+ if (config.sandbox.executor !== 'available') {
38
+ throw new SandboxNoExecutorError('MCP stdio install requires a sandbox executor.', { session_id: 'unknown' });
39
+ }
40
+ const install = config.install;
41
+ if (!install)
42
+ return;
43
+ const result = await config.sandbox.exec(install.command, {
44
+ ...(install.cwd ? { cwd: install.cwd } : {}),
45
+ ...(install.env ? { env: install.env } : {}),
46
+ timeoutMs: install.timeoutMs ?? config.timeoutMs,
47
+ ...(signal ? { signal } : {})
48
+ });
49
+ if (result.exitCode !== 0) {
50
+ throw mapStdioError(config, 'connect', new Error(`MCP install failed with exit code ${result.exitCode}: ${result.stderr || result.stdout}`));
51
+ }
52
+ }
53
+ async function exchange(config, calls, signal, timeoutMs) {
54
+ if (config.sandbox.executor !== 'available') {
55
+ throw new SandboxNoExecutorError('MCP stdio requires a sandbox executor.', { session_id: 'unknown' });
56
+ }
57
+ const stdin = [
58
+ JSON.stringify({
59
+ jsonrpc: '2.0',
60
+ id: 0,
61
+ method: 'initialize',
62
+ params: {
63
+ protocolVersion,
64
+ capabilities: {},
65
+ clientInfo: { name: '@purista/harness', version: '0.0.0' }
66
+ }
67
+ }),
68
+ JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }),
69
+ ...calls.map((call) => JSON.stringify({ jsonrpc: '2.0', id: call.id, method: call.method, params: call.params }))
70
+ ].join('\n') + '\n';
71
+ try {
72
+ const result = await config.sandbox.exec(commandLine(config.command, config.args), {
73
+ stdin,
74
+ ...(config.env ? { env: config.env } : {}),
75
+ timeoutMs: timeoutMs ?? config.timeoutMs,
76
+ ...(signal ? { signal } : {})
77
+ });
78
+ if (result.exitCode !== 0) {
79
+ throw new Error(`MCP server exited with code ${result.exitCode}: ${result.stderr || result.stdout}`);
80
+ }
81
+ return parseResponses(result.stdout);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof OperationTimeoutError)
85
+ throw mapStdioError(config, calls[0]?.method === 'tools/list' ? 'list' : 'call', error);
86
+ throw mapStdioError(config, calls[0]?.method === 'tools/list' ? 'list' : 'call', error);
87
+ }
88
+ }
89
+ function readResult(responses, id, phase, map) {
90
+ const response = responses.find((candidate) => candidate.id === id);
91
+ if (!response)
92
+ throw new Error(`MCP ${phase} response missing.`);
93
+ if (response.error)
94
+ throw new Error(response.error.message ?? `MCP ${phase} failed.`);
95
+ return map(response.result);
96
+ }
97
+ function parseResponses(stdout) {
98
+ const responses = [];
99
+ for (const line of stdout.split(/\r?\n/)) {
100
+ const trimmed = line.trim();
101
+ if (!trimmed.startsWith('{'))
102
+ continue;
103
+ const parsed = JSON.parse(trimmed);
104
+ if (isRecord(parsed) && ('id' in parsed || 'result' in parsed || 'error' in parsed))
105
+ responses.push(parsed);
106
+ }
107
+ return responses;
108
+ }
109
+ function commandLine(command, args) {
110
+ return [command, ...(args ?? [])].map(shellQuote).join(' ');
111
+ }
112
+ function shellQuote(value) {
113
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(value))
114
+ return value;
115
+ return `'${value.replaceAll("'", "'\\''")}'`;
116
+ }
117
+ function mapStdioError(config, phase, error) {
118
+ return new McpProtocolError('MCP stdio protocol failure.', { tool_id: config.localToolId, transport: 'stdio', phase }, error);
119
+ }
120
+ function isRecord(value) {
121
+ return typeof value === 'object' && value !== null;
122
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generates a monotonic ULID-like identifier.
3
+ *
4
+ * Subsequent calls within the same millisecond increment the random suffix to preserve ordering.
5
+ */
6
+ export declare function ulid(): string;
@@ -0,0 +1,35 @@
1
+ const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
2
+ let lastTime = -1;
3
+ let lastRandom = 0n;
4
+ function encode(value, length) {
5
+ let out = '';
6
+ let input = value;
7
+ const base = 32n;
8
+ for (let i = 0; i < length; i += 1) {
9
+ const index = Number(input % base);
10
+ out = ENCODING[index] + out;
11
+ input /= base;
12
+ }
13
+ return out;
14
+ }
15
+ function nextRandom() {
16
+ const now = Date.now();
17
+ if (now !== lastTime) {
18
+ lastTime = now;
19
+ const seed = BigInt(Math.floor(Math.random() * 2 ** 24));
20
+ lastRandom = seed << 56n;
21
+ return lastRandom;
22
+ }
23
+ lastRandom += 1n;
24
+ return lastRandom;
25
+ }
26
+ /**
27
+ * Generates a monotonic ULID-like identifier.
28
+ *
29
+ * Subsequent calls within the same millisecond increment the random suffix to preserve ordering.
30
+ */
31
+ export function ulid() {
32
+ const timePart = encode(BigInt(Date.now()), 10);
33
+ const randomPart = encode(nextRandom(), 16);
34
+ return `${timePart}${randomPart}`;
35
+ }
@@ -0,0 +1,8 @@
1
+ import type { BuilderState, InvokeOptions, WorkflowContext, WorkflowDefinition } from '../harness/defineHarness.js';
2
+ export declare function runWorkflow<S extends BuilderState>(args: {
3
+ workflowId: string;
4
+ workflow: WorkflowDefinition<S, any, any>;
5
+ input: unknown;
6
+ ctx: Omit<WorkflowContext<S, unknown, unknown>, 'input'>;
7
+ opts?: InvokeOptions;
8
+ }): Promise<unknown>;
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import { OperationCancelledError, ValidationError } from '../errors/index.js';
3
+ export async function runWorkflow(args) {
4
+ if (args.ctx['signal'].aborted)
5
+ throw new OperationCancelledError('Workflow execution was cancelled.', { scope: 'workflow' });
6
+ const schema = args.workflow.input;
7
+ let parsed;
8
+ try {
9
+ parsed = schema ? schema.parse(args.input) : args.input;
10
+ }
11
+ catch (error) {
12
+ throw new ValidationError('Workflow input validation failed.', { where: 'workflow_input', issues: validationIssues(error) }, error);
13
+ }
14
+ const output = await args.workflow.handler({ ...args.ctx, input: parsed });
15
+ if (!args.workflow.output)
16
+ return output;
17
+ try {
18
+ return args.workflow.output.parse(output);
19
+ }
20
+ catch (error) {
21
+ throw new ValidationError('Workflow output validation failed.', { where: 'workflow_output', issues: validationIssues(error) }, error);
22
+ }
23
+ }
24
+ function validationIssues(error) {
25
+ return error instanceof z.ZodError ? JSON.parse(JSON.stringify(error.issues)) : error;
26
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@purista/harness",
3
+ "version": "1.0.0",
4
+ "description": "Self-hosted enterprise agent harness for typed tools, agents, workflows, state, sandboxing, and telemetry.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./testing": {
14
+ "types": "./dist/testing/index.d.ts",
15
+ "import": "./dist/testing/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist/**/*.js",
20
+ "dist/**/*.d.ts",
21
+ "package.json",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "sideEffects": false,
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/puristajs/harness",
30
+ "directory": "packages/harness"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/puristajs/harness/issues"
34
+ },
35
+ "homepage": "https://github.com/puristajs/harness#readme",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "clean": "rm -rf dist",
41
+ "build": "npm run clean && tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test:types": "tsc -p tsconfig.type-tests.json",
44
+ "test": "vitest run",
45
+ "test:unit": "vitest run src",
46
+ "test:contracts": "vitest run src/ports test/sandbox.test.ts",
47
+ "test:integration": "vitest run test/harness.test.ts test/tools.test.ts test/skills.test.ts test/telemetry-flow.test.ts",
48
+ "test:failure": "vitest run test/failure"
49
+ },
50
+ "dependencies": {
51
+ "@opentelemetry/semantic-conventions": "^1.40.0",
52
+ "zod": "^4.4.3"
53
+ },
54
+ "peerDependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.29.0",
56
+ "@opentelemetry/api": "^1.9.1",
57
+ "just-bash": "^2.14.4"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@modelcontextprotocol/sdk": {
61
+ "optional": true
62
+ },
63
+ "just-bash": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "devDependencies": {
68
+ "@modelcontextprotocol/sdk": "^1.29.0",
69
+ "@opentelemetry/context-async-hooks": "^2.7.1",
70
+ "@types/node": "^25.6.0",
71
+ "just-bash": "^2.14.4",
72
+ "typescript": "^6.0.3",
73
+ "vitest": "^4.1.5"
74
+ }
75
+ }