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 +9 -2
- package/lib/auth.js +15 -0
- package/lib/config-schema.js +2 -2
- package/lib/forge-service.js +63 -8
- package/lib/handlers/admin.js +1 -14
- package/lib/handlers/agents.js +2 -16
- package/package.json +1 -1
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) {
|
package/lib/config-schema.js
CHANGED
|
@@ -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'];
|
package/lib/forge-service.js
CHANGED
|
@@ -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 &&
|
|
283
|
+
if (mcpHandler && sidecarPath.startsWith('/mcp')) {
|
|
224
284
|
return mcpHandler(req, res);
|
|
225
285
|
}
|
|
226
286
|
|
|
227
287
|
// ── /health ────────────────────────────────────────────────────────────
|
|
228
|
-
if (req.method === 'GET' &&
|
|
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);
|
package/lib/handlers/admin.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/handlers/agents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Admin Agent API — CRUD for multi-agent registry.
|
|
3
3
|
*
|
|
4
|
-
* All routes
|
|
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 {
|
|
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' });
|