@aiwg/cli 2026.8.19 → 2026.8.20

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 (34) hide show
  1. package/THIRD_PARTY_NOTICES.md +12 -0
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -0
  4. package/dist/src/artifacts/backend-runtime.js +26 -0
  5. package/dist/src/artifacts/backends/sqlite-backend.js +204 -28
  6. package/dist/src/artifacts/dep-graph.js +27 -5
  7. package/dist/src/artifacts/graph-backend.js +2 -2
  8. package/dist/src/artifacts/graph-query.js +21 -9
  9. package/dist/src/artifacts/index-builder.js +15 -0
  10. package/dist/src/artifacts/index-status.js +4 -1
  11. package/dist/src/artifacts/stats.js +4 -1
  12. package/dist/src/artifacts/types.js +13 -1
  13. package/dist/src/cli/handlers/help.js +2 -0
  14. package/dist/src/cli/handlers/index.js +6 -2
  15. package/dist/src/cli/handlers/mission.js +27 -0
  16. package/dist/src/cli/handlers/runtime-info.js +29 -0
  17. package/dist/src/cli/handlers/steward.js +12 -0
  18. package/dist/src/cli/handlers/uhp.js +88 -0
  19. package/dist/src/config/aiwg-config.js +10 -0
  20. package/dist/src/extensions/commands/definitions.js +38 -0
  21. package/dist/src/mission-protocol/codecs.js +265 -0
  22. package/dist/src/mission-protocol/index.js +3 -0
  23. package/dist/src/mission-protocol/types.js +2 -0
  24. package/dist/src/storage/backend-contract.js +64 -0
  25. package/dist/src/storage/index.js +2 -0
  26. package/dist/src/storage/migration-protocol.js +378 -0
  27. package/dist/src/uhp/client.js +374 -0
  28. package/dist/src/uhp/config.js +130 -0
  29. package/dist/src/uhp/errors.js +63 -0
  30. package/dist/src/uhp/index.js +7 -0
  31. package/dist/src/uhp/mission.js +111 -0
  32. package/dist/src/uhp/sse.js +76 -0
  33. package/dist/src/uhp/types.js +2 -0
  34. package/package.json +1 -1
@@ -0,0 +1,374 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lookup } from 'node:dns/promises';
3
+ import { link, lstat, mkdir, open, readFile, stat, unlink } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { DEFAULT_UHP_LIMITS, isUhpLoopback, isUhpPrivateAddress, resolveUhpLimits, validateUhpEndpoint } from './config.js';
6
+ import { parseUhpError, redactUhpText, UhpError } from './errors.js';
7
+ import { parseUhpEventStream } from './sse.js';
8
+ import { UHP_VERSION, } from './types.js';
9
+ const ID_PATTERNS = {
10
+ harness: /^chrn_[A-Za-z0-9_-]+$/,
11
+ response: /^resp_[A-Za-z0-9_-]+$/,
12
+ session: /^hsess[A-Za-z0-9_-]+$/,
13
+ container: /^cntr_[A-Za-z0-9_-]+$/,
14
+ file: /^file_[A-Za-z0-9_-]+$/,
15
+ };
16
+ const TERMINAL = new Set(['completed', 'failed', 'incomplete', 'cancelled']);
17
+ function stableValue(value) {
18
+ if (Array.isArray(value))
19
+ return value.map(stableValue);
20
+ if (!value || typeof value !== 'object')
21
+ return value;
22
+ return Object.fromEntries(Object.entries(value)
23
+ .sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, stableValue(child)]));
24
+ }
25
+ export function canonicalUhpRequestDigest(request) {
26
+ return createHash('sha256').update(JSON.stringify(stableValue(request))).digest('hex');
27
+ }
28
+ export function uhpIdempotencyKey(request) {
29
+ return `aiwg-${canonicalUhpRequestDigest(request)}`;
30
+ }
31
+ export function resolveEnvironmentCredential(profile, env = process.env) {
32
+ const value = env[profile.credential.name];
33
+ if (!value || /[\r\n]/.test(value))
34
+ throw new UhpError('missing_credential', `Credential reference '${profile.credential.name}' is unavailable or invalid`, { remoteState: 'not-started' });
35
+ return Promise.resolve(value);
36
+ }
37
+ function assertId(kind, value) {
38
+ if (!ID_PATTERNS[kind].test(value))
39
+ throw new UhpError('invalid_identifier', `Malformed UHP ${kind} identifier`, { remoteState: 'not-started' });
40
+ return value;
41
+ }
42
+ async function readBounded(response, maxBytes = 2 * 1024 * 1024) {
43
+ const declared = Number(response.headers.get('content-length'));
44
+ if (Number.isFinite(declared) && declared > maxBytes)
45
+ throw new UhpError('response_too_large', 'UHP response exceeded the configured size limit', { remoteState: 'unknown' });
46
+ if (!response.body)
47
+ return new Uint8Array();
48
+ const reader = response.body.getReader();
49
+ const chunks = [];
50
+ let length = 0;
51
+ try {
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done)
55
+ break;
56
+ length += value.byteLength;
57
+ if (length > maxBytes)
58
+ throw new UhpError('response_too_large', 'UHP response exceeded the configured size limit', { remoteState: 'unknown' });
59
+ chunks.push(value);
60
+ }
61
+ }
62
+ finally {
63
+ reader.releaseLock();
64
+ }
65
+ const output = new Uint8Array(length);
66
+ let offset = 0;
67
+ for (const chunk of chunks) {
68
+ output.set(chunk, offset);
69
+ offset += chunk.byteLength;
70
+ }
71
+ return output;
72
+ }
73
+ function parseJson(bytes) {
74
+ try {
75
+ return JSON.parse(new TextDecoder().decode(bytes));
76
+ }
77
+ catch {
78
+ throw new UhpError('invalid_json', 'UHP server returned malformed JSON', { remoteState: 'unknown' });
79
+ }
80
+ }
81
+ function ensureDiscovery(value) {
82
+ if (!value || typeof value !== 'object')
83
+ throw new UhpError('invalid_discovery', 'UHP discovery document must be an object');
84
+ const discovery = value;
85
+ if (discovery.object !== 'uhp.discovery' || discovery.protocol !== 'uhp')
86
+ throw new UhpError('invalid_discovery', 'Endpoint did not return a UHP discovery document');
87
+ if (!Array.isArray(discovery.versions) || !discovery.versions.length || !discovery.versions.every(version => typeof version === 'string'))
88
+ throw new UhpError('invalid_discovery', 'UHP discovery versions must be a non-empty string array');
89
+ if (!discovery.versions.includes(discovery.default_version))
90
+ throw new UhpError('invalid_discovery', 'UHP default version is not listed as supported');
91
+ if (!discovery.versions.includes(UHP_VERSION))
92
+ throw new UhpError('unsupported_protocol_version', `UHP endpoint does not advertise required version ${UHP_VERSION}`);
93
+ if (!['core', 'extended', 'full'].includes(discovery.conformance_class))
94
+ throw new UhpError('invalid_discovery', 'UHP discovery has an unknown conformance class');
95
+ const required = discovery.conformance_class === 'core' ? ['streaming', 'sessions', 'cancellation', 'idempotency']
96
+ : discovery.conformance_class === 'extended' ? ['streaming', 'sessions', 'cancellation', 'idempotency', 'files_input', 'files_output', 'session_listing']
97
+ : ['streaming', 'sessions', 'cancellation', 'idempotency', 'files_input', 'files_output', 'session_listing', 'harness_management', 'session_sharing'];
98
+ if (required.some(capability => discovery.capabilities?.[capability] !== true))
99
+ throw new UhpError('invalid_discovery', 'UHP conformance class contradicts advertised capabilities');
100
+ return discovery;
101
+ }
102
+ function ensureResponse(value) {
103
+ if (!value || typeof value !== 'object')
104
+ throw new UhpError('invalid_response', 'UHP response must be an object', { remoteState: 'unknown' });
105
+ const response = value;
106
+ if (response.object !== 'response' || !ID_PATTERNS.response.test(response.id)
107
+ || !Number.isSafeInteger(response.created_at) || typeof response.status !== 'string'
108
+ || typeof response.model !== 'string' || !Array.isArray(response.output)
109
+ || (response.metadata !== undefined && (!response.metadata || typeof response.metadata !== 'object'))) {
110
+ throw new UhpError('invalid_response', 'UHP response is missing required identity or lifecycle fields', { remoteState: 'unknown' });
111
+ }
112
+ return { ...response, metadata: response.metadata ?? {} };
113
+ }
114
+ function safeFilename(value) {
115
+ let decoded = value;
116
+ for (let i = 0; i < 3; i += 1) {
117
+ try {
118
+ const next = decodeURIComponent(decoded);
119
+ if (next === decoded)
120
+ break;
121
+ decoded = next;
122
+ }
123
+ catch {
124
+ break;
125
+ }
126
+ }
127
+ const base = path.basename(decoded.replace(/\\/g, '/')).replace(/[\u0000-\u001f\u007f]/g, '_').trim();
128
+ if (!base || base === '.' || base === '..')
129
+ return `artifact-${randomUUID()}`;
130
+ return base.slice(0, 240);
131
+ }
132
+ export class UhpClient {
133
+ profileName;
134
+ profile;
135
+ fetchImpl;
136
+ credentialResolver;
137
+ base;
138
+ limits;
139
+ idempotency = new Map();
140
+ enforceDnsPolicy;
141
+ constructor(profileName, profile, fetchImpl = globalThis.fetch, credentialResolver = resolveEnvironmentCredential) {
142
+ this.profileName = profileName;
143
+ this.profile = profile;
144
+ this.fetchImpl = fetchImpl;
145
+ this.credentialResolver = credentialResolver;
146
+ this.base = validateUhpEndpoint(profile.endpoint, profile);
147
+ this.limits = { ...DEFAULT_UHP_LIMITS, ...resolveUhpLimits(profile) };
148
+ this.enforceDnsPolicy = fetchImpl === globalThis.fetch;
149
+ }
150
+ async assertResolvedTarget(target) {
151
+ if (!this.enforceDnsPolicy || /^[\d.]+$/.test(target.hostname) || target.hostname.startsWith('['))
152
+ return;
153
+ let addresses;
154
+ try {
155
+ addresses = await lookup(target.hostname, { all: true, verbatim: true });
156
+ }
157
+ catch {
158
+ throw new UhpError('endpoint_resolution_failed', 'UHP endpoint hostname could not be resolved', { remoteState: 'not-started' });
159
+ }
160
+ for (const { address } of addresses) {
161
+ if (!isUhpPrivateAddress(address))
162
+ continue;
163
+ const permitted = isUhpLoopback(address)
164
+ ? this.profile.trust?.allowInsecureLoopback === true
165
+ : this.profile.trust?.allowPrivateNetwork === true;
166
+ if (!permitted)
167
+ throw new UhpError('endpoint_resolution_blocked', 'UHP endpoint resolved outside the profile network trust policy', { remoteState: 'not-started' });
168
+ }
169
+ }
170
+ url(relative, base = this.base) {
171
+ const root = new URL(base.toString());
172
+ if (!root.pathname.endsWith('/'))
173
+ root.pathname += '/';
174
+ return new URL(relative.replace(/^\/+/, ''), root);
175
+ }
176
+ async fetch(relative, init) {
177
+ const authenticated = init.authenticated !== false;
178
+ const secret = authenticated ? await this.credentialResolver(this.profile) : undefined;
179
+ let target = this.url(relative);
180
+ let headers = new Headers(init.headers);
181
+ headers.set('UHP-Version', UHP_VERSION);
182
+ if (authenticated && secret)
183
+ headers.set('Authorization', `Bearer ${secret}`);
184
+ const controller = new AbortController();
185
+ const timeout = setTimeout(() => controller.abort(new Error('request timeout')), init.timeoutMs ?? this.limits.requestTimeoutMs);
186
+ const externalSignal = init.signal;
187
+ const abort = () => controller.abort(externalSignal?.reason);
188
+ externalSignal?.addEventListener('abort', abort, { once: true });
189
+ try {
190
+ for (let redirects = 0;; redirects += 1) {
191
+ await this.assertResolvedTarget(target);
192
+ let response;
193
+ try {
194
+ response = await this.fetchImpl(target, { ...init, headers, redirect: 'manual', signal: controller.signal });
195
+ }
196
+ catch (error) {
197
+ const state = init.method === 'POST' ? 'unknown' : 'not-started';
198
+ throw new UhpError(controller.signal.aborted ? 'request_timeout' : 'network_error', redactUhpText(controller.signal.aborted ? 'UHP request timed out; remote task state is unknown' : `UHP network request failed: ${error.message}`, secret ? [secret] : []), { retryable: true, remoteState: state });
199
+ }
200
+ if (![301, 302, 303, 307, 308].includes(response.status))
201
+ return { response, secret };
202
+ const location = response.headers.get('location');
203
+ if (!this.profile.trust?.allowRedirects || !location || redirects >= 3)
204
+ throw new UhpError('redirect_blocked', 'UHP redirect was blocked by endpoint policy', { remoteState: init.method === 'POST' ? 'unknown' : 'not-started' });
205
+ const next = new URL(location, target);
206
+ validateUhpEndpoint(next.toString(), this.profile);
207
+ if (next.origin !== target.origin && authenticated)
208
+ throw new UhpError('credential_redirect_blocked', 'Cross-origin UHP redirect was blocked before forwarding authentication', { remoteState: init.method === 'POST' ? 'unknown' : 'not-started' });
209
+ target = next;
210
+ }
211
+ }
212
+ finally {
213
+ clearTimeout(timeout);
214
+ externalSignal?.removeEventListener('abort', abort);
215
+ }
216
+ }
217
+ async json(relative, init = {}) {
218
+ const { response, secret } = await this.fetch(relative, init);
219
+ const version = response.headers.get('UHP-Version');
220
+ const bytes = await readBounded(response);
221
+ const payload = bytes.byteLength ? parseJson(bytes) : null;
222
+ if (version !== UHP_VERSION)
223
+ throw new UhpError('protocol_version_mismatch', `Expected UHP-Version ${UHP_VERSION}, received ${version ?? 'none'}`, { remoteState: init.method === 'POST' ? 'unknown' : 'not-started' });
224
+ if (!response.ok)
225
+ throw parseUhpError(response.status, payload, secret ? [secret] : []);
226
+ return payload;
227
+ }
228
+ async discover() {
229
+ return ensureDiscovery(await this.json('v1/uhp', { authenticated: false }));
230
+ }
231
+ async listHarnesses() {
232
+ const payload = await this.json('v1/harnesses');
233
+ if (!Array.isArray(payload.harnesses))
234
+ throw new UhpError('invalid_response', 'UHP harness list is malformed');
235
+ return payload.harnesses;
236
+ }
237
+ async listModels(harnessId) {
238
+ return this.json(harnessId ? `v1/harnesses/${encodeURIComponent(assertId('harness', harnessId))}/models` : 'v1/models');
239
+ }
240
+ async createResponse(request, options = {}) {
241
+ const normalized = { ...request, stream: false, metadata: { ...request.metadata, ...(request.metadata?.harness_id || !this.profile.defaultHarness ? {} : { harness_id: this.profile.defaultHarness }) }, ...(request.model || !this.profile.defaultModel ? {} : { model: this.profile.defaultModel }) };
242
+ if ((normalized.timeout_seconds ?? this.limits.maxTaskSeconds) > this.limits.maxTaskSeconds)
243
+ throw new UhpError('task_budget_exceeded', 'Requested UHP task duration exceeds the profile limit', { remoteState: 'not-started' });
244
+ const digest = canonicalUhpRequestDigest(normalized);
245
+ const key = options.idempotencyKey ?? `aiwg-${digest}`;
246
+ const prior = this.idempotency.get(key);
247
+ if (prior && prior !== digest)
248
+ throw new UhpError('idempotency_key_reused', 'Idempotency key cannot be reused for changed UHP task content', { remoteState: 'not-started' });
249
+ this.idempotency.set(key, digest);
250
+ for (let attempt = 0;; attempt += 1) {
251
+ try {
252
+ const payload = await this.json('v1/responses', {
253
+ method: 'POST', signal: options.signal,
254
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'Idempotency-Key': key },
255
+ body: JSON.stringify(normalized),
256
+ timeoutMs: Math.max(this.limits.requestTimeoutMs, ((normalized.timeout_seconds ?? 0) * 1_000) + 30_000),
257
+ });
258
+ return ensureResponse(payload);
259
+ }
260
+ catch (error) {
261
+ const uhpError = error instanceof UhpError ? error : undefined;
262
+ if (!uhpError?.options.retryable || uhpError.code === 'session_busy' || attempt >= this.limits.maxRetries)
263
+ throw error;
264
+ await new Promise(resolve => setTimeout(resolve, Math.min(10 * 2 ** attempt, 250)));
265
+ options.signal?.throwIfAborted();
266
+ }
267
+ }
268
+ }
269
+ async *streamResponse(request, options = {}) {
270
+ const normalized = { ...request, stream: true, metadata: { ...request.metadata, ...(request.metadata?.harness_id || !this.profile.defaultHarness ? {} : { harness_id: this.profile.defaultHarness }) } };
271
+ const digest = canonicalUhpRequestDigest(normalized);
272
+ const key = options.idempotencyKey ?? `aiwg-${digest}`;
273
+ const prior = this.idempotency.get(key);
274
+ if (prior && prior !== digest)
275
+ throw new UhpError('idempotency_key_reused', 'Idempotency key cannot be reused for changed UHP task content', { remoteState: 'not-started' });
276
+ this.idempotency.set(key, digest);
277
+ const { response, secret } = await this.fetch('v1/responses', {
278
+ method: 'POST', signal: options.signal,
279
+ headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', 'Idempotency-Key': key }, body: JSON.stringify(normalized),
280
+ timeoutMs: Math.max(this.limits.requestTimeoutMs, ((normalized.timeout_seconds ?? 0) * 1_000) + 30_000),
281
+ });
282
+ if (response.headers.get('UHP-Version') !== UHP_VERSION)
283
+ throw new UhpError('protocol_version_mismatch', 'UHP stream response did not preserve the pinned version', { remoteState: 'unknown' });
284
+ if (!response.ok)
285
+ throw parseUhpError(response.status, parseJson(await readBounded(response)), secret ? [secret] : []);
286
+ if (!response.headers.get('content-type')?.toLowerCase().startsWith('text/event-stream') || !response.body)
287
+ throw new UhpError('invalid_stream', 'UHP streaming response must be text/event-stream', { remoteState: 'unknown' });
288
+ yield* parseUhpEventStream(response.body, { inactivityTimeoutMs: this.limits.inactivityTimeoutMs, secrets: secret ? [secret] : [], signal: options.signal });
289
+ }
290
+ async readResponse(responseId) {
291
+ return ensureResponse(await this.json(`v1/responses/${encodeURIComponent(assertId('response', responseId))}`));
292
+ }
293
+ /** Stored-response reads are authoritative after stream loss or local timeout. */
294
+ async reconcileUnknownResponse(responseId) {
295
+ return this.readResponse(responseId);
296
+ }
297
+ async continueResponse(previousResponseId, request, options = {}) {
298
+ const previous = await this.readResponse(previousResponseId);
299
+ const harness = previous.metadata.harness_id;
300
+ if (request.metadata?.harness_id && harness && request.metadata.harness_id !== harness)
301
+ throw new UhpError('harness_mismatch', 'Continuation harness does not match the existing UHP session', { remoteState: 'not-started' });
302
+ return this.createResponse({ ...request, previous_response_id: previousResponseId, metadata: { ...request.metadata, ...(harness ? { harness_id: harness } : {}) } }, options);
303
+ }
304
+ async cancelResponse(responseId, options = {}) {
305
+ const id = encodeURIComponent(assertId('response', responseId));
306
+ let response = ensureResponse(await this.json(`v1/responses/${id}/cancel`, { method: 'POST', signal: options.signal, headers: { 'Content-Type': 'application/json' }, body: '{}' }));
307
+ if (!options.wait)
308
+ return response;
309
+ for (let attempt = 0; attempt < 20 && !TERMINAL.has(response.status); attempt += 1) {
310
+ await new Promise(resolve => setTimeout(resolve, Math.min(250 * 2 ** attempt, 2_000)));
311
+ options.signal?.throwIfAborted();
312
+ response = await this.readResponse(responseId);
313
+ }
314
+ return response;
315
+ }
316
+ async listArtifacts(sessionId) {
317
+ const payload = await this.json(`v1/sessions/${encodeURIComponent(assertId('session', sessionId))}/files`);
318
+ if (!Array.isArray(payload.files))
319
+ throw new UhpError('invalid_response', 'UHP artifact list is malformed');
320
+ if (payload.files.length > this.limits.maxArtifactCount)
321
+ throw new UhpError('artifact_count_exceeded', 'UHP artifact count exceeds the profile limit');
322
+ return payload.files;
323
+ }
324
+ async downloadArtifact(containerId, fileId, destinationDirectory, filename) {
325
+ assertId('container', containerId);
326
+ assertId('file', fileId);
327
+ await mkdir(destinationDirectory, { recursive: true, mode: 0o700 });
328
+ const directoryInfo = await lstat(destinationDirectory);
329
+ if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink())
330
+ throw new UhpError('unsafe_artifact_destination', 'Artifact destination must be a non-symlink directory');
331
+ const { response, secret } = await this.fetch(`v1/containers/${encodeURIComponent(containerId)}/files/${encodeURIComponent(fileId)}/content`, { method: 'GET' });
332
+ if (response.headers.get('UHP-Version') !== UHP_VERSION)
333
+ throw new UhpError('protocol_version_mismatch', 'Artifact response did not preserve the pinned UHP version');
334
+ if (!response.ok)
335
+ throw parseUhpError(response.status, parseJson(await readBounded(response)), secret ? [secret] : []);
336
+ if (response.headers.get('x-content-type-options')?.toLowerCase() !== 'nosniff')
337
+ throw new UhpError('artifact_missing_nosniff', 'Refusing UHP artifact without X-Content-Type-Options: nosniff');
338
+ const bytes = await readBounded(response, this.limits.maxArtifactBytes);
339
+ const target = path.resolve(destinationDirectory, safeFilename(filename ?? fileId));
340
+ const root = path.resolve(destinationDirectory) + path.sep;
341
+ if (!target.startsWith(root))
342
+ throw new UhpError('artifact_path_traversal', 'Artifact filename escaped the approved destination');
343
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
344
+ const handle = await open(temporary, 'wx', 0o600);
345
+ try {
346
+ await handle.writeFile(bytes);
347
+ await handle.sync();
348
+ }
349
+ finally {
350
+ await handle.close();
351
+ }
352
+ try {
353
+ // A hard-link publishes atomically and, unlike rename, never overwrites an
354
+ // existing approved artifact path.
355
+ await link(temporary, target);
356
+ await unlink(temporary);
357
+ }
358
+ catch (error) {
359
+ await unlink(temporary).catch(() => undefined);
360
+ throw error;
361
+ }
362
+ return { path: target, bytes: bytes.byteLength, contentType: response.headers.get('content-type') ?? 'application/octet-stream' };
363
+ }
364
+ async uploadFile(filename) {
365
+ const info = await stat(filename);
366
+ if (!info.isFile() || info.size > this.limits.maxUploadBytes)
367
+ throw new UhpError('file_too_large', 'UHP upload is not a regular file within the configured size limit', { remoteState: 'not-started' });
368
+ const data = await readFile(filename);
369
+ const form = new FormData();
370
+ form.append('file', new Blob([data]), safeFilename(path.basename(filename)));
371
+ return this.json('v1/files', { method: 'POST', body: form });
372
+ }
373
+ }
374
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,130 @@
1
+ import { isIP } from 'node:net';
2
+ import { UHP_VERSION } from './types.js';
3
+ export const DEFAULT_UHP_LIMITS = {
4
+ requestTimeoutMs: 10 * 60_000,
5
+ inactivityTimeoutMs: 45_000,
6
+ maxTaskSeconds: 3_600,
7
+ maxUploadBytes: 50 * 1024 * 1024,
8
+ maxArtifactBytes: 100 * 1024 * 1024,
9
+ maxArtifactCount: 100,
10
+ maxRetries: 3,
11
+ };
12
+ const PROFILE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
13
+ const OBJECT_ID = /^(?:chrn_|resp_|hsess|cntr_|file_)[A-Za-z0-9_-]+$/;
14
+ const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
15
+ export function isUhpLoopback(hostname) {
16
+ const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
17
+ return host === 'localhost' || host === '::1' || host.startsWith('127.');
18
+ }
19
+ export function isUhpPrivateAddress(hostname) {
20
+ const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
21
+ if (isUhpLoopback(host))
22
+ return true;
23
+ if (isIP(host) === 4) {
24
+ const [a, b] = host.split('.').map(Number);
25
+ return a === 10 || a === 0 || a === 127 || (a === 169 && b === 254)
26
+ || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
27
+ }
28
+ return isIP(host) === 6 && (host === '::' || host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:'));
29
+ }
30
+ export function validateUhpEndpoint(endpoint, profile) {
31
+ let url;
32
+ try {
33
+ url = new URL(endpoint);
34
+ }
35
+ catch {
36
+ throw new Error('UHP endpoint must be an absolute URL');
37
+ }
38
+ if (url.username || url.password)
39
+ throw new Error('UHP endpoint must not contain inline credentials');
40
+ if (url.hash)
41
+ throw new Error('UHP endpoint must not contain a fragment');
42
+ if (!['https:', 'http:'].includes(url.protocol))
43
+ throw new Error('UHP endpoint must use HTTPS');
44
+ if (url.protocol === 'http:' && !(isUhpLoopback(url.hostname) && profile.trust?.allowInsecureLoopback === true)) {
45
+ throw new Error('UHP endpoint requires TLS except explicitly allowed loopback development');
46
+ }
47
+ if (isUhpPrivateAddress(url.hostname)) {
48
+ const permitted = isUhpLoopback(url.hostname)
49
+ ? profile.trust?.allowInsecureLoopback === true
50
+ : profile.trust?.allowPrivateNetwork === true;
51
+ if (!permitted)
52
+ throw new Error('UHP endpoint targets a private network outside the profile trust policy');
53
+ }
54
+ if (profile.trust?.allowedHosts?.length
55
+ && !profile.trust.allowedHosts.some(host => host.toLowerCase() === url.hostname.toLowerCase())) {
56
+ throw new Error(`UHP endpoint host '${url.hostname}' is not allowed by profile trust policy`);
57
+ }
58
+ return url;
59
+ }
60
+ export function validateUhpConfig(config) {
61
+ if (config === undefined || config === null)
62
+ return [];
63
+ if (typeof config !== 'object' || Array.isArray(config))
64
+ return ['uhp: must be an object'];
65
+ const value = config;
66
+ const errors = [];
67
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean')
68
+ errors.push('uhp.enabled: must be boolean');
69
+ if (value.profiles === undefined)
70
+ return errors;
71
+ if (!value.profiles || typeof value.profiles !== 'object' || Array.isArray(value.profiles))
72
+ return [...errors, 'uhp.profiles: must be an object'];
73
+ for (const [name, raw] of Object.entries(value.profiles)) {
74
+ const where = `uhp.profiles.${name}`;
75
+ if (!PROFILE_NAME.test(name))
76
+ errors.push(`${where}: invalid profile name`);
77
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
78
+ errors.push(`${where}: must be an object`);
79
+ continue;
80
+ }
81
+ const profile = raw;
82
+ for (const forbidden of ['token', 'bearer', 'apiKey', 'authorization']) {
83
+ if (forbidden in profile)
84
+ errors.push(`${where}.${forbidden}: inline credentials are forbidden`);
85
+ }
86
+ if (typeof profile.endpoint !== 'string')
87
+ errors.push(`${where}.endpoint: required string`);
88
+ else {
89
+ try {
90
+ validateUhpEndpoint(profile.endpoint, profile);
91
+ }
92
+ catch (error) {
93
+ errors.push(`${where}.endpoint: ${error.message}`);
94
+ }
95
+ }
96
+ if (profile.version !== UHP_VERSION)
97
+ errors.push(`${where}.version: must be '${UHP_VERSION}'`);
98
+ if (profile.experimental !== true)
99
+ errors.push(`${where}.experimental: must be true`);
100
+ if (!profile.credential || profile.credential.source !== 'env' || !/^[A-Z_][A-Z0-9_]*$/.test(profile.credential.name ?? '')) {
101
+ errors.push(`${where}.credential: must be an env secret reference with an uppercase variable name`);
102
+ }
103
+ if (profile.defaultHarness !== undefined && !OBJECT_ID.test(profile.defaultHarness))
104
+ errors.push(`${where}.defaultHarness: malformed UHP harness id`);
105
+ if (profile.defaultModel !== undefined && !MODEL_ID.test(profile.defaultModel))
106
+ errors.push(`${where}.defaultModel: malformed model id`);
107
+ if (profile.limits) {
108
+ for (const [key, limit] of Object.entries(profile.limits)) {
109
+ if (!(key in DEFAULT_UHP_LIMITS) || !Number.isSafeInteger(limit) || Number(limit) <= 0)
110
+ errors.push(`${where}.limits.${key}: must be a positive supported integer`);
111
+ }
112
+ }
113
+ }
114
+ return errors;
115
+ }
116
+ export function resolveUhpProfile(config, name) {
117
+ if (!config?.enabled)
118
+ throw new Error('Experimental UHP transport is not enabled');
119
+ const profile = config.profiles?.[name];
120
+ if (!profile)
121
+ throw new Error(`Unknown UHP endpoint profile '${name}'`);
122
+ const errors = validateUhpConfig({ enabled: true, profiles: { [name]: profile } });
123
+ if (errors.length)
124
+ throw new Error(errors.join('\n'));
125
+ return profile;
126
+ }
127
+ export function resolveUhpLimits(profile) {
128
+ return { ...DEFAULT_UHP_LIMITS, ...profile.limits };
129
+ }
130
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,63 @@
1
+ const SECRET_PATTERNS = [
2
+ /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi,
3
+ /\b(?:token|authorization|api[-_ ]?key)\b\s*[:=]\s*[^\s,;]+/gi,
4
+ ];
5
+ export function redactUhpText(value, secrets = []) {
6
+ let redacted = value;
7
+ for (const secret of secrets) {
8
+ if (secret)
9
+ redacted = redacted.split(secret).join('[REDACTED]');
10
+ }
11
+ for (const pattern of SECRET_PATTERNS)
12
+ redacted = redacted.replace(pattern, '[REDACTED]');
13
+ return redacted.slice(0, 2_000);
14
+ }
15
+ function redactUhpValue(value, secrets, key) {
16
+ if (key && /^(?:authorization|token|api[-_]?key|secret)$/i.test(key))
17
+ return '[REDACTED]';
18
+ if (typeof value === 'string')
19
+ return redactUhpText(value, secrets);
20
+ if (Array.isArray(value))
21
+ return value.map(item => redactUhpValue(item, secrets));
22
+ if (value && typeof value === 'object') {
23
+ return Object.fromEntries(Object.entries(value)
24
+ .map(([childKey, child]) => [childKey, redactUhpValue(child, secrets, childKey)]));
25
+ }
26
+ return value;
27
+ }
28
+ export class UhpError extends Error {
29
+ code;
30
+ options;
31
+ name = 'UhpError';
32
+ constructor(code, message, options = {}) {
33
+ super(redactUhpText(message));
34
+ this.code = code;
35
+ this.options = options;
36
+ }
37
+ }
38
+ export function isRetryableUhpError(code, status) {
39
+ if (code === 'rate_limited' || code === 'session_busy' || code === 'harness_unavailable')
40
+ return true;
41
+ if (code === 'quota_exhausted')
42
+ return false;
43
+ return status !== undefined && [500, 502, 503, 504].includes(status);
44
+ }
45
+ export function parseUhpError(status, payload, secrets = []) {
46
+ const candidate = payload && typeof payload === 'object'
47
+ ? payload.error
48
+ : undefined;
49
+ const code = typeof candidate?.code === 'string' ? candidate.code : `http_${status}`;
50
+ const message = typeof candidate?.message === 'string' ? candidate.message : `UHP request failed with HTTP ${status}`;
51
+ const detail = candidate?.detail && typeof candidate.detail === 'object'
52
+ ? redactUhpValue(candidate.detail, secrets)
53
+ : null;
54
+ return new UhpError(code, redactUhpText(message, secrets), {
55
+ status,
56
+ type: typeof candidate?.type === 'string' ? candidate.type : undefined,
57
+ param: typeof candidate?.param === 'string' ? candidate.param : null,
58
+ detail,
59
+ retryable: isRetryableUhpError(code, status),
60
+ remoteState: status >= 500 ? 'unknown' : 'not-started',
61
+ });
62
+ }
63
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './errors.js';
3
+ export * from './config.js';
4
+ export * from './sse.js';
5
+ export * from './mission.js';
6
+ export * from './client.js';
7
+ //# sourceMappingURL=index.js.map