agent-tool-forge 0.4.5 → 0.4.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/lib/auth.d.ts CHANGED
@@ -6,9 +6,11 @@ export interface AuthResult {
6
6
  }
7
7
 
8
8
  export interface AuthConfig {
9
- mode?: 'trust' | 'verify';
9
+ mode?: 'trust' | 'verify' | 'none';
10
10
  signingKey?: string;
11
11
  claimsPath?: string;
12
+ adminToken?: string | null;
13
+ metricsToken?: string | null;
12
14
  }
13
15
 
14
16
  export interface Authenticator {
@@ -22,4 +24,9 @@ export interface AdminAuthResult {
22
24
  error: string | null;
23
25
  }
24
26
 
25
- export function authenticateAdmin(req: object, adminKey: string): AdminAuthResult;
27
+ export function authenticateAdmin(req: object, adminKey: string | null): AdminAuthResult;
28
+
29
+ export function resolveSecret(
30
+ value: string | null | undefined,
31
+ env?: Record<string, string>
32
+ ): string | null;
package/lib/auth.js CHANGED
@@ -130,6 +130,21 @@ export function authenticateAdmin(req, adminKey) {
130
130
  return { authenticated: true, error: null };
131
131
  }
132
132
 
133
+ /**
134
+ * Resolve a secret value, expanding ${VAR} references against the given env object.
135
+ * @param {string|null|undefined} value
136
+ * @param {Record<string, string>} [env]
137
+ * @returns {string|null}
138
+ */
139
+ export function resolveSecret(value, env = {}) {
140
+ if (typeof value !== 'string' || !value) return null;
141
+ const e = env ?? {};
142
+ if (value.startsWith('${') && value.endsWith('}')) {
143
+ return e[value.slice(2, -1)] ?? null;
144
+ }
145
+ return value;
146
+ }
147
+
133
148
  // ── Helpers ──────────────────────────────────────────────────────────────────
134
149
 
135
150
  function base64UrlDecode(str) {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export const CONFIG_DEFAULTS = {
9
- auth: { mode: 'trust', signingKey: null, claimsPath: 'sub' },
9
+ auth: { mode: 'trust', signingKey: null, claimsPath: 'sub', adminToken: null, metricsToken: null },
10
10
  defaultModel: 'claude-sonnet-4-6',
11
11
  defaultHitlLevel: 'cautious',
12
12
  allowUserModelSelect: false,
@@ -47,7 +47,7 @@ export const CONFIG_DEFAULTS = {
47
47
  }
48
48
  };
49
49
 
50
- const VALID_AUTH_MODES = ['verify', 'trust'];
50
+ const VALID_AUTH_MODES = ['verify', 'trust', 'none'];
51
51
  const VALID_HITL_LEVELS = ['autonomous', 'cautious', 'standard', 'paranoid'];
52
52
  const VALID_STORE_TYPES = ['sqlite', 'redis', 'postgres'];
53
53
  const VALID_DB_TYPES = ['sqlite', 'postgres'];
@@ -28,7 +28,7 @@ import { getDb } from './db.js';
28
28
  import { createMcpServer } from './mcp-server.js';
29
29
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
30
30
  import { mergeDefaults } from './config-schema.js';
31
- import { createAuth } from './auth.js';
31
+ import { createAuth, resolveSecret, authenticateAdmin } from './auth.js';
32
32
  import { makePromptStore } from './prompt-store.js';
33
33
  import { makePreferenceStore } from './preference-store.js';
34
34
  import { makeConversationStore } from './conversation-store.js';
@@ -200,6 +200,53 @@ function serveWidgetFile(req, res, widgetDir, errorFn) {
200
200
  }
201
201
  }
202
202
 
203
+ /**
204
+ * Determine the auth tier for a given request path.
205
+ * 0 = open, 1 = app (JWT), 2 = admin (Bearer token), 3 = scrape (metrics token)
206
+ * @param {string} path — normalized sidecarPath
207
+ * @returns {0|1|2|3}
208
+ */
209
+ function getRouteTier(path) {
210
+ if (path === '/health') return 0;
211
+ if (path.startsWith('/forge-admin/') || path.startsWith('/agent-api/evals/')) return 2;
212
+ if (path === '/metrics') return 3;
213
+ return 1; // widget, mcp, agent-api/* → app tier
214
+ }
215
+
216
+ /**
217
+ * Apply tier-based auth to a request.
218
+ * @param {import('http').IncomingMessage} req
219
+ * @param {object} ctx — sidecar context
220
+ * @param {0|1|2|3} tier
221
+ * @returns {{ ok: boolean, status?: number, error?: string, userId?: string, claims?: object }}
222
+ */
223
+ function applyRouteAuth(req, ctx, tier) {
224
+ const { config, env, auth } = ctx;
225
+ if (config?.auth?.mode === 'none') return { ok: true };
226
+ if (tier === 0) return { ok: true };
227
+
228
+ if (tier === 2) {
229
+ const token = resolveSecret(config?.auth?.adminToken, env)
230
+ ?? resolveSecret(config?.adminKey, env);
231
+ if (!token) return { ok: false, status: 503, error: 'Admin credentials not configured' };
232
+ const r = authenticateAdmin(req, token);
233
+ return r.authenticated ? { ok: true } : { ok: false, status: 401, error: r.error };
234
+ }
235
+
236
+ if (tier === 3) {
237
+ const token = resolveSecret(config?.auth?.metricsToken, env);
238
+ if (!token) return { ok: true }; // open when metricsToken not set
239
+ const r = authenticateAdmin(req, token);
240
+ return r.authenticated ? { ok: true } : { ok: false, status: 401, error: r.error };
241
+ }
242
+
243
+ // tier 1 — JWT
244
+ const r = auth.authenticate(req);
245
+ return r.authenticated
246
+ ? { ok: true, userId: r.userId, claims: r.claims }
247
+ : { ok: false, status: 401, error: r.error };
248
+ }
249
+
203
250
  /**
204
251
  * Create an HTTP request handler for all sidecar routes.
205
252
  *
@@ -219,23 +266,31 @@ export function createSidecarRouter(ctx, options = {}) {
219
266
  return async (req, res) => {
220
267
  const url = new URL(req.url, 'http://localhost');
221
268
 
269
+ // ── Normalise /agent-api/v1/* → /agent-api/* ──────────────────────────
270
+ const sidecarPath = url.pathname.startsWith('/agent-api/v1/')
271
+ ? '/agent-api/' + url.pathname.slice('/agent-api/v1/'.length)
272
+ : url.pathname;
273
+
274
+ // ── Auth gate ──────────────────────────────────────────────────────────
275
+ const tier = getRouteTier(sidecarPath);
276
+ const authCheck = applyRouteAuth(req, ctx, tier);
277
+ if (!authCheck.ok) {
278
+ if (authCheck.status === 401) res.setHeader('WWW-Authenticate', 'Bearer');
279
+ return sendJson(res, authCheck.status, { error: authCheck.error });
280
+ }
281
+
222
282
  // ── /mcp route (optional) ──────────────────────────────────────────────
223
- if (mcpHandler && url.pathname.startsWith('/mcp')) {
283
+ if (mcpHandler && sidecarPath.startsWith('/mcp')) {
224
284
  return mcpHandler(req, res);
225
285
  }
226
286
 
227
287
  // ── /health ────────────────────────────────────────────────────────────
228
- if (req.method === 'GET' && url.pathname === '/health') {
288
+ if (req.method === 'GET' && sidecarPath === '/health') {
229
289
  sendJson(res, 200, { status: 'ok' });
230
290
  return;
231
291
  }
232
292
 
233
293
  // ── Sidecar API routes ─────────────────────────────────────────────────
234
- // Normalise /agent-api/v1/* → /agent-api/* so versioned paths hit
235
- // the same handlers without a proxy rewrite rule.
236
- const sidecarPath = url.pathname.startsWith('/agent-api/v1/')
237
- ? '/agent-api/' + url.pathname.slice('/agent-api/v1/'.length)
238
- : url.pathname;
239
294
 
240
295
  if (sidecarPath === '/agent-api/chat' && req.method === 'POST') {
241
296
  return handleChat(req, res, ctx);
@@ -4,13 +4,12 @@
4
4
  * PUT /forge-admin/config/:section — update a config section
5
5
  * GET /forge-admin/config — read current effective config
6
6
  *
7
- * Protected by adminKey (Bearer token).
7
+ * Protected by router-level admin auth (tier 2 — see forge-service.js).
8
8
  * Runtime overlay: in-memory Map merged on top of file config.
9
9
  * NOT written back to forge.config.json.
10
10
  */
11
11
 
12
12
  import { readFileSync, writeFileSync, renameSync } from 'fs';
13
- import { authenticateAdmin } from '../auth.js';
14
13
  import { readBody, sendJson } from '../http-utils.js';
15
14
 
16
15
  const VALID_SECTIONS = ['model', 'hitl', 'permissions', 'conversation'];
@@ -24,18 +23,6 @@ const runtimeOverlay = new Map();
24
23
  export async function handleAdminConfig(req, res, ctx) {
25
24
  const url = new URL(req.url, 'http://localhost');
26
25
 
27
- // Admin auth
28
- const adminKey = ctx.config.adminKey;
29
- if (!adminKey) {
30
- sendJson(res, 503, { error: 'No adminKey configured' });
31
- return;
32
- }
33
- const authResult = authenticateAdmin(req, adminKey);
34
- if (!authResult.authenticated) {
35
- sendJson(res, 403, { error: authResult.error ?? 'Forbidden' });
36
- return;
37
- }
38
-
39
26
  if (req.method === 'GET') {
40
27
  return handleAdminConfigGet(req, res, ctx);
41
28
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Admin Agent API — CRUD for multi-agent registry.
3
3
  *
4
- * All routes require adminKey (Bearer token).
4
+ * All routes protected by router-level admin auth (tier 2 — see forge-service.js).
5
5
  *
6
6
  * Routes:
7
7
  * GET /forge-admin/agents — list all agents
@@ -12,7 +12,6 @@
12
12
  * POST /forge-admin/agents/:agentId/set-default — set default
13
13
  */
14
14
 
15
- import { authenticateAdmin } from '../auth.js';
16
15
  import { readBody, sendJson } from '../http-utils.js';
17
16
 
18
17
  const AGENT_ID_RE = /^[a-z0-9_-]{1,64}$/;
@@ -24,20 +23,7 @@ const VALID_HITL_LEVELS = new Set(['autonomous', 'cautious', 'standard', 'parano
24
23
  * @param {object} ctx — { config, agentRegistry }
25
24
  */
26
25
  export async function handleAgents(req, res, ctx) {
27
- const { config, agentRegistry } = ctx;
28
-
29
- // Admin auth
30
- const adminKey = config.adminKey;
31
- if (!adminKey) {
32
- sendJson(res, 503, { error: 'No adminKey configured' });
33
- return;
34
- }
35
- const authResult = authenticateAdmin(req, adminKey);
36
- if (!authResult.authenticated) {
37
- res.setHeader('WWW-Authenticate', 'Bearer');
38
- sendJson(res, 401, { error: 'Unauthorized' });
39
- return;
40
- }
26
+ const { agentRegistry } = ctx;
41
27
 
42
28
  if (!agentRegistry) {
43
29
  sendJson(res, 501, { error: 'Agent registry not initialized' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tool-forge",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Production LLM agent sidecar + Claude Code skill library for building, testing, and running tool-calling agents.",
5
5
  "keywords": [
6
6
  "llm",