@lazyingart/agent-web 0.1.40
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.
- package/LICENSE +22 -0
- package/README.md +438 -0
- package/docs/architecture.md +503 -0
- package/package.json +43 -0
- package/src/aginti-adapter.js +602 -0
- package/src/chat-context.js +1020 -0
- package/src/chat-migrations.js +947 -0
- package/src/chat-store.js +3308 -0
- package/src/cli.js +134 -0
- package/src/cloud-server.js +2043 -0
- package/src/contracts.js +103 -0
- package/src/deterministic-context-summarizer.js +254 -0
- package/src/direct-chat-capability-limits.js +66 -0
- package/src/direct-chat-contract.js +3 -0
- package/src/errors.js +50 -0
- package/src/http-contract.js +592 -0
- package/src/index.js +88 -0
- package/src/localllm-connector.js +667 -0
- package/src/migrations.js +231 -0
- package/src/operator-health.js +184 -0
- package/src/password-verifier.js +131 -0
- package/src/service-config.js +547 -0
- package/src/service.js +408 -0
- package/src/sqlite-health.js +83 -0
- package/src/storage-path.js +130 -0
- package/src/store.js +914 -0
- package/src/validation.js +181 -0
- package/src/vision-attachment.js +404 -0
- package/src/web/aginti-client.js +552 -0
- package/src/web/aginti-protocol.js +1146 -0
- package/src/web/asset-map.js +462 -0
- package/src/web/browser-app.js +6491 -0
- package/src/web/cloud-session-client.js +427 -0
- package/src/web/direct-chat-client.js +1482 -0
- package/src/web/index.js +10 -0
- package/src/web/presentation-state.js +107 -0
- package/src/web/pwa-assets.js +854 -0
- package/src/web/pwa-update-handoff-store.js +179 -0
- package/src/web/safe-rendering.js +836 -0
- package/src/web/vision-image-client.js +546 -0
- package/src/web/vision-image-sanitizer.js +168 -0
- package/src/web/web-release.js +28 -0
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer as createNodeServer } from 'node:http';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AGENT_ROUTE_MAP,
|
|
7
|
+
CLOUD_HTTP_LIMITS,
|
|
8
|
+
CLOUD_ROUTES,
|
|
9
|
+
CLIENT_RELEASE_HEADER_NAME,
|
|
10
|
+
CSRF_COOKIE_NAME,
|
|
11
|
+
CSRF_HEADER_NAME,
|
|
12
|
+
IDEMPOTENCY_HEADER_NAME,
|
|
13
|
+
SESSION_COOKIE_NAME,
|
|
14
|
+
TRUSTED_CLIENT_ADDRESS_HEADER,
|
|
15
|
+
TRUSTED_PUBLIC_AUTHORITY_HEADER,
|
|
16
|
+
CloudHttpError,
|
|
17
|
+
bodyLimitForRoute,
|
|
18
|
+
classifyRequestTarget,
|
|
19
|
+
routeRequiresIdempotency,
|
|
20
|
+
snapshotAndValidateAssetMap,
|
|
21
|
+
validateAccountConfig,
|
|
22
|
+
validateAgentIdempotencyKey,
|
|
23
|
+
validateChatRequest,
|
|
24
|
+
validateEmptyBody,
|
|
25
|
+
validateLoginBody,
|
|
26
|
+
validatePublicOrigin,
|
|
27
|
+
validateRequestIdempotencyKey,
|
|
28
|
+
validateTransportAgentRequest
|
|
29
|
+
} from './http-contract.js';
|
|
30
|
+
import { ControlPlaneError } from './errors.js';
|
|
31
|
+
import { VISION_MODEL_ALIAS } from './vision-attachment.js';
|
|
32
|
+
import {
|
|
33
|
+
AGINTI_MAX_FILE_ARTIFACT_BYTES,
|
|
34
|
+
AGINTI_RPC_PATHS,
|
|
35
|
+
FAIL_CLOSED_AGENT_CAPABILITIES,
|
|
36
|
+
validateFileSpec,
|
|
37
|
+
validateAgentResponse,
|
|
38
|
+
validateEventEnvelope
|
|
39
|
+
} from './web/aginti-protocol.js';
|
|
40
|
+
|
|
41
|
+
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
42
|
+
const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
43
|
+
const DYNAMIC_CACHE_CONTROL = 'no-store';
|
|
44
|
+
const SESSION_MAX_AGE_SECONDS = 12 * 60 * 60;
|
|
45
|
+
const REMEMBERED_SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
46
|
+
const EPHEMERAL_SESSION_MAX_AGE_SECONDS = 60;
|
|
47
|
+
const GLOBAL_DISPATCH_RETRY_MS = 250;
|
|
48
|
+
const REQUEST_BODY_TIMEOUT_MARGIN_MS = 30_000;
|
|
49
|
+
const RESPONSE_RELEASE_ID = Symbol('responseReleaseId');
|
|
50
|
+
const SAFE_FAILURE_CODES = new Set([
|
|
51
|
+
'provider_unavailable',
|
|
52
|
+
'timeout',
|
|
53
|
+
'internal_error',
|
|
54
|
+
'response_limit',
|
|
55
|
+
'content_rejected'
|
|
56
|
+
]);
|
|
57
|
+
const PUBLIC_THREAD_FIELDS = Object.freeze([
|
|
58
|
+
'threadId', 'title', 'modelAlias', 'revision', 'ledgerHash', 'messageCount',
|
|
59
|
+
'ledgerBytes', 'currentGenerationId', 'createdAt', 'updatedAt'
|
|
60
|
+
]);
|
|
61
|
+
const PUBLIC_MESSAGE_FIELDS = Object.freeze([
|
|
62
|
+
'threadId', 'messageId', 'revision', 'role', 'content', 'contentBytes',
|
|
63
|
+
'previousHash', 'messageHash', 'generationId', 'createdAt'
|
|
64
|
+
]);
|
|
65
|
+
const PUBLIC_GENERATION_FIELDS = Object.freeze([
|
|
66
|
+
'threadId', 'generationId', 'assistantMessageId', 'status', 'terminal',
|
|
67
|
+
'modelAlias', 'sourceRevision', 'sourceHash', 'deltaCount', 'deltaBytes',
|
|
68
|
+
'lastDeltaHash', 'finalRevision', 'finalHash', 'failureCode', 'deltasPruned',
|
|
69
|
+
'startedAt', 'updatedAt', 'terminalAt', 'prunedAt'
|
|
70
|
+
]);
|
|
71
|
+
const PUBLIC_DELTA_FIELDS = Object.freeze([
|
|
72
|
+
'threadId', 'generationId', 'sequence', 'content', 'contentBytes',
|
|
73
|
+
'previousHash', 'deltaHash', 'createdAt'
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
function epochMilliseconds(clock) {
|
|
77
|
+
const value = clock();
|
|
78
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
79
|
+
const result = date.getTime();
|
|
80
|
+
if (!Number.isFinite(result)) throw new TypeError('clock returned an invalid time');
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function exactLimitOverrides(input = {}) {
|
|
85
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)
|
|
86
|
+
|| Object.getPrototypeOf(input) !== Object.prototype) {
|
|
87
|
+
throw new TypeError('limits must be a plain object');
|
|
88
|
+
}
|
|
89
|
+
const bounds = Object.freeze({
|
|
90
|
+
bodyTimeoutMs: [50, 15_000],
|
|
91
|
+
visionBodyTimeoutMs: [1_000, 300_000],
|
|
92
|
+
dependencyTimeoutMs: [50, 120_000],
|
|
93
|
+
jobTimeoutMs: [50, 600_000],
|
|
94
|
+
visionJobTimeoutMs: [50, 900_000],
|
|
95
|
+
sseLifetimeMs: [100, 120_000],
|
|
96
|
+
ssePollMs: [5, 5_000],
|
|
97
|
+
concurrentBodies: [1, 256],
|
|
98
|
+
concurrentBodiesPerSource: [1, 16],
|
|
99
|
+
concurrentLogins: [1, 32],
|
|
100
|
+
concurrentLoginsPerSource: [1, 4],
|
|
101
|
+
concurrentStreams: [1, 64],
|
|
102
|
+
concurrentStreamsPerSession: [1, 8],
|
|
103
|
+
loginAttemptsPerMinute: [1, 60],
|
|
104
|
+
directChatJobs: [1, 32]
|
|
105
|
+
});
|
|
106
|
+
const result = { ...CLOUD_HTTP_LIMITS };
|
|
107
|
+
for (const [name, value] of Object.entries(input)) {
|
|
108
|
+
if (!Object.hasOwn(bounds, name)) throw new TypeError(`unsupported HTTP limit ${name}`);
|
|
109
|
+
const [minimum, maximum] = bounds[name];
|
|
110
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
111
|
+
throw new TypeError(`${name} is outside its safe range`);
|
|
112
|
+
}
|
|
113
|
+
result[name] = value;
|
|
114
|
+
}
|
|
115
|
+
if (result.concurrentBodiesPerSource > result.concurrentBodies
|
|
116
|
+
|| result.concurrentLoginsPerSource > result.concurrentLogins
|
|
117
|
+
|| result.concurrentStreamsPerSession > result.concurrentStreams) {
|
|
118
|
+
throw new TypeError('per-source concurrency cannot exceed total concurrency');
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze(result);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function requireMethods(value, name, methods) {
|
|
124
|
+
if (!value || methods.some((method) => typeof value[method] !== 'function')) {
|
|
125
|
+
throw new TypeError(`${name} must provide ${methods.join('(), ')}()`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validatePasswordVerifier(value) {
|
|
131
|
+
if (!value || value.algorithm !== 'scrypt' || typeof value.verify !== 'function') {
|
|
132
|
+
throw new TypeError('passwordVerifier must be an injected scrypt verifier');
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sha256(value) {
|
|
138
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function derivedIdempotencyKey(label, ...parts) {
|
|
142
|
+
return `${label}.${sha256(parts.join('\u0000'))}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function safeEqual(first, second) {
|
|
146
|
+
if (typeof first !== 'string' || typeof second !== 'string') return false;
|
|
147
|
+
const firstBytes = Buffer.from(first, 'utf8');
|
|
148
|
+
const secondBytes = Buffer.from(second, 'utf8');
|
|
149
|
+
const maximum = Math.max(firstBytes.byteLength, secondBytes.byteLength, 1);
|
|
150
|
+
const left = Buffer.alloc(maximum);
|
|
151
|
+
const right = Buffer.alloc(maximum);
|
|
152
|
+
firstBytes.copy(left);
|
|
153
|
+
secondBytes.copy(right);
|
|
154
|
+
return timingSafeEqual(left, right) && firstBytes.byteLength === secondBytes.byteLength;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function opaqueToken() {
|
|
158
|
+
return randomBytes(32).toString('base64url');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function commonSecurityHeaders() {
|
|
162
|
+
return {
|
|
163
|
+
'x-content-type-options': 'nosniff',
|
|
164
|
+
'referrer-policy': 'same-origin',
|
|
165
|
+
'x-frame-options': 'DENY',
|
|
166
|
+
'permissions-policy': 'camera=(), microphone=(), geolocation=(), payment=(), usb=()',
|
|
167
|
+
'strict-transport-security': 'max-age=31536000; includeSubDomains',
|
|
168
|
+
'cross-origin-resource-policy': 'same-origin',
|
|
169
|
+
'cross-origin-opener-policy': 'same-origin'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function dynamicHeaders(extra = {}, releaseId) {
|
|
174
|
+
return {
|
|
175
|
+
...commonSecurityHeaders(),
|
|
176
|
+
'cache-control': DYNAMIC_CACHE_CONTROL,
|
|
177
|
+
pragma: 'no-cache',
|
|
178
|
+
expires: '0',
|
|
179
|
+
...(releaseId === undefined ? {} : { [CLIENT_RELEASE_HEADER_NAME]: releaseId }),
|
|
180
|
+
...extra
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function writeHead(res, status, headers) {
|
|
185
|
+
if (res.headersSent) return;
|
|
186
|
+
res.writeHead(status, headers);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sendBuffer(req, res, status, body, headers) {
|
|
190
|
+
const payload = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
191
|
+
writeHead(res, status, { ...headers, 'content-length': String(payload.byteLength) });
|
|
192
|
+
res.end(req.method === 'HEAD' ? undefined : payload);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function encodeJson(value, maximum = CLOUD_HTTP_LIMITS.responseJsonBytes) {
|
|
196
|
+
const body = Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
|
|
197
|
+
if (body.byteLength > maximum) throw new CloudHttpError(502, 'response_too_large', 'An upstream response exceeded its public limit.');
|
|
198
|
+
return body;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sendJson(req, res, status, value, extraHeaders = {}) {
|
|
202
|
+
sendBuffer(req, res, status, encodeJson(value), dynamicHeaders({
|
|
203
|
+
'content-type': JSON_CONTENT_TYPE,
|
|
204
|
+
...extraHeaders
|
|
205
|
+
}, res[RESPONSE_RELEASE_ID]));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function publicError(error) {
|
|
209
|
+
if (error instanceof CloudHttpError) return error;
|
|
210
|
+
if (error instanceof ControlPlaneError) {
|
|
211
|
+
const mapping = {
|
|
212
|
+
invalid_input: [400, 'invalid_request'],
|
|
213
|
+
not_found: [404, 'not_found'],
|
|
214
|
+
conflict: [409, 'conflict'],
|
|
215
|
+
idempotency_conflict: [409, 'idempotency_conflict'],
|
|
216
|
+
storage_security_error: [503, 'storage_unavailable'],
|
|
217
|
+
storage_corruption: [503, 'storage_unavailable'],
|
|
218
|
+
unsupported_schema: [503, 'storage_unavailable']
|
|
219
|
+
};
|
|
220
|
+
const [status, code] = mapping[error.code] ?? [500, 'internal_error'];
|
|
221
|
+
return new CloudHttpError(status, code, status >= 500 ? 'The service is temporarily unavailable.' : 'The request could not be completed.');
|
|
222
|
+
}
|
|
223
|
+
if (typeof error?.code === 'string' && error.code.startsWith('AGINTI_')) {
|
|
224
|
+
const status = Number(error.statusCode);
|
|
225
|
+
return new CloudHttpError(
|
|
226
|
+
[400, 401, 403, 404, 409, 429, 502, 503, 504].includes(status) ? status : 503,
|
|
227
|
+
status === 429 ? 'agent_rate_limited' : 'agent_unavailable',
|
|
228
|
+
status < 500 ? 'The Agent request was not accepted.' : 'AgInTi Agent is temporarily unavailable.'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (error?.name === 'AbortError') return new CloudHttpError(499, 'request_cancelled', 'The request was cancelled.');
|
|
232
|
+
if (error?.name === 'TimeoutError') return new CloudHttpError(504, 'dependency_timeout', 'A required service timed out.');
|
|
233
|
+
return new CloudHttpError(500, 'internal_error', 'The service could not complete the request.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sendError(req, res, error) {
|
|
237
|
+
const safe = publicError(error);
|
|
238
|
+
if (res.headersSent) {
|
|
239
|
+
if (!res.writableEnded) res.end();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const headers = {
|
|
243
|
+
...(safe.retryAfter === undefined ? {} : { 'retry-after': String(safe.retryAfter) }),
|
|
244
|
+
...([408, 413].includes(safe.status) || safe.code === 'unexpected_body' || req.shouldKeepAlive === false
|
|
245
|
+
? { connection: 'close' }
|
|
246
|
+
: {})
|
|
247
|
+
};
|
|
248
|
+
sendJson(req, res, safe.status === 499 ? 400 : safe.status, {
|
|
249
|
+
error: { code: safe.code, message: safe.message }
|
|
250
|
+
}, headers);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function methodNotAllowed(req, res, allow) {
|
|
254
|
+
sendJson(req, res, 405, { error: { code: 'method_not_allowed', message: 'The request method is not allowed.' } }, { allow });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function rawHeaderValues(req, requestedName) {
|
|
258
|
+
const name = requestedName.toLowerCase();
|
|
259
|
+
const values = [];
|
|
260
|
+
if (Array.isArray(req.rawHeaders)) {
|
|
261
|
+
for (let index = 0; index + 1 < req.rawHeaders.length; index += 2) {
|
|
262
|
+
if (String(req.rawHeaders[index]).toLowerCase() === name) values.push(req.rawHeaders[index + 1]);
|
|
263
|
+
}
|
|
264
|
+
return values;
|
|
265
|
+
}
|
|
266
|
+
const value = req.headers?.[name];
|
|
267
|
+
if (Array.isArray(value)) return value;
|
|
268
|
+
return value === undefined ? [] : [value];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isLoopbackPeer(peer) {
|
|
272
|
+
return peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function requirePublicAuthority(req, publicHost) {
|
|
276
|
+
const hostValues = rawHeaderValues(req, 'host');
|
|
277
|
+
const asserted = rawHeaderValues(req, TRUSTED_PUBLIC_AUTHORITY_HEADER);
|
|
278
|
+
const peer = req.socket?.remoteAddress;
|
|
279
|
+
if (hostValues.length !== 1 || hostValues[0] !== publicHost) {
|
|
280
|
+
throw new CloudHttpError(421, 'misdirected_request', 'The request authority is not accepted.');
|
|
281
|
+
}
|
|
282
|
+
if (isLoopbackPeer(peer)) {
|
|
283
|
+
if (asserted.length !== 1 || asserted[0] !== publicHost) {
|
|
284
|
+
throw new CloudHttpError(421, 'misdirected_request', 'The trusted proxy authority assertion is not accepted.');
|
|
285
|
+
}
|
|
286
|
+
} else if (asserted.length !== 0) {
|
|
287
|
+
throw new CloudHttpError(421, 'misdirected_request', 'A trusted proxy authority assertion was received from an untrusted peer.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function resolveTrustedClientAddress(req) {
|
|
292
|
+
const peer = req.socket?.remoteAddress;
|
|
293
|
+
const loopback = isLoopbackPeer(peer);
|
|
294
|
+
const asserted = rawHeaderValues(req, TRUSTED_CLIENT_ADDRESS_HEADER);
|
|
295
|
+
if (loopback) {
|
|
296
|
+
if (asserted.length !== 1 || typeof asserted[0] !== 'string' || isIP(asserted[0]) === 0) {
|
|
297
|
+
throw new CloudHttpError(403, 'proxy_assertion_rejected', 'The trusted proxy client-address assertion is missing or invalid.');
|
|
298
|
+
}
|
|
299
|
+
return asserted[0];
|
|
300
|
+
}
|
|
301
|
+
if (asserted.length !== 0) {
|
|
302
|
+
throw new CloudHttpError(403, 'proxy_assertion_rejected', 'A client-address assertion was received from an untrusted peer.');
|
|
303
|
+
}
|
|
304
|
+
if (typeof peer !== 'string' || isIP(peer) === 0) {
|
|
305
|
+
throw new CloudHttpError(403, 'peer_rejected', 'The network peer address is invalid.');
|
|
306
|
+
}
|
|
307
|
+
return peer;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
class ConcurrencyGate {
|
|
311
|
+
#maximum;
|
|
312
|
+
#perSource;
|
|
313
|
+
#sources = new Map();
|
|
314
|
+
#total = 0;
|
|
315
|
+
|
|
316
|
+
constructor(maximum, perSource) {
|
|
317
|
+
this.#maximum = maximum;
|
|
318
|
+
this.#perSource = perSource;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
enter(source) {
|
|
322
|
+
const current = this.#sources.get(source) ?? 0;
|
|
323
|
+
if (this.#total >= this.#maximum || current >= this.#perSource) return null;
|
|
324
|
+
this.#total += 1;
|
|
325
|
+
this.#sources.set(source, current + 1);
|
|
326
|
+
let released = false;
|
|
327
|
+
return () => {
|
|
328
|
+
if (released) return;
|
|
329
|
+
released = true;
|
|
330
|
+
this.#total -= 1;
|
|
331
|
+
const next = (this.#sources.get(source) ?? 1) - 1;
|
|
332
|
+
if (next <= 0) this.#sources.delete(source);
|
|
333
|
+
else this.#sources.set(source, next);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get active() {
|
|
338
|
+
return this.#total;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function hasRequestBodyFraming(req) {
|
|
343
|
+
const contentLengths = rawHeaderValues(req, 'content-length');
|
|
344
|
+
const transferEncodings = rawHeaderValues(req, 'transfer-encoding');
|
|
345
|
+
const contentEncodings = rawHeaderValues(req, 'content-encoding');
|
|
346
|
+
const expectations = rawHeaderValues(req, 'expect');
|
|
347
|
+
const validEmptyLength = contentLengths.length === 0
|
|
348
|
+
|| (contentLengths.length === 1 && contentLengths[0] === '0');
|
|
349
|
+
return !validEmptyLength || transferEncodings.length !== 0
|
|
350
|
+
|| contentEncodings.length !== 0 || expectations.length !== 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function rejectRequestBodyFraming(req) {
|
|
354
|
+
if (hasRequestBodyFraming(req)) {
|
|
355
|
+
req.shouldKeepAlive = false;
|
|
356
|
+
throw new CloudHttpError(400, 'unexpected_body', 'This request does not accept a body.');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function publicOwnedRecord(value, accountId, fields, label) {
|
|
361
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
362
|
+
throw new CloudHttpError(503, 'storage_unavailable', `The stored ${label} ownership is invalid.`);
|
|
363
|
+
}
|
|
364
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
365
|
+
const owner = descriptors.accountId;
|
|
366
|
+
if (!owner || !owner.enumerable || !Object.hasOwn(owner, 'value') || owner.value !== accountId) {
|
|
367
|
+
throw new CloudHttpError(503, 'storage_unavailable', `The stored ${label} ownership is invalid.`);
|
|
368
|
+
}
|
|
369
|
+
const result = {};
|
|
370
|
+
for (const field of fields) {
|
|
371
|
+
const descriptor = descriptors[field];
|
|
372
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
373
|
+
throw new CloudHttpError(503, 'storage_unavailable', `The stored ${label} shape is invalid.`);
|
|
374
|
+
}
|
|
375
|
+
result[field] = descriptor.value;
|
|
376
|
+
}
|
|
377
|
+
return Object.freeze(result);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function publicThread(value, accountId) {
|
|
381
|
+
return publicOwnedRecord(value, accountId, PUBLIC_THREAD_FIELDS, 'chat thread');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function publicMessage(value, accountId, { attachmentSchema = 2 } = {}) {
|
|
385
|
+
const message = publicOwnedRecord(value, accountId, PUBLIC_MESSAGE_FIELDS, 'chat message');
|
|
386
|
+
if (value.attachment === undefined && value.attachments === undefined) return message;
|
|
387
|
+
if (value.attachment !== undefined && value.attachments !== undefined) {
|
|
388
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment shape is ambiguous.');
|
|
389
|
+
}
|
|
390
|
+
const values = value.attachments ?? [value.attachment];
|
|
391
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 4) {
|
|
392
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment list is invalid.');
|
|
393
|
+
}
|
|
394
|
+
const checked = [];
|
|
395
|
+
const identifiers = new Set();
|
|
396
|
+
let bytes = 0;
|
|
397
|
+
for (const attachment of values) {
|
|
398
|
+
if (!attachment || typeof attachment !== 'object' || Array.isArray(attachment)) {
|
|
399
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment descriptor is invalid.');
|
|
400
|
+
}
|
|
401
|
+
const descriptors = Object.getOwnPropertyDescriptors(attachment);
|
|
402
|
+
const expected = ['attachmentId', 'mediaType', 'byteLength', 'width', 'height', 'sha256'];
|
|
403
|
+
if (Reflect.ownKeys(descriptors).length !== expected.length
|
|
404
|
+
|| expected.some((field) => !descriptors[field]?.enumerable
|
|
405
|
+
|| !Object.hasOwn(descriptors[field], 'value'))) {
|
|
406
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment descriptor is invalid.');
|
|
407
|
+
}
|
|
408
|
+
if (identifiers.has(attachment.attachmentId)) {
|
|
409
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment identifiers are not unique.');
|
|
410
|
+
}
|
|
411
|
+
identifiers.add(attachment.attachmentId);
|
|
412
|
+
bytes += attachment.byteLength;
|
|
413
|
+
if (bytes > 16 * 1024 * 1024) {
|
|
414
|
+
throw new CloudHttpError(503, 'storage_unavailable', 'The stored chat attachment list is too large.');
|
|
415
|
+
}
|
|
416
|
+
checked.push(Object.freeze({ ...attachment }));
|
|
417
|
+
}
|
|
418
|
+
return Object.freeze(checked.length === 1 || attachmentSchema === 1
|
|
419
|
+
? { ...message, attachment: checked[0] }
|
|
420
|
+
: { ...message, attachments: Object.freeze(checked) });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function publicGeneration(value, accountId) {
|
|
424
|
+
return publicOwnedRecord(value, accountId, PUBLIC_GENERATION_FIELDS, 'chat generation');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function publicDelta(value, accountId) {
|
|
428
|
+
return publicOwnedRecord(value, accountId, PUBLIC_DELTA_FIELDS, 'chat delta');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function requireAgentResponseCorrelation(pathname, input, response) {
|
|
432
|
+
let matches = true;
|
|
433
|
+
if ([AGINTI_RPC_PATHS.threadsGet, AGINTI_RPC_PATHS.threadsUpdate].includes(pathname)) {
|
|
434
|
+
matches = response.thread.id === input.threadId;
|
|
435
|
+
} else if (pathname === AGINTI_RPC_PATHS.threadsDelete) {
|
|
436
|
+
matches = response.threadId === input.threadId;
|
|
437
|
+
} else if (pathname === AGINTI_RPC_PATHS.runsStart) {
|
|
438
|
+
matches = response.run.threadId === input.threadId;
|
|
439
|
+
} else if ([AGINTI_RPC_PATHS.runsStatus, AGINTI_RPC_PATHS.runsCancel].includes(pathname)) {
|
|
440
|
+
matches = response.run.id === input.runId;
|
|
441
|
+
} else if (pathname === AGINTI_RPC_PATHS.runsResume) {
|
|
442
|
+
matches = response.run.previousRunId === input.runId;
|
|
443
|
+
} else if (pathname === AGINTI_RPC_PATHS.artifactsGet) {
|
|
444
|
+
matches = response.artifact.id === input.artifactId;
|
|
445
|
+
}
|
|
446
|
+
if (!matches) {
|
|
447
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned a response for a different resource.');
|
|
448
|
+
}
|
|
449
|
+
return response;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class LoginAdmission {
|
|
453
|
+
#attempts = new Map();
|
|
454
|
+
#clock;
|
|
455
|
+
#gate;
|
|
456
|
+
#maximumAttempts;
|
|
457
|
+
|
|
458
|
+
constructor({ clock, maximum, perSource, maximumAttempts }) {
|
|
459
|
+
this.#clock = clock;
|
|
460
|
+
this.#gate = new ConcurrencyGate(maximum, perSource);
|
|
461
|
+
this.#maximumAttempts = maximumAttempts;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
enter(source) {
|
|
465
|
+
const now = epochMilliseconds(this.#clock);
|
|
466
|
+
const existing = this.#attempts.get(source);
|
|
467
|
+
const record = !existing || now - existing.windowStart >= 60_000
|
|
468
|
+
? { windowStart: now, count: 0, lastSeen: now }
|
|
469
|
+
: existing;
|
|
470
|
+
if (record.count >= this.#maximumAttempts) {
|
|
471
|
+
record.lastSeen = now;
|
|
472
|
+
this.#attempts.set(source, record);
|
|
473
|
+
return { error: new CloudHttpError(429, 'login_rate_limited', 'Sign-in is temporarily rate limited.', { retryAfter: 60 }) };
|
|
474
|
+
}
|
|
475
|
+
const release = this.#gate.enter(source);
|
|
476
|
+
if (!release) {
|
|
477
|
+
return { error: new CloudHttpError(503, 'login_busy', 'The sign-in service is temporarily busy.', { retryAfter: 2 }) };
|
|
478
|
+
}
|
|
479
|
+
record.count += 1;
|
|
480
|
+
record.lastSeen = now;
|
|
481
|
+
this.#attempts.set(source, record);
|
|
482
|
+
if (this.#attempts.size > 1_024) {
|
|
483
|
+
const oldest = [...this.#attempts.entries()].sort((a, b) => a[1].lastSeen - b[1].lastSeen).slice(0, 256);
|
|
484
|
+
for (const [key] of oldest) if (key !== source) this.#attempts.delete(key);
|
|
485
|
+
}
|
|
486
|
+
return { release };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function validateJsonContentType(req) {
|
|
491
|
+
const encoding = req.headers['content-encoding'];
|
|
492
|
+
if (encoding !== undefined && String(encoding).toLowerCase() !== 'identity') {
|
|
493
|
+
throw new CloudHttpError(415, 'unsupported_content_encoding', 'Compressed request bodies are not accepted.');
|
|
494
|
+
}
|
|
495
|
+
const value = req.headers['content-type'];
|
|
496
|
+
if (typeof value !== 'string') throw new CloudHttpError(415, 'unsupported_media_type', 'Content-Type must be application/json.');
|
|
497
|
+
const parts = value.toLowerCase().split(';').map((part) => part.trim());
|
|
498
|
+
if (parts[0] !== 'application/json' || parts.length > 2 || (parts.length === 2 && parts[1] !== 'charset=utf-8')) {
|
|
499
|
+
throw new CloudHttpError(415, 'unsupported_media_type', 'Content-Type must be application/json.');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function readJsonBody(req, maximum, timeoutMs) {
|
|
504
|
+
validateJsonContentType(req);
|
|
505
|
+
const advertised = req.headers['content-length'];
|
|
506
|
+
if (advertised !== undefined && (typeof advertised !== 'string' || !/^\d+$/u.test(advertised)
|
|
507
|
+
|| Number(advertised) > maximum)) {
|
|
508
|
+
req.shouldKeepAlive = false;
|
|
509
|
+
throw new CloudHttpError(413, 'request_too_large', 'The request body is too large.');
|
|
510
|
+
}
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const chunks = [];
|
|
513
|
+
let bytes = 0;
|
|
514
|
+
let settled = false;
|
|
515
|
+
const finish = (callback) => {
|
|
516
|
+
if (settled) return;
|
|
517
|
+
settled = true;
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
req.off('data', onData);
|
|
520
|
+
req.off('end', onEnd);
|
|
521
|
+
req.off('aborted', onAborted);
|
|
522
|
+
req.off('error', onError);
|
|
523
|
+
callback();
|
|
524
|
+
};
|
|
525
|
+
const onData = (chunk) => {
|
|
526
|
+
bytes += chunk.byteLength;
|
|
527
|
+
if (bytes > maximum) {
|
|
528
|
+
req.shouldKeepAlive = false;
|
|
529
|
+
finish(() => reject(new CloudHttpError(413, 'request_too_large', 'The request body is too large.')));
|
|
530
|
+
req.pause();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
chunks.push(Buffer.from(chunk));
|
|
534
|
+
};
|
|
535
|
+
const onEnd = () => finish(() => {
|
|
536
|
+
let value;
|
|
537
|
+
try {
|
|
538
|
+
const source = Buffer.concat(chunks, bytes).toString('utf8');
|
|
539
|
+
if (Buffer.byteLength(source, 'utf8') !== bytes || source.length === 0) throw new Error('invalid UTF-8 or empty body');
|
|
540
|
+
value = JSON.parse(source);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
reject(new CloudHttpError(400, 'invalid_json', 'The request body must be valid UTF-8 JSON.', { cause: error }));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
resolve(value);
|
|
546
|
+
});
|
|
547
|
+
const onAborted = () => finish(() => reject(new CloudHttpError(400, 'request_aborted', 'The request body was interrupted.')));
|
|
548
|
+
const onError = (error) => finish(() => reject(new CloudHttpError(400, 'request_error', 'The request body could not be read.', { cause: error })));
|
|
549
|
+
const timer = setTimeout(() => finish(() => {
|
|
550
|
+
req.shouldKeepAlive = false;
|
|
551
|
+
reject(new CloudHttpError(408, 'request_timeout', 'The request body was not received in time.'));
|
|
552
|
+
}), timeoutMs);
|
|
553
|
+
timer.unref?.();
|
|
554
|
+
req.on('data', onData);
|
|
555
|
+
req.once('end', onEnd);
|
|
556
|
+
req.once('aborted', onAborted);
|
|
557
|
+
req.once('error', onError);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function requireOrigin(req, publicOrigin) {
|
|
562
|
+
if (req.headers.origin !== publicOrigin) {
|
|
563
|
+
throw new CloudHttpError(403, 'origin_rejected', 'The request origin is not allowed.');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function fetchMetadataState(req) {
|
|
568
|
+
const site = req.headers['sec-fetch-site'];
|
|
569
|
+
const mode = req.headers['sec-fetch-mode'];
|
|
570
|
+
const destination = req.headers['sec-fetch-dest'];
|
|
571
|
+
const present = [site, mode, destination].filter((value) => value !== undefined).length;
|
|
572
|
+
if (present === 0) return 'missing';
|
|
573
|
+
if ((site !== undefined && site !== 'same-origin')
|
|
574
|
+
|| (mode !== undefined && !['cors', 'same-origin'].includes(mode))
|
|
575
|
+
|| (destination !== undefined && destination !== 'empty')) return 'invalid';
|
|
576
|
+
return present === 3 ? 'complete' : 'partial';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function artifactFetchMetadataState(req) {
|
|
580
|
+
const site = req.headers['sec-fetch-site'];
|
|
581
|
+
const mode = req.headers['sec-fetch-mode'];
|
|
582
|
+
const destination = req.headers['sec-fetch-dest'];
|
|
583
|
+
const present = [site, mode, destination].filter((value) => value !== undefined).length;
|
|
584
|
+
if (present === 0) return 'missing';
|
|
585
|
+
if ((site !== undefined && site !== 'same-origin')
|
|
586
|
+
|| (mode !== undefined && !['cors', 'same-origin', 'navigate'].includes(mode))
|
|
587
|
+
|| (destination !== undefined && !['empty', 'document'].includes(destination))) return 'invalid';
|
|
588
|
+
return present === 3 ? 'complete' : 'partial';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function rejectFetchMetadata() {
|
|
592
|
+
throw new CloudHttpError(403, 'fetch_metadata_rejected', 'The request fetch metadata is not allowed.');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function clientReleaseState(req, currentRelease) {
|
|
596
|
+
const value = req.headers[CLIENT_RELEASE_HEADER_NAME];
|
|
597
|
+
if (value === undefined) return 'missing';
|
|
598
|
+
return typeof value === 'string' && value === currentRelease ? 'match' : 'mismatch';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function artifactClientReleaseState(req, routeRelease, currentRelease) {
|
|
602
|
+
const header = clientReleaseState(req, currentRelease);
|
|
603
|
+
if (routeRelease !== currentRelease || header === 'mismatch') return 'mismatch';
|
|
604
|
+
return 'match';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function rejectClientRelease() {
|
|
608
|
+
throw new CloudHttpError(409, 'client_release_mismatch', 'The browser app must load the current release before retrying.');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function parseCookie(req, name) {
|
|
612
|
+
const header = req.headers.cookie;
|
|
613
|
+
if (header === undefined) return null;
|
|
614
|
+
if (typeof header !== 'string' || Buffer.byteLength(header, 'utf8') > CLOUD_HTTP_LIMITS.cookieBytes) {
|
|
615
|
+
throw new CloudHttpError(400, 'invalid_cookie', 'The Cookie header is invalid.');
|
|
616
|
+
}
|
|
617
|
+
const matches = [];
|
|
618
|
+
for (const part of header.split(';')) {
|
|
619
|
+
const index = part.indexOf('=');
|
|
620
|
+
if (index < 1) continue;
|
|
621
|
+
if (part.slice(0, index).trim() === name) matches.push(part.slice(index + 1).trim());
|
|
622
|
+
}
|
|
623
|
+
if (matches.length > 1) throw new CloudHttpError(400, 'invalid_cookie', 'A session cookie was duplicated.');
|
|
624
|
+
if (matches.length === 0) return null;
|
|
625
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/u.test(matches[0])) throw new CloudHttpError(401, 'invalid_session', 'The browser session is invalid.');
|
|
626
|
+
return matches[0];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function csrfFromRequest(req) {
|
|
630
|
+
const value = req.headers[CSRF_HEADER_NAME];
|
|
631
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{32,128}$/u.test(value)) {
|
|
632
|
+
throw new CloudHttpError(403, 'csrf_rejected', 'The CSRF token is missing or invalid.');
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function requestIdempotency(req, agent = false) {
|
|
638
|
+
const value = req.headers[IDEMPOTENCY_HEADER_NAME];
|
|
639
|
+
return agent ? validateAgentIdempotencyKey(value) : validateRequestIdempotencyKey(value);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function artifactByteRange(req) {
|
|
643
|
+
const values = rawHeaderValues(req, 'range');
|
|
644
|
+
if (values.length === 0) return undefined;
|
|
645
|
+
if (values.length !== 1 || typeof values[0] !== 'string') {
|
|
646
|
+
throw new CloudHttpError(400, 'invalid_range', 'The artifact byte range is invalid.');
|
|
647
|
+
}
|
|
648
|
+
const match = /^bytes=(0|[1-9]\d*)-(?:(0|[1-9]\d*))?$/u.exec(values[0]);
|
|
649
|
+
if (!match) throw new CloudHttpError(400, 'invalid_range', 'Only one start-based byte range is supported.');
|
|
650
|
+
const start = Number(match[1]);
|
|
651
|
+
const end = match[2] === undefined ? undefined : Number(match[2]);
|
|
652
|
+
if (!Number.isSafeInteger(start)
|
|
653
|
+
|| (end !== undefined && (!Number.isSafeInteger(end) || end < start))) {
|
|
654
|
+
throw new CloudHttpError(400, 'invalid_range', 'The artifact byte range is outside its supported bound.');
|
|
655
|
+
}
|
|
656
|
+
return Object.freeze({ start, ...(end === undefined ? {} : { end }) });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function artifactContentDisposition(filename, download = false) {
|
|
660
|
+
const fallback = filename.normalize('NFKD')
|
|
661
|
+
.replace(/\p{M}+/gu, '')
|
|
662
|
+
.replace(/[^A-Za-z0-9._-]+/gu, '_')
|
|
663
|
+
.replace(/^\.+/u, '')
|
|
664
|
+
.slice(0, 120) || 'artifact';
|
|
665
|
+
const encoded = encodeURIComponent(filename).replace(/['()*]/gu, (value) => (
|
|
666
|
+
`%${value.codePointAt(0).toString(16).toUpperCase()}`
|
|
667
|
+
));
|
|
668
|
+
return `${download ? 'attachment' : 'inline'}; filename="${fallback}"; filename*=UTF-8''${encoded}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function sessionCookie(value, maximumAge) {
|
|
672
|
+
return `${SESSION_COOKIE_NAME}=${value}; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=${maximumAge}`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function csrfCookie(value, maximumAge) {
|
|
676
|
+
return `${CSRF_COOKIE_NAME}=${value}; Path=/; Secure; SameSite=Strict; Max-Age=${maximumAge}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function clearedCookies() {
|
|
680
|
+
const suffix = 'Path=/; Secure; SameSite=Strict; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
681
|
+
return [
|
|
682
|
+
`${SESSION_COOKIE_NAME}=; ${suffix}; HttpOnly`,
|
|
683
|
+
`${CSRF_COOKIE_NAME}=; ${suffix}`
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function browserSessionId(sessionToken) {
|
|
688
|
+
return sha256(sessionToken);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function requestAbortController(req, res) {
|
|
692
|
+
const controller = new AbortController();
|
|
693
|
+
const abort = () => {
|
|
694
|
+
if (!controller.signal.aborted) controller.abort(new DOMException('request disconnected', 'AbortError'));
|
|
695
|
+
};
|
|
696
|
+
const close = () => { if (!res.writableEnded) abort(); };
|
|
697
|
+
req.once('aborted', abort);
|
|
698
|
+
res.once('close', close);
|
|
699
|
+
return {
|
|
700
|
+
controller,
|
|
701
|
+
cleanup() {
|
|
702
|
+
req.off('aborted', abort);
|
|
703
|
+
res.off('close', close);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function deadlineSignal(parentSignal, milliseconds, message) {
|
|
709
|
+
const controller = new AbortController();
|
|
710
|
+
const abortFromParent = () => controller.abort(parentSignal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
711
|
+
if (parentSignal.aborted) abortFromParent();
|
|
712
|
+
else parentSignal.addEventListener('abort', abortFromParent, { once: true });
|
|
713
|
+
const timer = setTimeout(() => {
|
|
714
|
+
const error = new Error(message);
|
|
715
|
+
error.name = 'TimeoutError';
|
|
716
|
+
controller.abort(error);
|
|
717
|
+
}, milliseconds);
|
|
718
|
+
timer.unref?.();
|
|
719
|
+
return {
|
|
720
|
+
signal: controller.signal,
|
|
721
|
+
cleanup() {
|
|
722
|
+
clearTimeout(timer);
|
|
723
|
+
parentSignal.removeEventListener('abort', abortFromParent);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function withTimeout(callback, { signal, milliseconds, timeoutMessage = 'dependency timed out' }) {
|
|
729
|
+
const controller = new AbortController();
|
|
730
|
+
const abortFromCaller = () => controller.abort(signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
731
|
+
if (signal?.aborted) abortFromCaller();
|
|
732
|
+
else signal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
733
|
+
const timer = setTimeout(() => {
|
|
734
|
+
const error = new Error(timeoutMessage);
|
|
735
|
+
error.name = 'TimeoutError';
|
|
736
|
+
controller.abort(error);
|
|
737
|
+
}, milliseconds);
|
|
738
|
+
timer.unref?.();
|
|
739
|
+
try {
|
|
740
|
+
return await Promise.race([
|
|
741
|
+
Promise.resolve().then(() => callback(controller.signal)),
|
|
742
|
+
new Promise((_, reject) => {
|
|
743
|
+
const aborted = () => reject(controller.signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
744
|
+
if (controller.signal.aborted) aborted();
|
|
745
|
+
else controller.signal.addEventListener('abort', aborted, { once: true });
|
|
746
|
+
})
|
|
747
|
+
]);
|
|
748
|
+
} finally {
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function delay(milliseconds, signal) {
|
|
755
|
+
return new Promise((resolve, reject) => {
|
|
756
|
+
if (signal?.aborted) {
|
|
757
|
+
reject(signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const abort = () => {
|
|
761
|
+
clearTimeout(timer);
|
|
762
|
+
reject(signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
763
|
+
};
|
|
764
|
+
const timer = setTimeout(() => {
|
|
765
|
+
signal?.removeEventListener('abort', abort);
|
|
766
|
+
resolve();
|
|
767
|
+
}, milliseconds);
|
|
768
|
+
timer.unref?.();
|
|
769
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function nextWithAbort(iterator, signal) {
|
|
774
|
+
return new Promise((resolve, reject) => {
|
|
775
|
+
let settled = false;
|
|
776
|
+
const finish = (callback, value) => {
|
|
777
|
+
if (settled) return;
|
|
778
|
+
settled = true;
|
|
779
|
+
signal.removeEventListener('abort', onAbort);
|
|
780
|
+
callback(value);
|
|
781
|
+
};
|
|
782
|
+
const onAbort = () => finish(reject, signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
783
|
+
if (signal.aborted) {
|
|
784
|
+
onAbort();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
788
|
+
Promise.resolve().then(() => iterator.next()).then(
|
|
789
|
+
(value) => finish(resolve, value),
|
|
790
|
+
(error) => finish(reject, error)
|
|
791
|
+
);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function* abortableAsyncIterable(iterable, signal) {
|
|
796
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
797
|
+
try {
|
|
798
|
+
while (true) {
|
|
799
|
+
const item = await nextWithAbort(iterator, signal);
|
|
800
|
+
if (item.done) return;
|
|
801
|
+
yield item.value;
|
|
802
|
+
}
|
|
803
|
+
} finally {
|
|
804
|
+
if (signal.aborted && typeof iterator.return === 'function') {
|
|
805
|
+
try { void iterator.return(); } catch { /* The aborted producer is already detached. */ }
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function valueWithAbort(value, signal) {
|
|
811
|
+
return new Promise((resolve, reject) => {
|
|
812
|
+
let settled = false;
|
|
813
|
+
const finish = (callback, result) => {
|
|
814
|
+
if (settled) return;
|
|
815
|
+
settled = true;
|
|
816
|
+
signal.removeEventListener('abort', onAbort);
|
|
817
|
+
callback(result);
|
|
818
|
+
};
|
|
819
|
+
const onAbort = () => finish(reject, signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
820
|
+
if (signal.aborted) {
|
|
821
|
+
onAbort();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
825
|
+
Promise.resolve(value).then(
|
|
826
|
+
(result) => finish(resolve, result),
|
|
827
|
+
(error) => finish(reject, error)
|
|
828
|
+
);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function jsonSseEvent({ event, data, id }) {
|
|
833
|
+
return `${id === undefined ? '' : `id: ${id}\n`}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function writeSse(res, value, signal) {
|
|
837
|
+
if (signal.aborted || res.writableEnded || res.destroyed) throw signal.reason ?? new DOMException('aborted', 'AbortError');
|
|
838
|
+
if (res.write(value)) return;
|
|
839
|
+
await new Promise((resolve, reject) => {
|
|
840
|
+
let settled = false;
|
|
841
|
+
const finish = (callback, value) => {
|
|
842
|
+
if (settled) return;
|
|
843
|
+
settled = true;
|
|
844
|
+
res.off('drain', onDrain);
|
|
845
|
+
res.off('close', onClose);
|
|
846
|
+
signal.removeEventListener('abort', onAbort);
|
|
847
|
+
callback(value);
|
|
848
|
+
};
|
|
849
|
+
const onDrain = () => finish(resolve);
|
|
850
|
+
const onClose = () => finish(reject, new DOMException('response closed', 'AbortError'));
|
|
851
|
+
const onAbort = () => finish(reject, signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
852
|
+
res.once('drain', onDrain);
|
|
853
|
+
res.once('close', onClose);
|
|
854
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function waitForResponseDrain(res, signal) {
|
|
859
|
+
await new Promise((resolve, reject) => {
|
|
860
|
+
let settled = false;
|
|
861
|
+
const finish = (callback, value) => {
|
|
862
|
+
if (settled) return;
|
|
863
|
+
settled = true;
|
|
864
|
+
res.off('drain', onDrain);
|
|
865
|
+
res.off('close', onClose);
|
|
866
|
+
signal.removeEventListener('abort', onAbort);
|
|
867
|
+
callback(value);
|
|
868
|
+
};
|
|
869
|
+
const onDrain = () => finish(resolve);
|
|
870
|
+
const onClose = () => finish(reject, new DOMException('response closed', 'AbortError'));
|
|
871
|
+
const onAbort = () => finish(reject, signal.reason ?? new DOMException('aborted', 'AbortError'));
|
|
872
|
+
if (signal.aborted || res.destroyed || res.writableEnded) {
|
|
873
|
+
onAbort();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
res.once('drain', onDrain);
|
|
877
|
+
res.once('close', onClose);
|
|
878
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function streamArtifactBytes(res, body, expectedBytes, signal) {
|
|
883
|
+
if (!body || typeof body.getReader !== 'function'
|
|
884
|
+
|| !Number.isSafeInteger(expectedBytes) || expectedBytes < 1
|
|
885
|
+
|| expectedBytes > AGINTI_MAX_FILE_ARTIFACT_BYTES) {
|
|
886
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid artifact stream.');
|
|
887
|
+
}
|
|
888
|
+
const reader = body.getReader();
|
|
889
|
+
let received = 0;
|
|
890
|
+
let ended = false;
|
|
891
|
+
try {
|
|
892
|
+
for (;;) {
|
|
893
|
+
const item = await valueWithAbort(reader.read(), signal);
|
|
894
|
+
if (item.done) {
|
|
895
|
+
ended = true;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
if (!(item.value instanceof Uint8Array) || item.value.byteLength < 1) {
|
|
899
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid artifact stream.');
|
|
900
|
+
}
|
|
901
|
+
received += item.value.byteLength;
|
|
902
|
+
if (received > expectedBytes || received > AGINTI_MAX_FILE_ARTIFACT_BYTES) {
|
|
903
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned too many artifact bytes.');
|
|
904
|
+
}
|
|
905
|
+
if (!res.write(Buffer.from(item.value))) await waitForResponseDrain(res, signal);
|
|
906
|
+
}
|
|
907
|
+
if (received !== expectedBytes) {
|
|
908
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an incomplete artifact stream.');
|
|
909
|
+
}
|
|
910
|
+
res.end();
|
|
911
|
+
} finally {
|
|
912
|
+
if (!ended) {
|
|
913
|
+
try { void Promise.resolve(reader.cancel()).catch(() => {}); }
|
|
914
|
+
catch { /* The interrupted upstream reader is already unusable. */ }
|
|
915
|
+
}
|
|
916
|
+
try { reader.releaseLock?.(); }
|
|
917
|
+
catch { /* A hostile pending read must not retain the public stream admission slot. */ }
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function validateArtifactContentResult(value, request, metadataOnly) {
|
|
922
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
923
|
+
|| ![200, 206].includes(value.status)) {
|
|
924
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned invalid artifact metadata.');
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
validateFileSpec({
|
|
928
|
+
schemaVersion: '1',
|
|
929
|
+
filename: value.filename,
|
|
930
|
+
mime: value.mime,
|
|
931
|
+
bytes: value.totalBytes,
|
|
932
|
+
sha256: value.sha256
|
|
933
|
+
});
|
|
934
|
+
} catch (error) {
|
|
935
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned invalid artifact metadata.', { cause: error });
|
|
936
|
+
}
|
|
937
|
+
if (!Number.isSafeInteger(value.selectedBytes) || value.selectedBytes < 1
|
|
938
|
+
|| value.selectedBytes > value.totalBytes
|
|
939
|
+
|| (metadataOnly ? value.body !== null : (!value.body || typeof value.body.getReader !== 'function'))) {
|
|
940
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned invalid artifact byte metadata.');
|
|
941
|
+
}
|
|
942
|
+
let contentRange;
|
|
943
|
+
if (request.range === undefined) {
|
|
944
|
+
if (value.status !== 200 || value.range !== null || value.selectedBytes !== value.totalBytes) {
|
|
945
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an unsolicited artifact range.');
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
const range = value.range;
|
|
949
|
+
if (value.status !== 206 || !range || !Number.isSafeInteger(range.start)
|
|
950
|
+
|| !Number.isSafeInteger(range.end) || !Number.isSafeInteger(range.total)
|
|
951
|
+
|| range.start !== request.range.start || range.total !== value.totalBytes
|
|
952
|
+
|| range.end < range.start || range.end >= range.total
|
|
953
|
+
|| value.selectedBytes !== range.end - range.start + 1
|
|
954
|
+
|| (request.range.end !== undefined && range.end > request.range.end)) {
|
|
955
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid artifact range.');
|
|
956
|
+
}
|
|
957
|
+
contentRange = `bytes ${range.start}-${range.end}/${range.total}`;
|
|
958
|
+
}
|
|
959
|
+
return Object.freeze({
|
|
960
|
+
status: value.status,
|
|
961
|
+
filename: value.filename,
|
|
962
|
+
mime: value.mime,
|
|
963
|
+
totalBytes: value.totalBytes,
|
|
964
|
+
selectedBytes: value.selectedBytes,
|
|
965
|
+
sha256: value.sha256,
|
|
966
|
+
body: value.body,
|
|
967
|
+
contentRange
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function safeConnectorFailure(error, timedOut) {
|
|
972
|
+
if (timedOut || error?.name === 'TimeoutError') return 'timeout';
|
|
973
|
+
return SAFE_FAILURE_CODES.has(error?.failureCode) ? error.failureCode : 'provider_unavailable';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function splitUtf8(value, maximumBytes) {
|
|
977
|
+
if (typeof value !== 'string' || value.length < 1 || value.includes('\u0000')) {
|
|
978
|
+
throw new Error('connector returned an invalid delta');
|
|
979
|
+
}
|
|
980
|
+
const chunks = [];
|
|
981
|
+
let chunk = '';
|
|
982
|
+
let bytes = 0;
|
|
983
|
+
for (const scalar of value) {
|
|
984
|
+
const scalarBytes = Buffer.byteLength(scalar, 'utf8');
|
|
985
|
+
if (bytes + scalarBytes > maximumBytes && chunk) {
|
|
986
|
+
chunks.push(chunk);
|
|
987
|
+
chunk = '';
|
|
988
|
+
bytes = 0;
|
|
989
|
+
}
|
|
990
|
+
if (scalarBytes > maximumBytes) throw new Error('connector delta scalar exceeds its byte limit');
|
|
991
|
+
chunk += scalar;
|
|
992
|
+
bytes += scalarBytes;
|
|
993
|
+
}
|
|
994
|
+
if (chunk) chunks.push(chunk);
|
|
995
|
+
return chunks;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function persistedDeltas(store, accountId, threadId, generationId) {
|
|
999
|
+
const result = [];
|
|
1000
|
+
let afterSequence = 0;
|
|
1001
|
+
let generation = null;
|
|
1002
|
+
while (result.length < 8_192) {
|
|
1003
|
+
const page = store.replayGeneration({ accountId, threadId, generationId, afterSequence, limit: 200 });
|
|
1004
|
+
generation = page.generation;
|
|
1005
|
+
result.push(...page.deltas);
|
|
1006
|
+
if (page.deltas.length === 0 || !page.hasMore) break;
|
|
1007
|
+
afterSequence = page.deltas[page.deltas.length - 1].sequence;
|
|
1008
|
+
}
|
|
1009
|
+
return { generation, deltas: result };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export function createCloudRequestHandler({
|
|
1013
|
+
releaseId,
|
|
1014
|
+
assetMap,
|
|
1015
|
+
publicOrigin,
|
|
1016
|
+
account,
|
|
1017
|
+
passwordVerifier,
|
|
1018
|
+
sessionStore,
|
|
1019
|
+
controlStore,
|
|
1020
|
+
directChatStore,
|
|
1021
|
+
directChatContext,
|
|
1022
|
+
directChatConnector,
|
|
1023
|
+
visionEnabled = false,
|
|
1024
|
+
visionModelAlias = VISION_MODEL_ALIAS,
|
|
1025
|
+
agintiAdapter,
|
|
1026
|
+
clock = () => new Date(),
|
|
1027
|
+
requestOutcomeObserver,
|
|
1028
|
+
limits: limitOverrides = {}
|
|
1029
|
+
} = {}) {
|
|
1030
|
+
const assets = snapshotAndValidateAssetMap(assetMap, releaseId);
|
|
1031
|
+
const origin = validatePublicOrigin(publicOrigin);
|
|
1032
|
+
const publicHost = new URL(origin).host;
|
|
1033
|
+
const configuredAccount = validateAccountConfig(account);
|
|
1034
|
+
const verifier = validatePasswordVerifier(passwordVerifier);
|
|
1035
|
+
const sessions = requireMethods(sessionStore ?? controlStore, 'sessionStore', [
|
|
1036
|
+
'createBrowserSession', 'authenticateBrowserSession', 'authenticateBrowserMutation', 'revokeBrowserSession'
|
|
1037
|
+
]);
|
|
1038
|
+
const controls = requireMethods(controlStore, 'controlStore', ['getAccount']);
|
|
1039
|
+
const chat = requireMethods(directChatStore, 'directChatStore', [
|
|
1040
|
+
'createThread', 'getThread', 'listThreads', 'deleteThread', 'startTurn',
|
|
1041
|
+
'appendGenerationDelta', 'finalizeGeneration', 'cancelGeneration', 'failGeneration',
|
|
1042
|
+
'getGeneration', 'replayGeneration', 'listMessages',
|
|
1043
|
+
'getVisionAttachment', 'getLatestVisionAttachment', 'getLatestVisionAttachments',
|
|
1044
|
+
'claimGenerationLease', 'markGenerationDispatchStarted', 'renewGenerationLease',
|
|
1045
|
+
'releaseGenerationLease', 'getGenerationLease'
|
|
1046
|
+
]);
|
|
1047
|
+
if (directChatConnector !== undefined && directChatConnector !== null && typeof directChatConnector.generate !== 'function') {
|
|
1048
|
+
throw new TypeError('directChatConnector must provide generate()');
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof visionEnabled !== 'boolean' || typeof visionModelAlias !== 'string'
|
|
1051
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,63}$/u.test(visionModelAlias)) {
|
|
1052
|
+
throw new TypeError('Direct Chat vision configuration is invalid');
|
|
1053
|
+
}
|
|
1054
|
+
const contextCoordinator = directChatContext === undefined || directChatContext === null
|
|
1055
|
+
? null
|
|
1056
|
+
: requireMethods(directChatContext, 'directChatContext', ['prepareForTurn', 'assemble']);
|
|
1057
|
+
if (directChatConnector && contextCoordinator === null) {
|
|
1058
|
+
throw new TypeError('directChatContext is required when Direct LocalLLM chat is enabled');
|
|
1059
|
+
}
|
|
1060
|
+
if (agintiAdapter !== undefined && agintiAdapter !== null
|
|
1061
|
+
&& (typeof agintiAdapter.rpc !== 'function' || typeof agintiAdapter.capabilities !== 'function')) {
|
|
1062
|
+
throw new TypeError('agintiAdapter must provide the frozen rpc() and capabilities() interface');
|
|
1063
|
+
}
|
|
1064
|
+
if (typeof clock !== 'function') throw new TypeError('clock must be a function');
|
|
1065
|
+
if (requestOutcomeObserver !== undefined && typeof requestOutcomeObserver !== 'function') {
|
|
1066
|
+
throw new TypeError('requestOutcomeObserver must be a function');
|
|
1067
|
+
}
|
|
1068
|
+
const limits = exactLimitOverrides(limitOverrides);
|
|
1069
|
+
const bodyGate = new ConcurrencyGate(limits.concurrentBodies, limits.concurrentBodiesPerSource);
|
|
1070
|
+
const loginAdmission = new LoginAdmission({
|
|
1071
|
+
clock,
|
|
1072
|
+
maximum: limits.concurrentLogins,
|
|
1073
|
+
perSource: limits.concurrentLoginsPerSource,
|
|
1074
|
+
maximumAttempts: limits.loginAttemptsPerMinute
|
|
1075
|
+
});
|
|
1076
|
+
const streamGate = new ConcurrencyGate(limits.concurrentStreams, limits.concurrentStreamsPerSession);
|
|
1077
|
+
const jobs = new Map();
|
|
1078
|
+
const ephemeralSessions = new Map();
|
|
1079
|
+
let ephemeralExpiryTimer = null;
|
|
1080
|
+
let stopping = false;
|
|
1081
|
+
let backgroundDrain = null;
|
|
1082
|
+
|
|
1083
|
+
async function authenticate(req, { csrf = true } = {}) {
|
|
1084
|
+
const sessionToken = parseCookie(req, SESSION_COOKIE_NAME);
|
|
1085
|
+
if (!sessionToken) return null;
|
|
1086
|
+
const ephemeral = ephemeralSessions.get(sessionToken);
|
|
1087
|
+
if (ephemeral) {
|
|
1088
|
+
if (ephemeral.expiresAt <= epochMilliseconds(clock)) {
|
|
1089
|
+
ephemeralSessions.delete(sessionToken);
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
if (csrf && !safeEqual(csrfFromRequest(req), ephemeral.csrfToken)) {
|
|
1093
|
+
throw new CloudHttpError(403, 'csrf_rejected', 'The CSRF token is missing or invalid.');
|
|
1094
|
+
}
|
|
1095
|
+
return Object.freeze({
|
|
1096
|
+
token: sessionToken,
|
|
1097
|
+
view: ephemeral.view,
|
|
1098
|
+
browserSession: browserSessionId(sessionToken),
|
|
1099
|
+
ephemeral: true
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const session = await sessions.authenticateBrowserSession({ sessionToken });
|
|
1103
|
+
if (!session || session.accountId !== configuredAccount.principalId) return null;
|
|
1104
|
+
if (csrf) {
|
|
1105
|
+
const csrfToken = csrfFromRequest(req);
|
|
1106
|
+
const mutated = await sessions.authenticateBrowserMutation({ sessionToken, csrfToken });
|
|
1107
|
+
if (!mutated || mutated.accountId !== configuredAccount.principalId) {
|
|
1108
|
+
throw new CloudHttpError(403, 'csrf_rejected', 'The CSRF token is missing or invalid.');
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return Object.freeze({
|
|
1112
|
+
token: sessionToken,
|
|
1113
|
+
view: session,
|
|
1114
|
+
browserSession: browserSessionId(sessionToken),
|
|
1115
|
+
ephemeral: false
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function requireAuthentication(value) {
|
|
1120
|
+
if (!value) throw new CloudHttpError(401, 'authentication_required', 'A valid browser session is required.');
|
|
1121
|
+
return value;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function handleLogin(req, res, body, requestSignal, clientAddress) {
|
|
1125
|
+
const admission = loginAdmission.enter(`${clientAddress}\u0000${configuredAccount.principalId}`);
|
|
1126
|
+
if (admission.error) throw admission.error;
|
|
1127
|
+
let password = body.password;
|
|
1128
|
+
try {
|
|
1129
|
+
const passwordAccepted = await withTimeout(
|
|
1130
|
+
(signal) => verifier.verify(password, { signal }),
|
|
1131
|
+
{ signal: requestSignal, milliseconds: limits.dependencyTimeoutMs, timeoutMessage: 'password verification timed out' }
|
|
1132
|
+
);
|
|
1133
|
+
const accepted = passwordAccepted === true && safeEqual(body.username, configuredAccount.username);
|
|
1134
|
+
password = undefined;
|
|
1135
|
+
if (!accepted) throw new CloudHttpError(401, 'invalid_credentials', 'The username or password was not accepted.');
|
|
1136
|
+
const boundAccount = await controls.getAccount(configuredAccount.principalId);
|
|
1137
|
+
if (!boundAccount || boundAccount.id !== configuredAccount.principalId) {
|
|
1138
|
+
throw new CloudHttpError(503, 'account_unavailable', 'The configured account is unavailable.');
|
|
1139
|
+
}
|
|
1140
|
+
const sessionToken = opaqueToken();
|
|
1141
|
+
const csrfToken = opaqueToken();
|
|
1142
|
+
const ephemeral = body.sessionMode === 'ephemeral-memory';
|
|
1143
|
+
const maximumAge = ephemeral
|
|
1144
|
+
? EPHEMERAL_SESSION_MAX_AGE_SECONDS
|
|
1145
|
+
: (body.remember ? REMEMBERED_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS);
|
|
1146
|
+
const expiresAt = new Date(epochMilliseconds(clock) + maximumAge * 1_000).toISOString();
|
|
1147
|
+
if (ephemeral) {
|
|
1148
|
+
ephemeralSessions.clear();
|
|
1149
|
+
if (ephemeralExpiryTimer) clearTimeout(ephemeralExpiryTimer);
|
|
1150
|
+
ephemeralSessions.set(sessionToken, Object.freeze({
|
|
1151
|
+
csrfToken,
|
|
1152
|
+
expiresAt: Date.parse(expiresAt),
|
|
1153
|
+
view: Object.freeze({ accountId: configuredAccount.principalId })
|
|
1154
|
+
}));
|
|
1155
|
+
ephemeralExpiryTimer = setTimeout(() => {
|
|
1156
|
+
ephemeralSessions.delete(sessionToken);
|
|
1157
|
+
ephemeralExpiryTimer = null;
|
|
1158
|
+
}, maximumAge * 1_000);
|
|
1159
|
+
ephemeralExpiryTimer.unref?.();
|
|
1160
|
+
} else {
|
|
1161
|
+
await sessions.createBrowserSession({
|
|
1162
|
+
accountId: configuredAccount.principalId,
|
|
1163
|
+
sessionToken,
|
|
1164
|
+
csrfToken,
|
|
1165
|
+
expiresAt,
|
|
1166
|
+
idempotencyKey: derivedIdempotencyKey('login', sessionToken)
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
sendJson(req, res, 200, {
|
|
1170
|
+
authenticated: true,
|
|
1171
|
+
username: configuredAccount.username,
|
|
1172
|
+
csrfToken,
|
|
1173
|
+
...(ephemeral ? { sessionDisposition: 'ephemeral-memory' } : {})
|
|
1174
|
+
}, {
|
|
1175
|
+
'set-cookie': [sessionCookie(sessionToken, maximumAge), csrfCookie(csrfToken, maximumAge)]
|
|
1176
|
+
});
|
|
1177
|
+
} finally {
|
|
1178
|
+
password = undefined;
|
|
1179
|
+
admission.release?.();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function handleSession(req, res) {
|
|
1184
|
+
const withoutCsrf = await authenticate(req, { csrf: false });
|
|
1185
|
+
if (!withoutCsrf) {
|
|
1186
|
+
sendJson(req, res, 200, { authenticated: false }, { 'set-cookie': clearedCookies() });
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const authenticated = requireAuthentication(await authenticate(req));
|
|
1190
|
+
sendJson(req, res, 200, {
|
|
1191
|
+
authenticated: true,
|
|
1192
|
+
username: configuredAccount.username,
|
|
1193
|
+
csrfToken: csrfFromRequest(req)
|
|
1194
|
+
});
|
|
1195
|
+
return authenticated;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function handleLogout(req, res) {
|
|
1199
|
+
const session = requireAuthentication(await authenticate(req));
|
|
1200
|
+
if (session.ephemeral) {
|
|
1201
|
+
ephemeralSessions.delete(session.token);
|
|
1202
|
+
if (ephemeralSessions.size === 0 && ephemeralExpiryTimer) {
|
|
1203
|
+
clearTimeout(ephemeralExpiryTimer);
|
|
1204
|
+
ephemeralExpiryTimer = null;
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
const idempotencyKey = derivedIdempotencyKey('logout', session.token);
|
|
1208
|
+
await sessions.revokeBrowserSession({
|
|
1209
|
+
accountId: configuredAccount.principalId,
|
|
1210
|
+
sessionToken: session.token,
|
|
1211
|
+
idempotencyKey
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
sendJson(req, res, 200, {
|
|
1215
|
+
signedOut: true,
|
|
1216
|
+
// The frozen transport has no browser-session-scoped cancellation RPC.
|
|
1217
|
+
// Never imply that revoking a cloud cookie cancelled AgInTi-owned work.
|
|
1218
|
+
agentCancellationPending: agintiAdapter !== undefined && agintiAdapter !== null
|
|
1219
|
+
}, { 'set-cookie': clearedCookies() });
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function closeGenerationJobs() {
|
|
1223
|
+
stopping = true;
|
|
1224
|
+
ephemeralSessions.clear();
|
|
1225
|
+
if (ephemeralExpiryTimer) {
|
|
1226
|
+
clearTimeout(ephemeralExpiryTimer);
|
|
1227
|
+
ephemeralExpiryTimer = null;
|
|
1228
|
+
}
|
|
1229
|
+
if (backgroundDrain) return backgroundDrain;
|
|
1230
|
+
const pending = [...jobs.values()];
|
|
1231
|
+
for (const job of pending) {
|
|
1232
|
+
if (!job.controller.signal.aborted) {
|
|
1233
|
+
const error = new Error('cloud server is stopping');
|
|
1234
|
+
error.code = 'server_stopping';
|
|
1235
|
+
job.controller.abort(error);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
backgroundDrain = Promise.allSettled(pending.map((job) => job.promise)).then(() => undefined);
|
|
1239
|
+
return backgroundDrain;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function scheduleGeneration(accountId, threadId, generationId, startKey, preparation) {
|
|
1243
|
+
const jobKey = `${threadId}:${generationId}`;
|
|
1244
|
+
if (jobs.has(jobKey)) return true;
|
|
1245
|
+
if (stopping || !directChatConnector || jobs.size >= limits.directChatJobs) return false;
|
|
1246
|
+
const scheduled = chat.getGeneration({ accountId, threadId, generationId });
|
|
1247
|
+
const jobTimeoutMs = scheduled?.modelAlias === visionModelAlias
|
|
1248
|
+
? limits.visionJobTimeoutMs
|
|
1249
|
+
: limits.jobTimeoutMs;
|
|
1250
|
+
const controller = new AbortController();
|
|
1251
|
+
const job = { controller, timedOut: false, promise: null, lease: null };
|
|
1252
|
+
jobs.set(jobKey, job);
|
|
1253
|
+
const timer = setTimeout(() => {
|
|
1254
|
+
job.timedOut = true;
|
|
1255
|
+
const error = new Error('direct chat generation timed out');
|
|
1256
|
+
error.name = 'TimeoutError';
|
|
1257
|
+
controller.abort(error);
|
|
1258
|
+
}, jobTimeoutMs);
|
|
1259
|
+
timer.unref?.();
|
|
1260
|
+
job.promise = (async () => {
|
|
1261
|
+
try {
|
|
1262
|
+
const thread = chat.getThread(accountId, threadId);
|
|
1263
|
+
if (!thread) throw new Error('direct chat thread disappeared');
|
|
1264
|
+
const persisted = await persistedDeltas(chat, accountId, threadId, generationId);
|
|
1265
|
+
if (!persisted.generation || persisted.generation.status !== 'in_progress') return;
|
|
1266
|
+
const ownerToken = opaqueToken();
|
|
1267
|
+
let lease;
|
|
1268
|
+
try {
|
|
1269
|
+
lease = chat.claimGenerationLease({
|
|
1270
|
+
accountId,
|
|
1271
|
+
threadId,
|
|
1272
|
+
generationId,
|
|
1273
|
+
ownerToken,
|
|
1274
|
+
ttlMs: 30_000
|
|
1275
|
+
});
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
const observed = chat.getGenerationLease({ accountId, threadId, generationId });
|
|
1278
|
+
if (observed?.phase === 'interrupted') {
|
|
1279
|
+
try {
|
|
1280
|
+
chat.failGeneration({
|
|
1281
|
+
accountId,
|
|
1282
|
+
threadId,
|
|
1283
|
+
generationId,
|
|
1284
|
+
failureCode: 'provider_unavailable',
|
|
1285
|
+
idempotencyKey: derivedIdempotencyKey('chat-interrupted', startKey, generationId)
|
|
1286
|
+
});
|
|
1287
|
+
} catch {
|
|
1288
|
+
// Another process may have resolved the terminal state.
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
job.lease = Object.freeze({ ownerToken, fence: lease.fence });
|
|
1294
|
+
const renewTimer = setInterval(() => {
|
|
1295
|
+
try {
|
|
1296
|
+
chat.renewGenerationLease({
|
|
1297
|
+
accountId,
|
|
1298
|
+
threadId,
|
|
1299
|
+
generationId,
|
|
1300
|
+
ownerToken,
|
|
1301
|
+
fence: lease.fence,
|
|
1302
|
+
ttlMs: 30_000
|
|
1303
|
+
});
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (!controller.signal.aborted) controller.abort(error);
|
|
1306
|
+
}
|
|
1307
|
+
}, 10_000);
|
|
1308
|
+
renewTimer.unref?.();
|
|
1309
|
+
job.renewTimer = renewTimer;
|
|
1310
|
+
if (persisted.deltas.length !== 0) {
|
|
1311
|
+
const error = new Error('a partially delivered stateless generation cannot be redispatched');
|
|
1312
|
+
error.failureCode = 'provider_unavailable';
|
|
1313
|
+
throw error;
|
|
1314
|
+
}
|
|
1315
|
+
const context = await contextCoordinator.assemble({
|
|
1316
|
+
accountId,
|
|
1317
|
+
threadId,
|
|
1318
|
+
sourceRevision: persisted.generation.sourceRevision,
|
|
1319
|
+
sourceHash: persisted.generation.sourceHash,
|
|
1320
|
+
...(preparation === undefined ? {} : { preparation })
|
|
1321
|
+
});
|
|
1322
|
+
const visionAttachments = chat.getLatestVisionAttachments({
|
|
1323
|
+
accountId,
|
|
1324
|
+
threadId,
|
|
1325
|
+
sourceRevision: persisted.generation.sourceRevision
|
|
1326
|
+
});
|
|
1327
|
+
if ((persisted.generation.modelAlias === visionModelAlias) !== (visionAttachments.length > 0)) {
|
|
1328
|
+
const error = new Error('persisted vision inference authority is inconsistent');
|
|
1329
|
+
error.failureCode = 'content_rejected';
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1332
|
+
while (true) {
|
|
1333
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1334
|
+
const marker = chat.markGenerationDispatchStarted({
|
|
1335
|
+
accountId,
|
|
1336
|
+
threadId,
|
|
1337
|
+
generationId,
|
|
1338
|
+
ownerToken,
|
|
1339
|
+
fence: lease.fence
|
|
1340
|
+
});
|
|
1341
|
+
if (marker.dispatchAuthorized === true) break;
|
|
1342
|
+
if (marker.dispatchState !== 'global_busy') {
|
|
1343
|
+
const error = new Error('inference dispatch is already ambiguous');
|
|
1344
|
+
error.failureCode = 'provider_unavailable';
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
await delay(GLOBAL_DISPATCH_RETRY_MS, controller.signal);
|
|
1348
|
+
}
|
|
1349
|
+
const output = await valueWithAbort(directChatConnector.generate({
|
|
1350
|
+
modelAlias: persisted.generation.modelAlias,
|
|
1351
|
+
context: context.payload,
|
|
1352
|
+
...(visionAttachments.length === 0 ? {} : (visionAttachments.length === 1 ? {
|
|
1353
|
+
visionAttachment: Object.freeze({
|
|
1354
|
+
attachmentId: visionAttachments[0].attachmentId,
|
|
1355
|
+
messageId: visionAttachments[0].messageId,
|
|
1356
|
+
mediaType: visionAttachments[0].mediaType,
|
|
1357
|
+
byteLength: visionAttachments[0].byteLength,
|
|
1358
|
+
width: visionAttachments[0].width,
|
|
1359
|
+
height: visionAttachments[0].height,
|
|
1360
|
+
contentSha256: visionAttachments[0].contentSha256,
|
|
1361
|
+
content: visionAttachments[0].content
|
|
1362
|
+
})
|
|
1363
|
+
} : {
|
|
1364
|
+
visionAttachments: Object.freeze(visionAttachments.map((visionAttachment) => Object.freeze({
|
|
1365
|
+
attachmentId: visionAttachment.attachmentId,
|
|
1366
|
+
messageId: visionAttachment.messageId,
|
|
1367
|
+
mediaType: visionAttachment.mediaType,
|
|
1368
|
+
byteLength: visionAttachment.byteLength,
|
|
1369
|
+
width: visionAttachment.width,
|
|
1370
|
+
height: visionAttachment.height,
|
|
1371
|
+
contentSha256: visionAttachment.contentSha256,
|
|
1372
|
+
content: visionAttachment.content
|
|
1373
|
+
})))
|
|
1374
|
+
})),
|
|
1375
|
+
replay: Object.freeze({
|
|
1376
|
+
deltaCount: persisted.generation.deltaCount,
|
|
1377
|
+
lastDeltaHash: persisted.generation.lastDeltaHash
|
|
1378
|
+
}),
|
|
1379
|
+
signal: controller.signal
|
|
1380
|
+
}), controller.signal);
|
|
1381
|
+
if (!output || typeof output[Symbol.asyncIterator] !== 'function') {
|
|
1382
|
+
throw new Error('directChatConnector.generate() must return an async iterable');
|
|
1383
|
+
}
|
|
1384
|
+
let sequence = persisted.generation.deltaCount;
|
|
1385
|
+
let hash = persisted.generation.lastDeltaHash;
|
|
1386
|
+
let outputBytes = persisted.generation.deltaBytes;
|
|
1387
|
+
for await (const rawDelta of abortableAsyncIterable(output, controller.signal)) {
|
|
1388
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1389
|
+
for (const delta of splitUtf8(rawDelta, limits.connectorDeltaBytes)) {
|
|
1390
|
+
outputBytes += Buffer.byteLength(delta, 'utf8');
|
|
1391
|
+
if (outputBytes > limits.connectorOutputBytes) {
|
|
1392
|
+
const error = new Error('connector output limit exceeded');
|
|
1393
|
+
error.failureCode = 'response_limit';
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
const appended = chat.appendGenerationDelta({
|
|
1397
|
+
accountId,
|
|
1398
|
+
threadId,
|
|
1399
|
+
generationId,
|
|
1400
|
+
expectedSequence: sequence,
|
|
1401
|
+
expectedHash: hash,
|
|
1402
|
+
content: delta,
|
|
1403
|
+
dispatchLease: job.lease
|
|
1404
|
+
});
|
|
1405
|
+
sequence = appended.sequence;
|
|
1406
|
+
hash = appended.deltaHash;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
1410
|
+
chat.finalizeGeneration({
|
|
1411
|
+
accountId,
|
|
1412
|
+
threadId,
|
|
1413
|
+
generationId,
|
|
1414
|
+
idempotencyKey: derivedIdempotencyKey('chat-finalize', startKey, generationId),
|
|
1415
|
+
dispatchLease: job.lease
|
|
1416
|
+
});
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
const current = chat.getGeneration({ accountId, threadId, generationId });
|
|
1419
|
+
if (!current || current.status !== 'in_progress') return;
|
|
1420
|
+
if (error?.code === 'server_stopping') return;
|
|
1421
|
+
try {
|
|
1422
|
+
chat.failGeneration({
|
|
1423
|
+
accountId,
|
|
1424
|
+
threadId,
|
|
1425
|
+
generationId,
|
|
1426
|
+
failureCode: safeConnectorFailure(error, job.timedOut),
|
|
1427
|
+
idempotencyKey: derivedIdempotencyKey('chat-fail', startKey, generationId),
|
|
1428
|
+
...(job.lease === null ? {} : { dispatchLease: job.lease })
|
|
1429
|
+
});
|
|
1430
|
+
} catch {
|
|
1431
|
+
// Another request may have cancelled or completed the same durable generation.
|
|
1432
|
+
}
|
|
1433
|
+
} finally {
|
|
1434
|
+
clearTimeout(timer);
|
|
1435
|
+
if (job.renewTimer) clearInterval(job.renewTimer);
|
|
1436
|
+
if (job.lease) {
|
|
1437
|
+
try {
|
|
1438
|
+
chat.releaseGenerationLease({
|
|
1439
|
+
accountId,
|
|
1440
|
+
threadId,
|
|
1441
|
+
generationId,
|
|
1442
|
+
ownerToken: job.lease.ownerToken,
|
|
1443
|
+
fence: job.lease.fence
|
|
1444
|
+
});
|
|
1445
|
+
} catch {
|
|
1446
|
+
// A terminal write, cancellation, expiry, or newer fence already owns the durable result.
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (jobs.get(jobKey) === job) jobs.delete(jobKey);
|
|
1450
|
+
}
|
|
1451
|
+
})();
|
|
1452
|
+
void job.promise.catch(() => {
|
|
1453
|
+
// The durable generation remains recoverable; shutdown waits for this task.
|
|
1454
|
+
});
|
|
1455
|
+
return true;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function schedulePersistedGeneration(accountId, threadId, generationId) {
|
|
1459
|
+
return scheduleGeneration(
|
|
1460
|
+
accountId,
|
|
1461
|
+
threadId,
|
|
1462
|
+
generationId,
|
|
1463
|
+
derivedIdempotencyKey('chat-recovery', accountId, threadId, generationId),
|
|
1464
|
+
undefined
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async function handleChat(req, res, route, body, session, requestSignal) {
|
|
1469
|
+
const accountId = configuredAccount.principalId;
|
|
1470
|
+
const input = validateChatRequest(route.pathname, body);
|
|
1471
|
+
const idempotencyKey = routeRequiresIdempotency(route.pathname)
|
|
1472
|
+
? requestIdempotency(req)
|
|
1473
|
+
: undefined;
|
|
1474
|
+
if (route.pathname === CLOUD_ROUTES.chatCapabilities) {
|
|
1475
|
+
sendJson(req, res, 200, {
|
|
1476
|
+
visionInput: visionEnabled,
|
|
1477
|
+
visionMediaTypes: visionEnabled ? ['image/jpeg', 'image/png'] : [],
|
|
1478
|
+
maximumImageBytes: visionEnabled ? 4 * 1024 * 1024 : 0
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (route.pathname === CLOUD_ROUTES.chatThreadsList) {
|
|
1483
|
+
const threads = chat.listThreads({ accountId, limit: input.limit })
|
|
1484
|
+
.map((thread) => publicThread(thread, accountId));
|
|
1485
|
+
sendJson(req, res, 200, { threads });
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (route.pathname === CLOUD_ROUTES.chatThreadsCreate) {
|
|
1489
|
+
const thread = chat.createThread({ accountId, ...input, idempotencyKey });
|
|
1490
|
+
sendJson(req, res, 201, { thread: publicThread(thread, accountId) });
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (route.pathname === CLOUD_ROUTES.chatThreadsGet) {
|
|
1494
|
+
const thread = chat.getThread(accountId, input.threadId);
|
|
1495
|
+
if (!thread) throw new CloudHttpError(404, 'not_found', 'The chat thread does not exist.');
|
|
1496
|
+
sendJson(req, res, 200, { thread: publicThread(thread, accountId) });
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (route.pathname === CLOUD_ROUTES.chatThreadsDelete) {
|
|
1500
|
+
const deleted = chat.deleteThread({ accountId, ...input, idempotencyKey });
|
|
1501
|
+
sendJson(req, res, 200, { deleted: deleted.deleted, threadId: deleted.threadId });
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (route.pathname === CLOUD_ROUTES.chatMessagesList) {
|
|
1505
|
+
const thread = chat.getThread(accountId, input.threadId);
|
|
1506
|
+
if (!thread) throw new CloudHttpError(404, 'not_found', 'The chat thread does not exist.');
|
|
1507
|
+
const { attachmentSchema, ...messageQuery } = input;
|
|
1508
|
+
const messages = chat.listMessages({ accountId, ...messageQuery });
|
|
1509
|
+
sendJson(req, res, 200, {
|
|
1510
|
+
messages: messages.map((message) => publicMessage(message, accountId, { attachmentSchema }))
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (route.pathname === CLOUD_ROUTES.chatAttachmentsGet) {
|
|
1515
|
+
const attachment = chat.getVisionAttachment({ accountId, ...input });
|
|
1516
|
+
if (!attachment) throw new CloudHttpError(404, 'not_found', 'The chat attachment does not exist.');
|
|
1517
|
+
sendBuffer(req, res, 200, attachment.content, dynamicHeaders({
|
|
1518
|
+
'content-type': attachment.mediaType,
|
|
1519
|
+
'content-disposition': 'inline',
|
|
1520
|
+
'x-content-type-options': 'nosniff'
|
|
1521
|
+
}, res[RESPONSE_RELEASE_ID]));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (route.pathname === CLOUD_ROUTES.chatRunsStart) {
|
|
1525
|
+
if (!directChatConnector) throw new CloudHttpError(503, 'localllm_unavailable', 'Direct LocalLLM chat is unavailable.');
|
|
1526
|
+
const existing = chat.getGeneration({ accountId, threadId: input.threadId, generationId: input.generationId });
|
|
1527
|
+
if (!existing && input.attachments !== undefined && !visionEnabled) {
|
|
1528
|
+
throw new CloudHttpError(503, 'vision_unavailable', 'Direct LocalLLM vision is not enabled.');
|
|
1529
|
+
}
|
|
1530
|
+
if (!existing && !visionEnabled && input.expectedRevision > 0
|
|
1531
|
+
&& chat.getLatestVisionAttachments({
|
|
1532
|
+
accountId,
|
|
1533
|
+
threadId: input.threadId,
|
|
1534
|
+
sourceRevision: input.expectedRevision
|
|
1535
|
+
}).length > 0) {
|
|
1536
|
+
throw new CloudHttpError(503, 'vision_unavailable', 'Direct LocalLLM vision is not enabled.');
|
|
1537
|
+
}
|
|
1538
|
+
if (!existing && jobs.size >= limits.directChatJobs) {
|
|
1539
|
+
throw new CloudHttpError(503, 'chat_busy', 'Direct chat is temporarily busy.', { retryAfter: 2 });
|
|
1540
|
+
}
|
|
1541
|
+
const preparation = existing ? undefined : await contextCoordinator.prepareForTurn({
|
|
1542
|
+
accountId,
|
|
1543
|
+
threadId: input.threadId,
|
|
1544
|
+
expectedRevision: input.expectedRevision,
|
|
1545
|
+
expectedHash: input.expectedHash,
|
|
1546
|
+
pendingUser: Object.freeze({ messageId: input.messageId, content: input.content }),
|
|
1547
|
+
signal: requestSignal
|
|
1548
|
+
});
|
|
1549
|
+
const turn = chat.startTurn({
|
|
1550
|
+
accountId,
|
|
1551
|
+
threadId: input.threadId,
|
|
1552
|
+
messageId: input.messageId,
|
|
1553
|
+
content: input.content,
|
|
1554
|
+
generationId: input.generationId,
|
|
1555
|
+
assistantMessageId: input.assistantMessageId,
|
|
1556
|
+
expectedRevision: input.expectedRevision,
|
|
1557
|
+
expectedHash: input.expectedHash,
|
|
1558
|
+
idempotencyKey,
|
|
1559
|
+
...(input.attachments === undefined ? {} : { attachments: input.attachments })
|
|
1560
|
+
});
|
|
1561
|
+
const { generation } = turn;
|
|
1562
|
+
if (generation.status === 'in_progress' && !scheduleGeneration(
|
|
1563
|
+
accountId,
|
|
1564
|
+
input.threadId,
|
|
1565
|
+
input.generationId,
|
|
1566
|
+
idempotencyKey,
|
|
1567
|
+
preparation
|
|
1568
|
+
)) {
|
|
1569
|
+
throw new CloudHttpError(503, 'chat_resume_pending', 'The persisted generation is waiting for LocalLLM capacity.', { retryAfter: 2 });
|
|
1570
|
+
}
|
|
1571
|
+
sendJson(req, res, 202, { generation: publicGeneration(generation, accountId) });
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (route.pathname === CLOUD_ROUTES.chatRunsStatus) {
|
|
1575
|
+
const generation = chat.getGeneration({ accountId, ...input });
|
|
1576
|
+
if (!generation) throw new CloudHttpError(404, 'not_found', 'The chat generation does not exist.');
|
|
1577
|
+
if (generation.status === 'in_progress') {
|
|
1578
|
+
schedulePersistedGeneration(accountId, input.threadId, input.generationId);
|
|
1579
|
+
}
|
|
1580
|
+
sendJson(req, res, 200, { generation: publicGeneration(generation, accountId) });
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (route.pathname === CLOUD_ROUTES.chatRunsCancel) {
|
|
1584
|
+
const generation = chat.cancelGeneration({ accountId, ...input, idempotencyKey });
|
|
1585
|
+
const job = jobs.get(`${input.threadId}:${input.generationId}`);
|
|
1586
|
+
if (job && !job.controller.signal.aborted) {
|
|
1587
|
+
const error = new Error('direct chat generation cancelled');
|
|
1588
|
+
error.code = 'generation_cancelled';
|
|
1589
|
+
job.controller.abort(error);
|
|
1590
|
+
}
|
|
1591
|
+
sendJson(req, res, 200, { generation: publicGeneration(generation, accountId) });
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (route.pathname === CLOUD_ROUTES.chatRunsEvents) {
|
|
1595
|
+
await streamChatEvents(req, res, input, accountId, requestSignal);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
throw new CloudHttpError(404, 'not_found', 'The requested route does not exist.');
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
async function streamChatEvents(req, res, input, accountId, signal) {
|
|
1602
|
+
const initial = chat.getGeneration({ accountId, threadId: input.threadId, generationId: input.generationId });
|
|
1603
|
+
if (!initial) throw new CloudHttpError(404, 'not_found', 'The chat generation does not exist.');
|
|
1604
|
+
if (initial.status === 'in_progress') {
|
|
1605
|
+
schedulePersistedGeneration(accountId, input.threadId, input.generationId);
|
|
1606
|
+
}
|
|
1607
|
+
writeHead(res, 200, dynamicHeaders({
|
|
1608
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1609
|
+
connection: 'keep-alive',
|
|
1610
|
+
'x-accel-buffering': 'no'
|
|
1611
|
+
}, res[RESPONSE_RELEASE_ID]));
|
|
1612
|
+
res.flushHeaders?.();
|
|
1613
|
+
let afterSequence = input.afterSequence;
|
|
1614
|
+
const streamDeadline = deadlineSignal(
|
|
1615
|
+
signal,
|
|
1616
|
+
limits.sseLifetimeMs,
|
|
1617
|
+
'Direct Chat event stream reached its reconnect boundary'
|
|
1618
|
+
);
|
|
1619
|
+
try {
|
|
1620
|
+
while (!streamDeadline.signal.aborted && !res.writableEnded) {
|
|
1621
|
+
const page = chat.replayGeneration({
|
|
1622
|
+
accountId,
|
|
1623
|
+
threadId: input.threadId,
|
|
1624
|
+
generationId: input.generationId,
|
|
1625
|
+
afterSequence,
|
|
1626
|
+
limit: 200
|
|
1627
|
+
});
|
|
1628
|
+
for (const delta of page.deltas) {
|
|
1629
|
+
await writeSse(res, jsonSseEvent({
|
|
1630
|
+
event: 'delta',
|
|
1631
|
+
id: delta.sequence,
|
|
1632
|
+
data: publicDelta(delta, accountId)
|
|
1633
|
+
}), streamDeadline.signal);
|
|
1634
|
+
afterSequence = delta.sequence;
|
|
1635
|
+
}
|
|
1636
|
+
if (page.generation.terminal) {
|
|
1637
|
+
await writeSse(res, jsonSseEvent({
|
|
1638
|
+
event: 'generation',
|
|
1639
|
+
data: publicGeneration(page.generation, accountId)
|
|
1640
|
+
}), streamDeadline.signal);
|
|
1641
|
+
res.end();
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (page.deltas.length === 0) {
|
|
1645
|
+
await delay(limits.ssePollMs, streamDeadline.signal);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
if (!streamDeadline.signal.aborted || signal.aborted) throw error;
|
|
1650
|
+
} finally {
|
|
1651
|
+
streamDeadline.cleanup();
|
|
1652
|
+
}
|
|
1653
|
+
if (!signal.aborted && !res.writableEnded && !res.destroyed) {
|
|
1654
|
+
// Give a healthy reader an authenticated cursor before the bounded
|
|
1655
|
+
// reconnect. A stalled reader has already applied backpressure, so keep
|
|
1656
|
+
// the hard socket cutoff instead of buffering more data for it.
|
|
1657
|
+
if (res.writableNeedDrain) {
|
|
1658
|
+
res.destroy();
|
|
1659
|
+
} else {
|
|
1660
|
+
res.end(jsonSseEvent({ event: 'reconnect', data: { afterSequence } }));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
async function handleAgent(req, res, route, body, session, requestSignal) {
|
|
1666
|
+
const nativePath = route.nativeAgentPath;
|
|
1667
|
+
const input = validateTransportAgentRequest(nativePath, body);
|
|
1668
|
+
const mutation = routeRequiresIdempotency(route.pathname, nativePath);
|
|
1669
|
+
const idempotencyKey = mutation ? requestIdempotency(req, true) : undefined;
|
|
1670
|
+
if (!agintiAdapter) {
|
|
1671
|
+
if (nativePath === AGINTI_RPC_PATHS.capabilities) {
|
|
1672
|
+
sendJson(req, res, 200, FAIL_CLOSED_AGENT_CAPABILITIES);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
throw new CloudHttpError(503, 'agent_unavailable', 'AgInTi Agent is unavailable.');
|
|
1676
|
+
}
|
|
1677
|
+
const contextFor = (signal) => Object.freeze({
|
|
1678
|
+
principalId: configuredAccount.principalId,
|
|
1679
|
+
browserSession: session.browserSession,
|
|
1680
|
+
...(mutation ? { idempotencyKey } : {}),
|
|
1681
|
+
signal
|
|
1682
|
+
});
|
|
1683
|
+
const readContextFor = (signal) => Object.freeze({
|
|
1684
|
+
principalId: configuredAccount.principalId,
|
|
1685
|
+
browserSession: session.browserSession,
|
|
1686
|
+
signal
|
|
1687
|
+
});
|
|
1688
|
+
const requestedSearch = input.input?.search;
|
|
1689
|
+
if (requestedSearch !== undefined) {
|
|
1690
|
+
const proof = await withTimeout(
|
|
1691
|
+
(signal) => agintiAdapter.capabilities(readContextFor(signal)),
|
|
1692
|
+
{ signal: requestSignal, milliseconds: limits.dependencyTimeoutMs, timeoutMessage: 'AgInTi capability check timed out' }
|
|
1693
|
+
);
|
|
1694
|
+
let capability;
|
|
1695
|
+
try { capability = validateAgentResponse(AGINTI_RPC_PATHS.capabilities, proof); }
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid capability response.', { cause: error });
|
|
1698
|
+
}
|
|
1699
|
+
if (capability.enabled !== true || capability.search?.enabled !== true
|
|
1700
|
+
|| !capability.search.modes.includes(requestedSearch.mode)
|
|
1701
|
+
|| requestedSearch.limit > capability.search.maximumSources) {
|
|
1702
|
+
throw new CloudHttpError(409, 'agent_search_unavailable', 'AgInTi Search is not enabled for this session.');
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (nativePath === AGINTI_RPC_PATHS.runsEvents) {
|
|
1706
|
+
const streamDeadline = deadlineSignal(requestSignal, limits.sseLifetimeMs, 'Agent event stream reached its reconnect boundary');
|
|
1707
|
+
try {
|
|
1708
|
+
const events = await valueWithAbort(
|
|
1709
|
+
agintiAdapter.rpc(nativePath, input, contextFor(streamDeadline.signal)),
|
|
1710
|
+
streamDeadline.signal
|
|
1711
|
+
);
|
|
1712
|
+
if (!events || typeof events[Symbol.asyncIterator] !== 'function') {
|
|
1713
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid event stream.');
|
|
1714
|
+
}
|
|
1715
|
+
writeHead(res, 200, dynamicHeaders({
|
|
1716
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1717
|
+
connection: 'keep-alive',
|
|
1718
|
+
'x-accel-buffering': 'no'
|
|
1719
|
+
}, res[RESPONSE_RELEASE_ID]));
|
|
1720
|
+
res.flushHeaders?.();
|
|
1721
|
+
for await (const rawEvent of abortableAsyncIterable(events, streamDeadline.signal)) {
|
|
1722
|
+
let event;
|
|
1723
|
+
try { event = validateEventEnvelope(rawEvent); }
|
|
1724
|
+
catch (error) { throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid event.', { cause: error }); }
|
|
1725
|
+
if (event.runId !== input.runId) {
|
|
1726
|
+
throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an event for a different run.');
|
|
1727
|
+
}
|
|
1728
|
+
await writeSse(res, jsonSseEvent({ event: event.type, id: event.id, data: event }), streamDeadline.signal);
|
|
1729
|
+
}
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
if (!streamDeadline.signal.aborted || requestSignal.aborted) throw error;
|
|
1732
|
+
} finally {
|
|
1733
|
+
streamDeadline.cleanup();
|
|
1734
|
+
}
|
|
1735
|
+
if (!requestSignal.aborted && !res.writableEnded) res.end();
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
const response = await withTimeout(
|
|
1740
|
+
(signal) => nativePath === AGINTI_RPC_PATHS.capabilities
|
|
1741
|
+
? agintiAdapter.capabilities(contextFor(signal))
|
|
1742
|
+
: agintiAdapter.rpc(nativePath, input, contextFor(signal)),
|
|
1743
|
+
{ signal: requestSignal, milliseconds: limits.dependencyTimeoutMs, timeoutMessage: 'AgInTi request timed out' }
|
|
1744
|
+
);
|
|
1745
|
+
let validated;
|
|
1746
|
+
try { validated = validateAgentResponse(nativePath, response); }
|
|
1747
|
+
catch (error) { throw new CloudHttpError(502, 'invalid_agent_response', 'AgInTi returned an invalid response.', { cause: error }); }
|
|
1748
|
+
requireAgentResponseCorrelation(nativePath, input, validated);
|
|
1749
|
+
sendJson(req, res, 200, validated);
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
if (nativePath === AGINTI_RPC_PATHS.capabilities) {
|
|
1752
|
+
sendJson(req, res, 200, FAIL_CLOSED_AGENT_CAPABILITIES);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
throw error;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
async function handleAgentArtifactContent(req, res, route, session, requestSignal) {
|
|
1760
|
+
if (!agintiAdapter || typeof agintiAdapter.artifactContent !== 'function') {
|
|
1761
|
+
throw new CloudHttpError(503, 'agent_unavailable', 'AgInTi Agent file artifacts are unavailable.');
|
|
1762
|
+
}
|
|
1763
|
+
const range = artifactByteRange(req);
|
|
1764
|
+
const metadataOnly = req.method === 'HEAD';
|
|
1765
|
+
const input = Object.freeze({
|
|
1766
|
+
artifactId: route.artifactId,
|
|
1767
|
+
...(metadataOnly ? { metadataOnly: true } : {}),
|
|
1768
|
+
...(range === undefined ? {} : { range })
|
|
1769
|
+
});
|
|
1770
|
+
const contextFor = (signal) => Object.freeze({
|
|
1771
|
+
principalId: configuredAccount.principalId,
|
|
1772
|
+
browserSession: session.browserSession,
|
|
1773
|
+
signal
|
|
1774
|
+
});
|
|
1775
|
+
const raw = await withTimeout(
|
|
1776
|
+
(signal) => agintiAdapter.artifactContent(input, contextFor(signal)),
|
|
1777
|
+
{ signal: requestSignal, milliseconds: limits.dependencyTimeoutMs, timeoutMessage: 'AgInTi artifact request timed out' }
|
|
1778
|
+
);
|
|
1779
|
+
if (raw?.status === 404) {
|
|
1780
|
+
sendJson(req, res, 404, { error: { code: 'not_found', message: 'The requested artifact does not exist.' } }, {
|
|
1781
|
+
'cache-control': 'no-store, private'
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (raw?.status === 410) {
|
|
1786
|
+
sendJson(req, res, 410, {
|
|
1787
|
+
error: { code: 'artifact_content_gone', message: 'This local artifact file has been removed.' }
|
|
1788
|
+
}, { 'cache-control': 'no-store, private' });
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (raw?.status === 416) {
|
|
1792
|
+
sendJson(req, res, 416, {
|
|
1793
|
+
error: { code: 'range_not_satisfiable', message: 'The requested artifact byte range is not available.' }
|
|
1794
|
+
}, { 'cache-control': 'no-store, private' });
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const result = validateArtifactContentResult(raw, input, metadataOnly);
|
|
1798
|
+
writeHead(res, result.status, dynamicHeaders({
|
|
1799
|
+
'accept-ranges': 'bytes',
|
|
1800
|
+
'cache-control': 'no-store, private',
|
|
1801
|
+
pragma: 'no-cache',
|
|
1802
|
+
expires: '0',
|
|
1803
|
+
'content-type': result.mime,
|
|
1804
|
+
'content-length': String(result.selectedBytes),
|
|
1805
|
+
'content-disposition': artifactContentDisposition(result.filename, route.download),
|
|
1806
|
+
etag: `"${result.sha256}"`,
|
|
1807
|
+
'x-artifact-content-length': String(result.selectedBytes),
|
|
1808
|
+
'content-security-policy': "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
1809
|
+
'referrer-policy': 'no-referrer',
|
|
1810
|
+
...(result.contentRange === undefined ? {} : { 'content-range': result.contentRange })
|
|
1811
|
+
}, res[RESPONSE_RELEASE_ID]));
|
|
1812
|
+
if (metadataOnly) {
|
|
1813
|
+
res.end();
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const streamDeadline = deadlineSignal(
|
|
1817
|
+
requestSignal,
|
|
1818
|
+
limits.visionBodyTimeoutMs,
|
|
1819
|
+
'Agent artifact stream exceeded its delivery deadline'
|
|
1820
|
+
);
|
|
1821
|
+
try {
|
|
1822
|
+
await streamArtifactBytes(res, result.body, result.selectedBytes, streamDeadline.signal);
|
|
1823
|
+
} finally {
|
|
1824
|
+
streamDeadline.cleanup();
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
const handler = async (req, res) => {
|
|
1829
|
+
const disconnect = requestAbortController(req, res);
|
|
1830
|
+
res[RESPONSE_RELEASE_ID] = assets.releaseId;
|
|
1831
|
+
let releaseBody = null;
|
|
1832
|
+
let releaseStream = null;
|
|
1833
|
+
let outcomeRoute = 'unknown';
|
|
1834
|
+
let outcomeFetchMetadata = 'unchecked';
|
|
1835
|
+
let outcomeRelease = 'unchecked';
|
|
1836
|
+
let outcomeErrorCode = null;
|
|
1837
|
+
try {
|
|
1838
|
+
requirePublicAuthority(req, publicHost);
|
|
1839
|
+
const clientAddress = resolveTrustedClientAddress(req);
|
|
1840
|
+
if (req.method !== 'POST') rejectRequestBodyFraming(req);
|
|
1841
|
+
const route = classifyRequestTarget(req.url, assets);
|
|
1842
|
+
outcomeRoute = route.kind === 'chat' ? 'chat'
|
|
1843
|
+
: (route.kind === 'agent' ? 'agent'
|
|
1844
|
+
: (route.kind === 'agent_artifact' ? 'agent_artifact'
|
|
1845
|
+
: (route.pathname === CLOUD_ROUTES.login ? 'login'
|
|
1846
|
+
: (route.pathname === CLOUD_ROUTES.session ? 'session'
|
|
1847
|
+
: (route.pathname === CLOUD_ROUTES.logout ? 'logout'
|
|
1848
|
+
: (route.kind === 'asset' ? 'asset' : 'unknown'))))));
|
|
1849
|
+
if (route.kind === 'invalid' || route.kind === 'not_found') {
|
|
1850
|
+
if (req.method === 'POST' && hasRequestBodyFraming(req)) req.shouldKeepAlive = false;
|
|
1851
|
+
if (route.kind === 'invalid') throw new CloudHttpError(400, 'invalid_target', 'The request target is not normalized.');
|
|
1852
|
+
throw new CloudHttpError(404, 'not_found', 'The requested route does not exist.');
|
|
1853
|
+
}
|
|
1854
|
+
if (route.kind === 'asset') {
|
|
1855
|
+
if (!['GET', 'HEAD'].includes(req.method)) {
|
|
1856
|
+
if (hasRequestBodyFraming(req)) req.shouldKeepAlive = false;
|
|
1857
|
+
methodNotAllowed(req, res, 'GET, HEAD');
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const asset = assets.get(route.target);
|
|
1861
|
+
const cacheControl = route.target === '/sw.js'
|
|
1862
|
+
? 'no-store, no-cache, must-revalidate'
|
|
1863
|
+
: (assets.isImmutable(route.target) ? IMMUTABLE_CACHE_CONTROL : DYNAMIC_CACHE_CONTROL);
|
|
1864
|
+
sendBuffer(req, res, 200, asset.body, {
|
|
1865
|
+
...commonSecurityHeaders(),
|
|
1866
|
+
...asset.headers,
|
|
1867
|
+
'content-type': asset.contentType,
|
|
1868
|
+
'cache-control': cacheControl,
|
|
1869
|
+
...(route.target === '/' ? {
|
|
1870
|
+
'content-security-policy': "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' blob:; connect-src 'self'; font-src 'none'; manifest-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; worker-src 'self'"
|
|
1871
|
+
} : {}),
|
|
1872
|
+
...(route.target === '/sw.js' ? { pragma: 'no-cache', expires: '0', 'service-worker-allowed': '/' } : {})
|
|
1873
|
+
});
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (route.kind === 'agent_artifact') {
|
|
1877
|
+
if (!['GET', 'HEAD'].includes(req.method)) {
|
|
1878
|
+
if (hasRequestBodyFraming(req)) req.shouldKeepAlive = false;
|
|
1879
|
+
methodNotAllowed(req, res, 'GET, HEAD');
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
outcomeFetchMetadata = artifactFetchMetadataState(req);
|
|
1883
|
+
if (outcomeFetchMetadata === 'invalid') rejectFetchMetadata();
|
|
1884
|
+
outcomeRelease = artifactClientReleaseState(req, route.releaseId, assets.releaseId);
|
|
1885
|
+
if (outcomeRelease !== 'match') rejectClientRelease();
|
|
1886
|
+
const authenticated = requireAuthentication(await authenticate(req, { csrf: false }));
|
|
1887
|
+
releaseStream = streamGate.enter(authenticated.browserSession);
|
|
1888
|
+
if (!releaseStream) {
|
|
1889
|
+
throw new CloudHttpError(429, 'stream_rate_limited', 'Too many artifact streams are active.', { retryAfter: 2 });
|
|
1890
|
+
}
|
|
1891
|
+
await handleAgentArtifactContent(req, res, route, authenticated, disconnect.controller.signal);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (req.method !== 'POST') {
|
|
1895
|
+
methodNotAllowed(req, res, 'POST');
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
requireOrigin(req, origin);
|
|
1899
|
+
outcomeFetchMetadata = fetchMetadataState(req);
|
|
1900
|
+
if (outcomeFetchMetadata === 'invalid') rejectFetchMetadata();
|
|
1901
|
+
let metadataAuthenticated = null;
|
|
1902
|
+
if (outcomeFetchMetadata !== 'complete' && (route.kind === 'chat' || route.kind === 'agent')) {
|
|
1903
|
+
try {
|
|
1904
|
+
metadataAuthenticated = await authenticate(req);
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
if (error instanceof CloudHttpError && error.code === 'csrf_rejected') rejectFetchMetadata();
|
|
1907
|
+
throw error;
|
|
1908
|
+
}
|
|
1909
|
+
if (!metadataAuthenticated) rejectFetchMetadata();
|
|
1910
|
+
}
|
|
1911
|
+
outcomeRelease = clientReleaseState(req, assets.releaseId);
|
|
1912
|
+
if (outcomeRelease === 'mismatch') rejectClientRelease();
|
|
1913
|
+
const preauthenticated = route.kind === 'chat' || route.kind === 'agent'
|
|
1914
|
+
? requireAuthentication(metadataAuthenticated ?? await authenticate(req))
|
|
1915
|
+
: null;
|
|
1916
|
+
const admissionKey = preauthenticated
|
|
1917
|
+
? `${clientAddress}\u0000${configuredAccount.principalId}`
|
|
1918
|
+
: clientAddress;
|
|
1919
|
+
releaseBody = bodyGate.enter(admissionKey);
|
|
1920
|
+
if (!releaseBody) throw new CloudHttpError(503, 'request_busy', 'The request service is temporarily busy.', { retryAfter: 1 });
|
|
1921
|
+
const body = await readJsonBody(
|
|
1922
|
+
req,
|
|
1923
|
+
bodyLimitForRoute(route.pathname),
|
|
1924
|
+
route.pathname === CLOUD_ROUTES.chatRunsStart
|
|
1925
|
+
? limits.visionBodyTimeoutMs
|
|
1926
|
+
: limits.bodyTimeoutMs
|
|
1927
|
+
);
|
|
1928
|
+
releaseBody();
|
|
1929
|
+
releaseBody = null;
|
|
1930
|
+
const streamRoute = route.pathname === CLOUD_ROUTES.chatRunsEvents
|
|
1931
|
+
|| (route.kind === 'agent' && route.nativeAgentPath === AGINTI_RPC_PATHS.runsEvents);
|
|
1932
|
+
if (streamRoute) {
|
|
1933
|
+
releaseStream = streamGate.enter(preauthenticated.browserSession);
|
|
1934
|
+
if (!releaseStream) {
|
|
1935
|
+
throw new CloudHttpError(429, 'stream_rate_limited', 'Too many event streams are active.', { retryAfter: 2 });
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (route.pathname === CLOUD_ROUTES.login) {
|
|
1939
|
+
await handleLogin(req, res, validateLoginBody(body), disconnect.controller.signal, clientAddress);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (route.pathname === CLOUD_ROUTES.session) {
|
|
1943
|
+
validateEmptyBody(body, 'session request');
|
|
1944
|
+
await handleSession(req, res);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (route.pathname === CLOUD_ROUTES.logout) {
|
|
1948
|
+
validateEmptyBody(body, 'logout request');
|
|
1949
|
+
await handleLogout(req, res);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (route.kind === 'chat') {
|
|
1953
|
+
await handleChat(req, res, route, body, preauthenticated, disconnect.controller.signal);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (route.kind === 'agent') {
|
|
1957
|
+
await handleAgent(req, res, route, body, preauthenticated, disconnect.controller.signal);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
throw new CloudHttpError(404, 'not_found', 'The requested route does not exist.');
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
const safe = publicError(error);
|
|
1963
|
+
outcomeErrorCode = safe.code;
|
|
1964
|
+
sendError(req, res, safe);
|
|
1965
|
+
} finally {
|
|
1966
|
+
releaseBody?.();
|
|
1967
|
+
releaseStream?.();
|
|
1968
|
+
disconnect.cleanup();
|
|
1969
|
+
if (requestOutcomeObserver !== undefined && req.method === 'POST') {
|
|
1970
|
+
try {
|
|
1971
|
+
const status = Number.isSafeInteger(res.statusCode) && res.statusCode >= 100 && res.statusCode <= 599
|
|
1972
|
+
? res.statusCode
|
|
1973
|
+
: 500;
|
|
1974
|
+
requestOutcomeObserver(Object.freeze({
|
|
1975
|
+
schemaVersion: 1,
|
|
1976
|
+
timestamp: new Date(epochMilliseconds(clock)).toISOString(),
|
|
1977
|
+
route: outcomeRoute,
|
|
1978
|
+
status,
|
|
1979
|
+
result: status < 400 ? 'accepted' : 'rejected',
|
|
1980
|
+
errorCode: outcomeErrorCode,
|
|
1981
|
+
fetchMetadata: outcomeFetchMetadata,
|
|
1982
|
+
release: outcomeRelease
|
|
1983
|
+
}));
|
|
1984
|
+
} catch { /* Telemetry is bounded, best-effort, and never affects a request. */ }
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
Object.defineProperties(handler, {
|
|
1989
|
+
releaseId: { value: assets.releaseId, enumerable: true },
|
|
1990
|
+
closeBackgroundJobs: { value: closeGenerationJobs, enumerable: true },
|
|
1991
|
+
activeDirectChatJobs: { get: () => jobs.size, enumerable: true },
|
|
1992
|
+
activeStreams: { get: () => streamGate.active, enumerable: true },
|
|
1993
|
+
activeEphemeralSessions: { get: () => ephemeralSessions.size, enumerable: true },
|
|
1994
|
+
maximumBodyReadTimeoutMs: {
|
|
1995
|
+
value: Math.max(limits.bodyTimeoutMs, limits.visionBodyTimeoutMs)
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
return handler;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
export function createCloudServer(options) {
|
|
2002
|
+
const handler = createCloudRequestHandler(options);
|
|
2003
|
+
const server = createNodeServer({
|
|
2004
|
+
connectionsCheckingInterval: 1_000,
|
|
2005
|
+
highWaterMark: 64 * 1024,
|
|
2006
|
+
insecureHTTPParser: false,
|
|
2007
|
+
joinDuplicateHeaders: false,
|
|
2008
|
+
keepAlive: true,
|
|
2009
|
+
keepAliveInitialDelay: 1_000,
|
|
2010
|
+
maxHeaderSize: 16 * 1024,
|
|
2011
|
+
noDelay: true,
|
|
2012
|
+
requireHostHeader: true,
|
|
2013
|
+
uniqueHeaders: ['content-length', 'content-type', 'cache-control']
|
|
2014
|
+
}, handler);
|
|
2015
|
+
server.headersTimeout = 10_000;
|
|
2016
|
+
// Node's outer request deadline begins before the route-level body reader.
|
|
2017
|
+
// Keep a bounded allowance for headers/authentication ahead of the longest
|
|
2018
|
+
// accepted body window so mobile vision uploads reach the stricter reader.
|
|
2019
|
+
server.requestTimeout = handler.maximumBodyReadTimeoutMs + REQUEST_BODY_TIMEOUT_MARGIN_MS;
|
|
2020
|
+
server.keepAliveTimeout = 5_000;
|
|
2021
|
+
server.maxHeadersCount = 64;
|
|
2022
|
+
server.on('close', () => { void handler.closeBackgroundJobs(); });
|
|
2023
|
+
let shutdownPromise = null;
|
|
2024
|
+
Object.defineProperty(server, 'shutdown', {
|
|
2025
|
+
enumerable: true,
|
|
2026
|
+
value() {
|
|
2027
|
+
if (shutdownPromise) return shutdownPromise;
|
|
2028
|
+
const drained = handler.closeBackgroundJobs();
|
|
2029
|
+
shutdownPromise = new Promise((resolve, reject) => {
|
|
2030
|
+
if (!server.listening) {
|
|
2031
|
+
resolve();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
2035
|
+
server.closeAllConnections?.();
|
|
2036
|
+
}).then(() => drained);
|
|
2037
|
+
return shutdownPromise;
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
return server;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
export const CLOUD_AGENT_PUBLIC_ROUTES = Object.freeze(Object.keys(AGENT_ROUTE_MAP));
|