{"version":3,"file":"admission-service.d.ts","sourceRoot":"","sources":["../../../src/core/shared-inference/admission-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAMN,KAAK,iBAAiB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAI1D,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,mBAAmB,CAAC;CAC3B;AAED,MAAM,WAAW,sCAAsC;IACtD,SAAS,EAAE,wBAAwB,CAAC;IACpC,SAAS,EAAE,uBAAuB,EAAE,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAYD,qBAAa,+BAA+B;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAClE,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAK;IAElB,YAAY,OAAO,EAAE,sCAAsC,EAO1D;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;IAED,4EAA4E;IAC5E,UAAU,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,oBAAoB,CASxG;IAED,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE/B;YAMa,OAAO;IAgGrB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,iBAAiB;YAsBX,SAAS;IAiBvB,OAAO,CAAC,MAAM;IAed,OAAO,CAAC,KAAK;CAKb;AAED,YAAY,EAAE,iBAAiB,EAAE,CAAC","sourcesContent":["/**\n * Shared inference admission service (3.0.0 cross-host bridge).\n *\n * A SMALL local HTTP service on Bucephalus that exposes the central\n * `SharedInferenceScheduler` to remote Jensen runtimes. It is deliberately NOT a\n * general distributed orchestration server: it exposes only inference admission\n * (request/wait/renew/release/cancel) plus a health probe, over structured JSON.\n *\n * Authority: the service wraps a scheduler over the SAME durable ledger local\n * processes use. It never duplicates the scheduling algorithm and never issues\n * independent capacity. Remote clients authenticate with a short-lived,\n * execution-scoped bearer token.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport {\n\tADMISSION_PROTOCOL_VERSION,\n\ttype AdmissionErrorCode,\n\ttype AdmissionReleasePayload,\n\ttype AdmissionRenewPayload,\n\ttype AdmissionRequestPayload,\n\ttype AdmissionResponse,\n} from \"./admission-protocol.js\";\nimport type { SharedInferenceScheduler } from \"./scheduler.js\";\nimport type { SharedInferenceResource } from \"./types.js\";\n\nconst MAX_BODY_BYTES = 1_000_000;\n\nexport interface AdmissionTokenScope {\n\texecutionId: string;\n\tremoteTargetId?: string;\n\texpiresAtMs: number;\n}\n\nexport interface IssuedAdmissionToken {\n\ttoken: string;\n\tscope: AdmissionTokenScope;\n}\n\nexport interface SharedInferenceAdmissionServiceOptions {\n\tscheduler: SharedInferenceScheduler;\n\tresources: SharedInferenceResource[];\n\thost?: string;\n\tport?: number;\n\ttokenTtlMs?: number;\n\tnow?: () => number;\n}\n\nclass AdmissionHttpError extends Error {\n\treadonly status: number;\n\treadonly code: AdmissionErrorCode;\n\tconstructor(status: number, code: AdmissionErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t\tthis.code = code;\n\t}\n}\n\nexport class SharedInferenceAdmissionService {\n\tprivate readonly _scheduler: SharedInferenceScheduler;\n\tprivate readonly _resources: SharedInferenceResource[];\n\tprivate readonly _host: string;\n\tprivate readonly _requestedPort: number;\n\tprivate readonly _tokenTtlMs: number;\n\tprivate readonly _now: () => number;\n\tprivate readonly _tokens = new Map<string, AdmissionTokenScope>();\n\tprivate _server?: Server;\n\tprivate _port = 0;\n\n\tconstructor(options: SharedInferenceAdmissionServiceOptions) {\n\t\tthis._scheduler = options.scheduler;\n\t\tthis._resources = options.resources;\n\t\tthis._host = options.host ?? \"127.0.0.1\";\n\t\tthis._requestedPort = options.port ?? 0;\n\t\tthis._tokenTtlMs = options.tokenTtlMs ?? 10 * 60_000;\n\t\tthis._now = options.now ?? (() => Date.now());\n\t}\n\n\tget port(): number {\n\t\treturn this._port;\n\t}\n\n\tget url(): string {\n\t\treturn `http://${this._host}:${this._port}`;\n\t}\n\n\tasync start(): Promise<void> {\n\t\tawait Promise.all(this._resources.map((resource) => this._scheduler.registerResource(resource)));\n\t\tthis._server = createServer((req, res) => {\n\t\t\tvoid this._handle(req, res).catch((error) => {\n\t\t\t\tif (error instanceof AdmissionHttpError) {\n\t\t\t\t\tthis._send(res, error.status, { kind: \"error\", code: error.code, message: error.message });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._send(res, 500, {\n\t\t\t\t\tkind: \"error\",\n\t\t\t\t\tcode: \"INTERNAL_ERROR\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis._server!.once(\"error\", reject);\n\t\t\tthis._server!.listen(this._requestedPort, this._host, () => {\n\t\t\t\tconst address = this._server!.address();\n\t\t\t\tthis._port = typeof address === \"object\" && address ? address.port : this._requestedPort;\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tconst server = this._server;\n\t\tthis._server = undefined;\n\t\tthis._port = 0;\n\t\tif (!server) return;\n\t\tawait new Promise<void>((resolve) => server.close(() => resolve()));\n\t}\n\n\t/** Issue a short-lived, execution-scoped token for one remote execution. */\n\tissueToken(scope: { executionId: string; remoteTargetId?: string; ttlMs?: number }): IssuedAdmissionToken {\n\t\tconst token = `sched_${randomUUID()}`;\n\t\tconst fullScope: AdmissionTokenScope = {\n\t\t\texecutionId: scope.executionId,\n\t\t\tremoteTargetId: scope.remoteTargetId,\n\t\t\texpiresAtMs: this._now() + (scope.ttlMs ?? this._tokenTtlMs),\n\t\t};\n\t\tthis._tokens.set(token, fullScope);\n\t\treturn { token, scope: { ...fullScope } };\n\t}\n\n\trevokeToken(token: string): void {\n\t\tthis._tokens.delete(token);\n\t}\n\n\t// =========================================================================\n\t// HTTP dispatch\n\t// =========================================================================\n\n\tprivate async _handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n\t\tconst url = new URL(req.url ?? \"/\", this.url);\n\t\tif (req.method === \"GET\" && url.pathname === \"/v1/health\") {\n\t\t\tthis._send(res, 200, { ok: true, protocolVersion: ADMISSION_PROTOCOL_VERSION });\n\t\t\treturn;\n\t\t}\n\n\t\tconst scope = this._authorize(req);\n\t\tif (!scope) {\n\t\t\tthis._send(res, 401, { kind: \"error\", code: \"UNAUTHORIZED\", message: \"missing or invalid admission token\" });\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\" && url.pathname === \"/v1/request\") {\n\t\t\tconst body = await this._readBody(req);\n\t\t\tconst doc = this._parse(body);\n\t\t\tthis._assertExecution(this._str(doc, \"executionId\"), scope);\n\t\t\tconst payload = doc as unknown as AdmissionRequestPayload;\n\t\t\tconst logicalAgentId = this._str(doc, \"logicalAgentId\");\n\t\t\tconst provider = this._str(doc, \"provider\");\n\t\t\tconst model = this._str(doc, \"model\");\n\t\t\tconst resource = this._scheduler.resourceFor({ provider, id: model });\n\t\t\tif (!resource) {\n\t\t\t\tthrow new AdmissionHttpError(404, \"RESOURCE_NOT_REGISTERED\", `no shared resource for ${provider}/${model}`);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst outcome = await this._scheduler.enqueue({\n\t\t\t\t\tlogicalAgentId,\n\t\t\t\t\tresource,\n\t\t\t\t\tmodel: { provider, id: model },\n\t\t\t\t\tinferenceRequestId: payload.inferenceRequestId,\n\t\t\t\t\tmissionId: payload.missionId,\n\t\t\t\t\tassignmentId: payload.assignmentId,\n\t\t\t\t\texecutionId: payload.executionId,\n\t\t\t\t\tpriority: payload.priority,\n\t\t\t\t\tdependency: payload.dependency,\n\t\t\t\t\testimatedInputTokens: payload.estimatedInputTokens,\n\t\t\t\t\tmaxOutputTokens: payload.maxOutputTokens,\n\t\t\t\t});\n\t\t\t\tthis._send(res, 200, { kind: \"request_result\", outcome });\n\t\t\t\treturn;\n\t\t\t} catch (error) {\n\t\t\t\tthrow new AdmissionHttpError(\n\t\t\t\t\t503,\n\t\t\t\t\t\"SCHEDULER_UNAVAILABLE\",\n\t\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tif (req.method === \"GET\" && url.pathname === \"/v1/status\") {\n\t\t\tconst inferenceRequestId = url.searchParams.get(\"inferenceRequestId\");\n\t\t\tconst executionId = url.searchParams.get(\"executionId\");\n\t\t\tif (!inferenceRequestId || !executionId) {\n\t\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"missing query params\");\n\t\t\t}\n\t\t\tthis._assertExecution(executionId, scope);\n\t\t\tconst status = await this._scheduler.admissionStatus(inferenceRequestId);\n\t\t\tthis._send(res, 200, { kind: \"status_result\", status });\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\" && url.pathname === \"/v1/release\") {\n\t\t\tconst body = await this._readBody(req);\n\t\t\tconst doc = this._parse(body);\n\t\t\tthis._assertExecution(this._str(doc, \"executionId\"), scope);\n\t\t\tconst payload = doc as unknown as AdmissionReleasePayload;\n\t\t\tthis._validateAdmitted(payload.admitted);\n\t\t\tconst outcome = await this._scheduler.release(payload.admitted, payload.outcome);\n\t\t\tthis._send(res, 200, { kind: \"release_result\", outcome });\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\" && url.pathname === \"/v1/renew\") {\n\t\t\tconst body = await this._readBody(req);\n\t\t\tconst doc = this._parse(body);\n\t\t\tthis._assertExecution(this._str(doc, \"executionId\"), scope);\n\t\t\tconst payload = doc as unknown as AdmissionRenewPayload;\n\t\t\tthis._validateAdmitted(payload.admitted);\n\t\t\tconst outcome = await this._scheduler.renew(payload.admitted);\n\t\t\tthis._send(res, 200, { kind: \"renew_result\", outcome });\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\" && url.pathname === \"/v1/cancel\") {\n\t\t\tconst body = await this._readBody(req);\n\t\t\tconst doc = this._parse(body);\n\t\t\tthis._assertExecution(this._str(doc, \"executionId\"), scope);\n\t\t\tconst outcome = await this._scheduler.cancel(this._str(doc, \"inferenceRequestId\"));\n\t\t\tthis._send(res, 200, { kind: \"cancel_result\", outcome });\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new AdmissionHttpError(404, \"MALFORMED_REQUEST\", `unknown route ${req.method} ${url.pathname}`);\n\t}\n\n\tprivate _authorize(req: IncomingMessage): AdmissionTokenScope | undefined {\n\t\tconst header = req.headers.authorization ?? \"\";\n\t\tconst token = header.startsWith(\"Bearer \") ? header.slice(\"Bearer \".length) : undefined;\n\t\tif (!token) return undefined;\n\t\tconst scope = this._tokens.get(token);\n\t\tif (!scope) return undefined;\n\t\tif (scope.expiresAtMs <= this._now()) {\n\t\t\tthis._tokens.delete(token);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn scope;\n\t}\n\n\tprivate _assertExecution(executionId: unknown, scope: AdmissionTokenScope): void {\n\t\tif (typeof executionId !== \"string\" || executionId !== scope.executionId) {\n\t\t\tthrow new AdmissionHttpError(403, \"UNAUTHORIZED\", \"executionId does not match token scope\");\n\t\t}\n\t}\n\n\tprivate _str(doc: Record<string, unknown>, key: string): string {\n\t\tconst value = doc[key];\n\t\tif (typeof value !== \"string\" || value.length === 0) {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", `field '${key}' must be a non-empty string`);\n\t\t}\n\t\treturn value;\n\t}\n\n\tprivate _validateAdmitted(value: unknown): void {\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"field 'admitted' must be an object\");\n\t\t}\n\t\tconst admitted = value as Record<string, unknown>;\n\t\tif (typeof admitted.inferenceRequestId !== \"string\" || typeof admitted.resourceId !== \"string\") {\n\t\t\tthrow new AdmissionHttpError(\n\t\t\t\t400,\n\t\t\t\t\"MALFORMED_REQUEST\",\n\t\t\t\t\"admitted.inferenceRequestId/resourceId must be strings\",\n\t\t\t);\n\t\t}\n\t\tconst lease = admitted.lease;\n\t\tif (typeof lease !== \"object\" || lease === null || Array.isArray(lease)) {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"admitted.lease must be an object\");\n\t\t}\n\t\tconst l = lease as Record<string, unknown>;\n\t\tif (typeof l.leaseId !== \"string\" || typeof l.ownerId !== \"string\") {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"admitted.lease.leaseId/ownerId must be strings\");\n\t\t}\n\t}\n\n\tprivate async _readBody(req: IncomingMessage): Promise<unknown> {\n\t\tconst chunks: Buffer[] = [];\n\t\tlet size = 0;\n\t\tfor await (const chunk of req) {\n\t\t\tsize += (chunk as Buffer).length;\n\t\t\tif (size > MAX_BODY_BYTES) throw new AdmissionHttpError(413, \"MALFORMED_REQUEST\", \"request body too large\");\n\t\t\tchunks.push(chunk as Buffer);\n\t\t}\n\t\tconst raw = Buffer.concat(chunks).toString(\"utf8\");\n\t\tif (!raw.trim()) throw new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"empty request body\");\n\t\ttry {\n\t\t\treturn JSON.parse(raw);\n\t\t} catch {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"body is not valid JSON\");\n\t\t}\n\t}\n\n\tprivate _parse(body: unknown): Record<string, unknown> {\n\t\tif (typeof body !== \"object\" || body === null || Array.isArray(body)) {\n\t\t\tthrow new AdmissionHttpError(400, \"MALFORMED_REQUEST\", \"body must be an object\");\n\t\t}\n\t\tconst doc = body as Record<string, unknown>;\n\t\tif (doc.protocolVersion !== ADMISSION_PROTOCOL_VERSION) {\n\t\t\tthrow new AdmissionHttpError(\n\t\t\t\t400,\n\t\t\t\t\"PROTOCOL_MISMATCH\",\n\t\t\t\t`protocolVersion mismatch: ${String(doc.protocolVersion)}`,\n\t\t\t);\n\t\t}\n\t\treturn doc;\n\t}\n\n\tprivate _send(res: ServerResponse, status: number, body: unknown): void {\n\t\tif (res.headersSent) return;\n\t\tres.writeHead(status, { \"content-type\": \"application/json\" });\n\t\tres.end(JSON.stringify(body));\n\t}\n}\n\nexport type { AdmissionResponse };\n"]}