@ouraihub/aether-worker 0.1.0 → 0.1.1
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/package.json +5 -4
- package/src/app.ts +52 -16
- package/src/context.ts +9 -3
- package/src/conversations/action.ts +31 -8
- package/src/conversations/index.ts +144 -141
- package/src/conversations/model.ts +36 -11
- package/src/conversations/routes.ts +101 -0
- package/src/coordinator-helpers.ts +131 -0
- package/src/coordinator.ts +54 -126
- package/src/credentials.ts +22 -4
- package/src/files.ts +17 -5
- package/src/host.ts +10 -2
- package/src/identity.ts +54 -25
- package/src/images/index.ts +39 -12
- package/src/logger.ts +15 -4
- package/src/mcp/auth.ts +65 -17
- package/src/mcp/client.ts +89 -24
- package/src/mcp/common.ts +7 -3
- package/src/mcp/index.ts +74 -21
- package/src/mcp/oauth.ts +82 -23
- package/src/mcp/types.ts +9 -2
- package/src/outbound.ts +23 -8
- package/src/providers/catalog.ts +63 -0
- package/src/providers/http.ts +88 -0
- package/src/providers/index.ts +103 -163
- package/src/providers/model-options.ts +28 -0
- package/src/runtime-input.ts +44 -0
- package/src/runtime-tools.ts +298 -0
- package/src/runtime.ts +105 -264
- package/src/search/index.ts +89 -23
- package/src/skills/persistence.ts +18 -5
- package/src/skills/service.ts +60 -13
- package/src/storage.ts +108 -13
- package/src/testing/fakes.ts +97 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouraihub/aether-worker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "LGPL-3.0-only",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"./*": "./src/*.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@ouraihub/aether-contracts": "0.1.
|
|
23
|
-
"@ouraihub/aether-core": "0.1.
|
|
22
|
+
"@ouraihub/aether-contracts": "0.1.1",
|
|
23
|
+
"@ouraihub/aether-core": "0.1.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {},
|
|
26
26
|
"files": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "tsx --test \"src/**/*.test.ts\""
|
|
38
|
+
"test": "tsx --test \"src/**/*.test.ts\"",
|
|
39
|
+
"scenarios": "tsx scenarios/run.ts"
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/app.ts
CHANGED
|
@@ -47,7 +47,11 @@ const statuses: Record<KitErrorCode, number> = {
|
|
|
47
47
|
function failureResponse(error: unknown): Response {
|
|
48
48
|
let safe: KitError = safeError(error)
|
|
49
49
|
if (safe.code === 'internal')
|
|
50
|
-
safe = {
|
|
50
|
+
safe = {
|
|
51
|
+
code: 'internal',
|
|
52
|
+
message: 'Internal request failure',
|
|
53
|
+
details: { errorId: crypto.randomUUID() },
|
|
54
|
+
}
|
|
51
55
|
return Response.json(safe, { status: statuses[safe.code] })
|
|
52
56
|
}
|
|
53
57
|
function loopback(url: URL): boolean {
|
|
@@ -97,17 +101,25 @@ function secured(response: Response, origin: string | null, allowed: string): Re
|
|
|
97
101
|
headers.set('Referrer-Policy', 'no-referrer')
|
|
98
102
|
headers.set(
|
|
99
103
|
'Cache-Control',
|
|
100
|
-
headers.get('content-type')?.startsWith('text/event-stream')
|
|
104
|
+
headers.get('content-type')?.startsWith('text/event-stream')
|
|
105
|
+
? 'no-cache, no-transform'
|
|
106
|
+
: 'no-store',
|
|
101
107
|
)
|
|
102
108
|
if (origin === allowed) {
|
|
103
109
|
headers.set('Access-Control-Allow-Origin', allowed)
|
|
104
110
|
headers.set('Access-Control-Allow-Credentials', 'true')
|
|
105
111
|
headers.append('Vary', 'Origin')
|
|
106
112
|
}
|
|
107
|
-
return new Response(response.body, {
|
|
113
|
+
return new Response(response.body, {
|
|
114
|
+
status: response.status,
|
|
115
|
+
statusText: response.statusText,
|
|
116
|
+
headers,
|
|
117
|
+
})
|
|
108
118
|
}
|
|
109
119
|
/** Product entrypoints compose this handler; this package never supplies product login or business tools. */
|
|
110
|
-
export function createAetherWorkerApp(options: WorkerAppOptions): {
|
|
120
|
+
export function createAetherWorkerApp(options: WorkerAppOptions): {
|
|
121
|
+
fetch(request: Request): Promise<Response>
|
|
122
|
+
} {
|
|
111
123
|
const publicOrigin = new URL(options.origin)
|
|
112
124
|
if (
|
|
113
125
|
!options.applicationId ||
|
|
@@ -127,9 +139,13 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
127
139
|
origin = raw.headers.get('origin')
|
|
128
140
|
let response: Response
|
|
129
141
|
try {
|
|
130
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
url.protocol !== 'https:' &&
|
|
144
|
+
!(options.allowLoopbackHttp && url.protocol === 'http:' && loopback(url))
|
|
145
|
+
)
|
|
131
146
|
throw new KitFailure('forbidden', 'Secure transport required')
|
|
132
|
-
if (origin !== null && origin !== options.origin)
|
|
147
|
+
if (origin !== null && origin !== options.origin)
|
|
148
|
+
throw new KitFailure('forbidden', 'Origin rejected')
|
|
133
149
|
if (raw.method === 'OPTIONS') {
|
|
134
150
|
if (origin !== options.origin) throw new KitFailure('forbidden', 'Origin rejected')
|
|
135
151
|
return secured(
|
|
@@ -147,12 +163,15 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
147
163
|
}
|
|
148
164
|
const mutating = !['GET', 'HEAD'].includes(raw.method)
|
|
149
165
|
const publicRoute =
|
|
150
|
-
url.pathname === '/mcp' ||
|
|
166
|
+
url.pathname === '/mcp' ||
|
|
167
|
+
url.pathname.startsWith('/oauth/') ||
|
|
168
|
+
url.pathname.startsWith('/.well-known/')
|
|
151
169
|
if (
|
|
152
170
|
mutating &&
|
|
153
171
|
raw.body !== null &&
|
|
154
172
|
!publicRoute &&
|
|
155
|
-
raw.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !==
|
|
173
|
+
raw.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !==
|
|
174
|
+
'application/json'
|
|
156
175
|
)
|
|
157
176
|
throw badRequest('contentType', 'json_required')
|
|
158
177
|
const request = await boundedRequest(raw, maximum)
|
|
@@ -162,7 +181,8 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
162
181
|
publicResponse = await handler.handlePublic(request)
|
|
163
182
|
if (publicResponse) break
|
|
164
183
|
}
|
|
165
|
-
if (!publicResponse)
|
|
184
|
+
if (!publicResponse)
|
|
185
|
+
throw new KitFailure('not_found', 'Unknown route', { entity: 'route' })
|
|
166
186
|
response = publicResponse
|
|
167
187
|
} else {
|
|
168
188
|
const actor = await options.identity.resolve(request)
|
|
@@ -176,7 +196,12 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
176
196
|
)
|
|
177
197
|
throw new KitFailure('forbidden', 'Request authorization rejected')
|
|
178
198
|
await options.authorizeRoute(actor, request)
|
|
179
|
-
const context: WorkerContext = {
|
|
199
|
+
const context: WorkerContext = {
|
|
200
|
+
actor: immutableActor(actor),
|
|
201
|
+
requestId,
|
|
202
|
+
now,
|
|
203
|
+
signal: request.signal,
|
|
204
|
+
}
|
|
180
205
|
if (url.pathname === '/api/v1/runs' && request.method === 'POST') {
|
|
181
206
|
const input: unknown = await request.json()
|
|
182
207
|
if (!isContract('RunRequest', input)) throw badRequest('run', 'invalid')
|
|
@@ -190,25 +215,33 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
190
215
|
})
|
|
191
216
|
const value: unknown = await accepted.json()
|
|
192
217
|
if (accepted.status !== 202) {
|
|
193
|
-
if (isContract('KitError', value))
|
|
218
|
+
if (isContract('KitError', value))
|
|
219
|
+
throw new KitFailure(value.code, value.message, value.details)
|
|
194
220
|
throw new KitFailure('internal', 'Invalid coordinator reply')
|
|
195
221
|
}
|
|
196
|
-
if (!isContract('RunAccepted', value))
|
|
222
|
+
if (!isContract('RunAccepted', value))
|
|
223
|
+
throw new KitFailure('internal', 'Invalid coordinator acceptance')
|
|
197
224
|
response = Response.json(
|
|
198
225
|
{ accepted: value, events: `/api/v1/runs/${encodeURIComponent(runId)}/events` },
|
|
199
226
|
{ status: 202 },
|
|
200
227
|
)
|
|
201
228
|
} else {
|
|
202
|
-
const run = /^\/api\/v1\/runs\/([^/]+)(?:\/(events|cancel|approval))?$/u.exec(
|
|
229
|
+
const run = /^\/api\/v1\/runs\/([^/]+)(?:\/(events|cancel|approval))?$/u.exec(
|
|
230
|
+
url.pathname,
|
|
231
|
+
)
|
|
203
232
|
if (run) {
|
|
204
233
|
const runId = decodeURIComponent(run[1] ?? ''),
|
|
205
234
|
action = run[2] ?? 'snapshot'
|
|
206
235
|
if (
|
|
207
236
|
!/^[A-Za-z0-9_-]{1,128}$/u.test(runId) ||
|
|
208
|
-
(['events', 'snapshot'].includes(action)
|
|
237
|
+
(['events', 'snapshot'].includes(action)
|
|
238
|
+
? request.method !== 'GET'
|
|
239
|
+
: request.method !== 'POST')
|
|
209
240
|
)
|
|
210
241
|
throw badRequest('run', 'invalid')
|
|
211
|
-
const stub = options.runs.get(
|
|
242
|
+
const stub = options.runs.get(
|
|
243
|
+
options.runs.idFromName(`${options.applicationId}:${runId}`),
|
|
244
|
+
),
|
|
212
245
|
headers = new Headers({ 'x-aether-actor': actorHeader(context.actor) })
|
|
213
246
|
if (request.headers.has('Last-Event-ID'))
|
|
214
247
|
headers.set('Last-Event-ID', request.headers.get('Last-Event-ID') ?? '')
|
|
@@ -244,7 +277,10 @@ export function createAetherWorkerApp(options: WorkerAppOptions): { fetch(reques
|
|
|
244
277
|
stub
|
|
245
278
|
.fetch('https://aether.internal/cancel', {
|
|
246
279
|
method: 'POST',
|
|
247
|
-
headers: {
|
|
280
|
+
headers: {
|
|
281
|
+
'x-aether-actor': actorHeader(context.actor),
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
},
|
|
248
284
|
body: '{}',
|
|
249
285
|
})
|
|
250
286
|
.then(async (reply) => {
|
package/src/context.ts
CHANGED
|
@@ -29,14 +29,20 @@ export function isActor(value: unknown): value is Actor {
|
|
|
29
29
|
(value.kind === 'user' || value.kind === 'service' || value.kind === 'mcp_client') &&
|
|
30
30
|
'tenantId' in value &&
|
|
31
31
|
(value.tenantId === null ||
|
|
32
|
-
(typeof value.tenantId === 'string' &&
|
|
32
|
+
(typeof value.tenantId === 'string' &&
|
|
33
|
+
value.tenantId.length > 0 &&
|
|
34
|
+
value.tenantId.length <= 256)) &&
|
|
33
35
|
'sessionId' in value &&
|
|
34
36
|
(value.sessionId === null ||
|
|
35
|
-
(typeof value.sessionId === 'string' &&
|
|
37
|
+
(typeof value.sessionId === 'string' &&
|
|
38
|
+
value.sessionId.length > 0 &&
|
|
39
|
+
value.sessionId.length <= 256)) &&
|
|
36
40
|
'scopes' in value &&
|
|
37
41
|
Array.isArray(value.scopes) &&
|
|
38
42
|
value.scopes.length <= 256 &&
|
|
39
|
-
value.scopes.every(
|
|
43
|
+
value.scopes.every(
|
|
44
|
+
(scope: unknown) => typeof scope === 'string' && scope.length > 0 && scope.length <= 256,
|
|
45
|
+
)
|
|
40
46
|
)
|
|
41
47
|
}
|
|
42
48
|
export function immutableActor(actor: Actor): Actor {
|
|
@@ -21,7 +21,9 @@ export type Action =
|
|
|
21
21
|
| { kind: 'retry'; sourceId: string }
|
|
22
22
|
| { kind: 'translation'; sourceId: string; targetLanguage: string }
|
|
23
23
|
export type SelectionGuard =
|
|
24
|
-
|
|
24
|
+
| { kind: 'explicit' }
|
|
25
|
+
| { kind: 'selected'; turnId: string }
|
|
26
|
+
| { kind: 'latest'; turnId: string | null }
|
|
25
27
|
export interface ReservationInput {
|
|
26
28
|
input: string
|
|
27
29
|
images: ImagePart[]
|
|
@@ -46,12 +48,16 @@ export function authoritative(
|
|
|
46
48
|
guard: SelectionGuard,
|
|
47
49
|
document: ConversationDocument | null,
|
|
48
50
|
): ReservationInput {
|
|
49
|
-
if ((request.conversationId ?? null) !== (document?.summary.id ?? null))
|
|
51
|
+
if ((request.conversationId ?? null) !== (document?.summary.id ?? null))
|
|
52
|
+
throw notFound('conversation')
|
|
50
53
|
if (document) {
|
|
51
54
|
idle(document)
|
|
52
55
|
if (guard.kind === 'selected' && document.summary.selectedTurnId !== guard.turnId)
|
|
53
56
|
throw conflict('preparation_changed')
|
|
54
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
guard.kind === 'latest' &&
|
|
59
|
+
(document.summary.selectedTurnId !== null || latest(document) !== guard.turnId)
|
|
60
|
+
)
|
|
55
61
|
throw conflict('preparation_changed')
|
|
56
62
|
}
|
|
57
63
|
let input = request.input,
|
|
@@ -101,7 +107,11 @@ async function digest(value: string): Promise<string> {
|
|
|
101
107
|
const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
|
|
102
108
|
return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
103
109
|
}
|
|
104
|
-
async function imageBasis(
|
|
110
|
+
async function imageBasis(
|
|
111
|
+
turnId: string,
|
|
112
|
+
kind: string,
|
|
113
|
+
images: readonly StoredImage[],
|
|
114
|
+
): Promise<unknown[]> {
|
|
105
115
|
return Promise.all(
|
|
106
116
|
images.map(async (image, index) => ({
|
|
107
117
|
turnId,
|
|
@@ -151,7 +161,11 @@ export async function fingerprint(
|
|
|
151
161
|
})),
|
|
152
162
|
)
|
|
153
163
|
const currentImages = await Promise.all(
|
|
154
|
-
input.images.map(async (image, index) => ({
|
|
164
|
+
input.images.map(async (image, index) => ({
|
|
165
|
+
index,
|
|
166
|
+
mime: image.mime,
|
|
167
|
+
data: await digest(image.data),
|
|
168
|
+
})),
|
|
155
169
|
)
|
|
156
170
|
return digest(
|
|
157
171
|
canonicalJson({
|
|
@@ -185,13 +199,22 @@ export async function resolveAction(
|
|
|
185
199
|
checkText('input', request.input, 100_000)
|
|
186
200
|
validateImages(request.images ?? [])
|
|
187
201
|
const sourced = Boolean(request.retryTurnId || request.translateTurnId || request.regenerate)
|
|
188
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
Number(Boolean(request.retryTurnId)) +
|
|
204
|
+
Number(Boolean(request.translateTurnId)) +
|
|
205
|
+
Number(request.regenerate) >
|
|
206
|
+
1
|
|
207
|
+
)
|
|
189
208
|
throw badRequest('action', 'multiple')
|
|
190
209
|
if (sourced && request.replyToId) throw badRequest('replyToId', 'action_conflict')
|
|
191
210
|
if (sourced && (request.images?.length ?? 0) > 0) throw badRequest('images', 'action_conflict')
|
|
192
|
-
if (
|
|
211
|
+
if (
|
|
212
|
+
Boolean(request.translateTurnId) !==
|
|
213
|
+
(request.targetLanguage !== null && request.targetLanguage !== undefined)
|
|
214
|
+
)
|
|
193
215
|
throw badRequest('targetLanguage', 'translation_required')
|
|
194
|
-
if (!request.conversationId && (sourced || request.replyToId))
|
|
216
|
+
if (!request.conversationId && (sourced || request.replyToId))
|
|
217
|
+
throw badRequest('conversationId', 'required')
|
|
195
218
|
if ((request.retryTurnId || request.translateTurnId) && request.webSearch)
|
|
196
219
|
throw badRequest('webSearch', 'action_conflict')
|
|
197
220
|
let guard: SelectionGuard = { kind: 'explicit' },
|