@agentuity/runtime 1.0.4 → 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/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
- // Try to extract API key from inline workbench config if available
294
- try {
295
- // @ts-expect-error - AGENTUITY_WORKBENCH_CONFIG_INLINE will be replaced at build time
296
- if (typeof AGENTUITY_WORKBENCH_CONFIG_INLINE !== 'undefined') {
297
- // @ts-expect-error - AGENTUITY_WORKBENCH_CONFIG_INLINE will be replaced at build time
298
- const encoded = AGENTUITY_WORKBENCH_CONFIG_INLINE;
299
-
300
- // Decode the config manually to avoid async import
301
- const json = Buffer.from(encoded, 'base64').toString('utf-8');
302
- const config = JSON.parse(json);
303
-
304
- // Extract API key from Authorization header if present
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
- } catch {
314
- // Silently ignore if config is not available or invalid
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
- const router = createRouter();
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) {