@agentuity/runtime 1.0.5 → 1.0.6
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/dist/handlers/cron.d.ts.map +1 -1
- package/dist/handlers/cron.js +2 -44
- package/dist/handlers/cron.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +25 -12
- package/dist/middleware.js.map +1 -1
- package/dist/signature.d.ts +22 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +63 -0
- package/dist/signature.js.map +1 -0
- package/dist/workbench.d.ts.map +1 -1
- package/dist/workbench.js +103 -109
- package/dist/workbench.js.map +1 -1
- package/package.json +7 -7
- package/src/handlers/cron.ts +6 -60
- package/src/middleware.ts +28 -12
- package/src/signature.ts +82 -0
- package/src/workbench.ts +113 -122
package/src/workbench.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Context, Handler, MiddlewareHandler } from 'hono';
|
|
2
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
3
2
|
import { toJSONSchema } from '@agentuity/server';
|
|
4
3
|
import { getAgents, createAgentMiddleware } from './agent';
|
|
5
4
|
import { createRouter } from './router';
|
|
@@ -13,6 +12,71 @@ import {
|
|
|
13
12
|
ensureAgentsImported,
|
|
14
13
|
} from './_metadata';
|
|
15
14
|
import { TOKENS_HEADER, DURATION_HEADER } from './_tokens';
|
|
15
|
+
import { verifySignature } from './signature';
|
|
16
|
+
import { isProduction } from './_config';
|
|
17
|
+
import { createCorsMiddleware } from './middleware';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Trusted Agentuity domain suffixes for workbench CORS.
|
|
21
|
+
* Any origin matching https://*.{suffix} is allowed.
|
|
22
|
+
* In development, any origin is allowed.
|
|
23
|
+
*/
|
|
24
|
+
const TRUSTED_WORKBENCH_DOMAIN_SUFFIXES = [
|
|
25
|
+
'.agentuity.com',
|
|
26
|
+
'.agentuity.dev',
|
|
27
|
+
'.agentuity.io',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if an origin is a trusted Agentuity app origin.
|
|
32
|
+
* Matches any HTTPS subdomain of the trusted domain suffixes.
|
|
33
|
+
*/
|
|
34
|
+
function isTrustedWorkbenchOrigin(origin: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(origin);
|
|
37
|
+
if (url.protocol !== 'https:') return false;
|
|
38
|
+
return TRUSTED_WORKBENCH_DOMAIN_SUFFIXES.some((suffix) => url.hostname.endsWith(suffix));
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Middleware that verifies workbench request signatures in production.
|
|
46
|
+
* In development mode, all requests are allowed.
|
|
47
|
+
* Supports both header-based auth (for HTTP) and query param auth (for WebSocket).
|
|
48
|
+
*/
|
|
49
|
+
const createWorkbenchAuthMiddleware = (): MiddlewareHandler => {
|
|
50
|
+
return async (c, next) => {
|
|
51
|
+
// Allow CORS preflight requests through (they don't have auth headers)
|
|
52
|
+
if (c.req.method === 'OPTIONS') {
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Skip auth in dev mode
|
|
57
|
+
if (!isProduction()) {
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check signature from headers or query params (for WebSocket)
|
|
62
|
+
const signature = c.req.header('X-Agentuity-Workbench-Signature') || c.req.query('signature');
|
|
63
|
+
const timestamp = c.req.header('X-Agentuity-Workbench-Timestamp') || c.req.query('timestamp');
|
|
64
|
+
|
|
65
|
+
// For non-POST requests, body is empty
|
|
66
|
+
let body = '';
|
|
67
|
+
if (c.req.method === 'POST') {
|
|
68
|
+
const clonedReq = c.req.raw.clone();
|
|
69
|
+
body = await clonedReq.text();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isValid = await verifySignature(signature, timestamp, body);
|
|
73
|
+
if (!isValid) {
|
|
74
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return next();
|
|
78
|
+
};
|
|
79
|
+
};
|
|
16
80
|
|
|
17
81
|
/**
|
|
18
82
|
* Middleware that captures execution metadata (tokens, duration, sessionId) after the handler completes
|
|
@@ -89,27 +153,7 @@ const createWorkbenchExecutionMetadataMiddleware = (): MiddlewareHandler => {
|
|
|
89
153
|
};
|
|
90
154
|
|
|
91
155
|
export const createWorkbenchExecutionRoute = (): Handler => {
|
|
92
|
-
const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
|
|
93
|
-
? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
|
|
94
|
-
: undefined;
|
|
95
156
|
return async (ctx: Context) => {
|
|
96
|
-
// Authentication check
|
|
97
|
-
if (authHeader) {
|
|
98
|
-
try {
|
|
99
|
-
const authValue = ctx.req.header('Authorization');
|
|
100
|
-
if (
|
|
101
|
-
!authValue ||
|
|
102
|
-
!timingSafeEqual(Buffer.from(authValue, 'utf-8'), Buffer.from(authHeader, 'utf-8'))
|
|
103
|
-
) {
|
|
104
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
// timing safe equals will throw if the input/output lengths are mismatched
|
|
108
|
-
// so we treat all exceptions as invalid
|
|
109
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
157
|
// Content-type validation
|
|
114
158
|
const contentType = ctx.req.header('Content-Type');
|
|
115
159
|
if (!contentType || !contentType.includes('application/json')) {
|
|
@@ -188,27 +232,7 @@ export const createWorkbenchExecutionRoute = (): Handler => {
|
|
|
188
232
|
};
|
|
189
233
|
|
|
190
234
|
export const createWorkbenchClearStateRoute = (): Handler => {
|
|
191
|
-
const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
|
|
192
|
-
? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
|
|
193
|
-
: undefined;
|
|
194
235
|
return async (ctx: Context) => {
|
|
195
|
-
// Authentication check
|
|
196
|
-
if (authHeader) {
|
|
197
|
-
try {
|
|
198
|
-
const authValue = ctx.req.header('Authorization');
|
|
199
|
-
if (
|
|
200
|
-
!authValue ||
|
|
201
|
-
!timingSafeEqual(Buffer.from(authValue, 'utf-8'), Buffer.from(authHeader, 'utf-8'))
|
|
202
|
-
) {
|
|
203
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
// timing safe equals will throw if the input/output lengths are mismatched
|
|
207
|
-
// so we treat all exceptions as invalid
|
|
208
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
236
|
const agentId = ctx.req.query('agentId');
|
|
213
237
|
|
|
214
238
|
if (!agentId) {
|
|
@@ -245,27 +269,7 @@ export const createWorkbenchClearStateRoute = (): Handler => {
|
|
|
245
269
|
};
|
|
246
270
|
|
|
247
271
|
export const createWorkbenchStateRoute = (): Handler => {
|
|
248
|
-
const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
|
|
249
|
-
? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
|
|
250
|
-
: undefined;
|
|
251
272
|
return async (ctx: Context) => {
|
|
252
|
-
// Authentication check
|
|
253
|
-
if (authHeader) {
|
|
254
|
-
try {
|
|
255
|
-
const authValue = ctx.req.header('Authorization');
|
|
256
|
-
if (
|
|
257
|
-
!authValue ||
|
|
258
|
-
!timingSafeEqual(Buffer.from(authValue, 'utf-8'), Buffer.from(authHeader, 'utf-8'))
|
|
259
|
-
) {
|
|
260
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
261
|
-
}
|
|
262
|
-
} catch {
|
|
263
|
-
// timing safe equals will throw if the input/output lengths are mismatched
|
|
264
|
-
// so we treat all exceptions as invalid
|
|
265
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
273
|
const agentId = ctx.req.query('agentId');
|
|
270
274
|
if (!agentId) {
|
|
271
275
|
return ctx.json({ error: 'agentId query parameter is required' }, { status: 400 });
|
|
@@ -290,31 +294,56 @@ export const createWorkbenchStateRoute = (): Handler => {
|
|
|
290
294
|
* Creates a workbench router with proper agent middleware for execution routes
|
|
291
295
|
*/
|
|
292
296
|
export const createWorkbenchRouter = () => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (config.headers?.['Authorization']) {
|
|
306
|
-
const authHeader = config.headers['Authorization'];
|
|
307
|
-
if (authHeader.startsWith('Bearer ')) {
|
|
308
|
-
const apiKey = authHeader.slice('Bearer '.length);
|
|
309
|
-
process.env.AGENTUITY_WORKBENCH_APIKEY = apiKey;
|
|
297
|
+
const router = createRouter();
|
|
298
|
+
|
|
299
|
+
// Apply CORS middleware first so that even error responses get CORS headers
|
|
300
|
+
// In production, restrict origins to known Agentuity app domains + same-origin
|
|
301
|
+
// In development, allow any origin for local testing flexibility
|
|
302
|
+
router.use(
|
|
303
|
+
'/_agentuity/workbench/*',
|
|
304
|
+
createCorsMiddleware({
|
|
305
|
+
origin: (origin: string, c) => {
|
|
306
|
+
// In dev mode, allow any origin for local testing flexibility
|
|
307
|
+
if (!isProduction()) {
|
|
308
|
+
return origin;
|
|
310
309
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
// In production, allow any *.agentuity.{com,dev,io} origin
|
|
311
|
+
if (isTrustedWorkbenchOrigin(origin)) {
|
|
312
|
+
return origin;
|
|
313
|
+
}
|
|
314
|
+
// Allow same-origin requests (agent calling its own workbench)
|
|
315
|
+
try {
|
|
316
|
+
const requestOrigin = new URL(c.req.url).origin;
|
|
317
|
+
if (origin === requestOrigin) {
|
|
318
|
+
return origin;
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// Invalid URL, reject
|
|
322
|
+
}
|
|
323
|
+
// Reject unknown origins — no Access-Control-Allow-Origin header
|
|
324
|
+
return undefined;
|
|
325
|
+
},
|
|
326
|
+
allowHeaders: [
|
|
327
|
+
'Content-Type',
|
|
328
|
+
'Authorization',
|
|
329
|
+
'Accept',
|
|
330
|
+
'Origin',
|
|
331
|
+
'X-Requested-With',
|
|
332
|
+
'X-Agentuity-Workbench-Signature',
|
|
333
|
+
'X-Agentuity-Workbench-Timestamp',
|
|
334
|
+
'x-thread-id',
|
|
335
|
+
],
|
|
336
|
+
exposeHeaders: [
|
|
337
|
+
'x-thread-id',
|
|
338
|
+
'x-session-id',
|
|
339
|
+
'x-agentuity-tokens',
|
|
340
|
+
'x-agentuity-duration',
|
|
341
|
+
],
|
|
342
|
+
})
|
|
343
|
+
);
|
|
316
344
|
|
|
317
|
-
|
|
345
|
+
// Apply auth middleware (signature verification in production)
|
|
346
|
+
router.use('/_agentuity/workbench/*', createWorkbenchAuthMiddleware());
|
|
318
347
|
|
|
319
348
|
// Apply agent middleware to ensure proper context is available
|
|
320
349
|
router.use('/_agentuity/workbench/*', createAgentMiddleware(''));
|
|
@@ -334,25 +363,7 @@ export const createWorkbenchRouter = () => {
|
|
|
334
363
|
};
|
|
335
364
|
|
|
336
365
|
export const createWorkbenchSampleRoute = (): Handler => {
|
|
337
|
-
const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
|
|
338
|
-
? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
|
|
339
|
-
: undefined;
|
|
340
366
|
return async (ctx: Context) => {
|
|
341
|
-
// Authentication check
|
|
342
|
-
if (authHeader) {
|
|
343
|
-
try {
|
|
344
|
-
const authValue = ctx.req.header('Authorization');
|
|
345
|
-
if (
|
|
346
|
-
!authValue ||
|
|
347
|
-
!timingSafeEqual(Buffer.from(authValue, 'utf-8'), Buffer.from(authHeader, 'utf-8'))
|
|
348
|
-
) {
|
|
349
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
350
|
-
}
|
|
351
|
-
} catch {
|
|
352
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
367
|
try {
|
|
357
368
|
const agentId = ctx.req.query('agentId');
|
|
358
369
|
if (!agentId) {
|
|
@@ -484,27 +495,7 @@ Return a JSON object that matches this schema with realistic values.`;
|
|
|
484
495
|
};
|
|
485
496
|
|
|
486
497
|
export const createWorkbenchMetadataRoute = (): Handler => {
|
|
487
|
-
const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
|
|
488
|
-
? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
|
|
489
|
-
: undefined;
|
|
490
|
-
|
|
491
498
|
return async (ctx) => {
|
|
492
|
-
if (authHeader) {
|
|
493
|
-
try {
|
|
494
|
-
const authValue = ctx.req.header('Authorization');
|
|
495
|
-
if (
|
|
496
|
-
!authValue ||
|
|
497
|
-
!timingSafeEqual(Buffer.from(authValue, 'utf-8'), Buffer.from(authHeader, 'utf-8'))
|
|
498
|
-
) {
|
|
499
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
500
|
-
}
|
|
501
|
-
} catch {
|
|
502
|
-
// timing safe equals will throw if the input/output lengths are mismatched
|
|
503
|
-
// so we treat all exceptions as invalid
|
|
504
|
-
return ctx.text('Unauthorized', { status: 401 });
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
499
|
// Read metadata from agentuity.metadata.json file
|
|
509
500
|
const metadata = loadBuildMetadata();
|
|
510
501
|
if (!metadata) {
|