@agentuity/runtime 1.0.5 → 1.0.7

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,67 @@ 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 = ['.agentuity.com', '.agentuity.dev', '.agentuity.io'];
25
+
26
+ /**
27
+ * Check if an origin is a trusted Agentuity app origin.
28
+ * Matches any HTTPS subdomain of the trusted domain suffixes.
29
+ */
30
+ function isTrustedWorkbenchOrigin(origin: string): boolean {
31
+ try {
32
+ const url = new URL(origin);
33
+ if (url.protocol !== 'https:') return false;
34
+ return TRUSTED_WORKBENCH_DOMAIN_SUFFIXES.some((suffix) => url.hostname.endsWith(suffix));
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Middleware that verifies workbench request signatures in production.
42
+ * In development mode, all requests are allowed.
43
+ * Supports both header-based auth (for HTTP) and query param auth (for WebSocket).
44
+ */
45
+ const createWorkbenchAuthMiddleware = (): MiddlewareHandler => {
46
+ return async (c, next) => {
47
+ // Allow CORS preflight requests through (they don't have auth headers)
48
+ if (c.req.method === 'OPTIONS') {
49
+ return next();
50
+ }
51
+
52
+ // Skip auth in dev mode
53
+ if (!isProduction()) {
54
+ return next();
55
+ }
56
+
57
+ // Check signature from headers or query params (for WebSocket)
58
+ const signature = c.req.header('X-Agentuity-Workbench-Signature') || c.req.query('signature');
59
+ const timestamp = c.req.header('X-Agentuity-Workbench-Timestamp') || c.req.query('timestamp');
60
+
61
+ // For non-POST requests, body is empty
62
+ let body = '';
63
+ if (c.req.method === 'POST') {
64
+ const clonedReq = c.req.raw.clone();
65
+ body = await clonedReq.text();
66
+ }
67
+
68
+ const isValid = await verifySignature(signature, timestamp, body);
69
+ if (!isValid) {
70
+ return c.json({ error: 'Unauthorized' }, 401);
71
+ }
72
+
73
+ return next();
74
+ };
75
+ };
16
76
 
17
77
  /**
18
78
  * Middleware that captures execution metadata (tokens, duration, sessionId) after the handler completes
@@ -89,27 +149,7 @@ const createWorkbenchExecutionMetadataMiddleware = (): MiddlewareHandler => {
89
149
  };
90
150
 
91
151
  export const createWorkbenchExecutionRoute = (): Handler => {
92
- const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
93
- ? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
94
- : undefined;
95
152
  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
153
  // Content-type validation
114
154
  const contentType = ctx.req.header('Content-Type');
115
155
  if (!contentType || !contentType.includes('application/json')) {
@@ -188,27 +228,7 @@ export const createWorkbenchExecutionRoute = (): Handler => {
188
228
  };
189
229
 
190
230
  export const createWorkbenchClearStateRoute = (): Handler => {
191
- const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
192
- ? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
193
- : undefined;
194
231
  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
232
  const agentId = ctx.req.query('agentId');
213
233
 
214
234
  if (!agentId) {
@@ -245,27 +265,7 @@ export const createWorkbenchClearStateRoute = (): Handler => {
245
265
  };
246
266
 
247
267
  export const createWorkbenchStateRoute = (): Handler => {
248
- const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
249
- ? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
250
- : undefined;
251
268
  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
269
  const agentId = ctx.req.query('agentId');
270
270
  if (!agentId) {
271
271
  return ctx.json({ error: 'agentId query parameter is required' }, { status: 400 });
@@ -290,31 +290,56 @@ export const createWorkbenchStateRoute = (): Handler => {
290
290
  * Creates a workbench router with proper agent middleware for execution routes
291
291
  */
292
292
  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;
293
+ const router = createRouter();
294
+
295
+ // Apply CORS middleware first so that even error responses get CORS headers
296
+ // In production, restrict origins to known Agentuity app domains + same-origin
297
+ // In development, allow any origin for local testing flexibility
298
+ router.use(
299
+ '/_agentuity/workbench/*',
300
+ createCorsMiddleware({
301
+ origin: (origin: string, c) => {
302
+ // In dev mode, allow any origin for local testing flexibility
303
+ if (!isProduction()) {
304
+ return origin;
310
305
  }
311
- }
312
- }
313
- } catch {
314
- // Silently ignore if config is not available or invalid
315
- }
306
+ // In production, allow any *.agentuity.{com,dev,io} origin
307
+ if (isTrustedWorkbenchOrigin(origin)) {
308
+ return origin;
309
+ }
310
+ // Allow same-origin requests (agent calling its own workbench)
311
+ try {
312
+ const requestOrigin = new URL(c.req.url).origin;
313
+ if (origin === requestOrigin) {
314
+ return origin;
315
+ }
316
+ } catch {
317
+ // Invalid URL, reject
318
+ }
319
+ // Reject unknown origins — no Access-Control-Allow-Origin header
320
+ return undefined;
321
+ },
322
+ allowHeaders: [
323
+ 'Content-Type',
324
+ 'Authorization',
325
+ 'Accept',
326
+ 'Origin',
327
+ 'X-Requested-With',
328
+ 'X-Agentuity-Workbench-Signature',
329
+ 'X-Agentuity-Workbench-Timestamp',
330
+ 'x-thread-id',
331
+ ],
332
+ exposeHeaders: [
333
+ 'x-thread-id',
334
+ 'x-session-id',
335
+ 'x-agentuity-tokens',
336
+ 'x-agentuity-duration',
337
+ ],
338
+ })
339
+ );
316
340
 
317
- const router = createRouter();
341
+ // Apply auth middleware (signature verification in production)
342
+ router.use('/_agentuity/workbench/*', createWorkbenchAuthMiddleware());
318
343
 
319
344
  // Apply agent middleware to ensure proper context is available
320
345
  router.use('/_agentuity/workbench/*', createAgentMiddleware(''));
@@ -334,25 +359,7 @@ export const createWorkbenchRouter = () => {
334
359
  };
335
360
 
336
361
  export const createWorkbenchSampleRoute = (): Handler => {
337
- const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
338
- ? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
339
- : undefined;
340
362
  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
363
  try {
357
364
  const agentId = ctx.req.query('agentId');
358
365
  if (!agentId) {
@@ -484,27 +491,7 @@ Return a JSON object that matches this schema with realistic values.`;
484
491
  };
485
492
 
486
493
  export const createWorkbenchMetadataRoute = (): Handler => {
487
- const authHeader = process.env.AGENTUITY_WORKBENCH_APIKEY
488
- ? `Bearer ${process.env.AGENTUITY_WORKBENCH_APIKEY}`
489
- : undefined;
490
-
491
494
  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
495
  // Read metadata from agentuity.metadata.json file
509
496
  const metadata = loadBuildMetadata();
510
497
  if (!metadata) {