agent-tool-forge 0.4.5 → 0.4.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/README.md +16 -16
- package/config/api-endpoints.template.json +17 -0
- package/config/forge.config.template.json +106 -0
- package/lib/auth.d.ts +9 -2
- package/lib/auth.js +15 -0
- package/lib/config-schema.js +3 -3
- package/lib/config.d.ts +33 -4
- package/lib/forge-service.js +88 -13
- package/lib/handlers/admin.js +1 -14
- package/lib/handlers/agents.js +2 -16
- package/lib/hitl-engine.d.ts +8 -2
- package/lib/sidecar.d.ts +12 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -40,10 +40,10 @@ See [docs/tui-workflow.md](docs/tui-workflow.md) for a start-to-finish walkthrou
|
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
# Global install (available in all projects)
|
|
43
|
-
cp -r tool-forge/skills/forge-tool ~/.claude/skills/
|
|
44
|
-
cp -r tool-forge/skills/forge-eval ~/.claude/skills/
|
|
45
|
-
cp -r tool-forge/skills/forge-mcp ~/.claude/skills/
|
|
46
|
-
cp -r tool-forge/skills/forge-verifier ~/.claude/skills/
|
|
43
|
+
cp -r node_modules/agent-tool-forge/skills/forge-tool ~/.claude/skills/
|
|
44
|
+
cp -r node_modules/agent-tool-forge/skills/forge-eval ~/.claude/skills/
|
|
45
|
+
cp -r node_modules/agent-tool-forge/skills/forge-mcp ~/.claude/skills/
|
|
46
|
+
cp -r node_modules/agent-tool-forge/skills/forge-verifier ~/.claude/skills/
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Then in any Claude Code session:
|
|
@@ -123,23 +123,23 @@ All subpaths ship with TypeScript declarations.
|
|
|
123
123
|
|
|
124
124
|
```js
|
|
125
125
|
import { createSidecar } from 'agent-tool-forge' // main entry
|
|
126
|
-
import { reactLoop } from 'tool-forge/react-engine'
|
|
127
|
-
import { createAuth } from 'tool-forge/auth'
|
|
128
|
-
import { makeConversationStore } from 'tool-forge/conversation-store'
|
|
129
|
-
import { mergeDefaults } from 'tool-forge/config'
|
|
130
|
-
import { makeHitlEngine } from 'tool-forge/hitl-engine'
|
|
131
|
-
import { makePromptStore } from 'tool-forge/prompt-store'
|
|
132
|
-
import { makePreferenceStore } from 'tool-forge/preference-store'
|
|
133
|
-
import { makeRateLimiter } from 'tool-forge/rate-limiter'
|
|
134
|
-
import { getDb } from 'tool-forge/db'
|
|
135
|
-
import { initSSE } from 'tool-forge/sse'
|
|
126
|
+
import { reactLoop } from 'agent-tool-forge/react-engine'
|
|
127
|
+
import { createAuth } from 'agent-tool-forge/auth'
|
|
128
|
+
import { makeConversationStore } from 'agent-tool-forge/conversation-store'
|
|
129
|
+
import { mergeDefaults } from 'agent-tool-forge/config'
|
|
130
|
+
import { makeHitlEngine } from 'agent-tool-forge/hitl-engine'
|
|
131
|
+
import { makePromptStore } from 'agent-tool-forge/prompt-store'
|
|
132
|
+
import { makePreferenceStore } from 'agent-tool-forge/preference-store'
|
|
133
|
+
import { makeRateLimiter } from 'agent-tool-forge/rate-limiter'
|
|
134
|
+
import { getDb } from 'agent-tool-forge/db'
|
|
135
|
+
import { initSSE } from 'agent-tool-forge/sse'
|
|
136
136
|
import {
|
|
137
137
|
PostgresStore,
|
|
138
138
|
PostgresEvalStore,
|
|
139
139
|
PostgresChatAuditStore,
|
|
140
140
|
PostgresVerifierStore
|
|
141
|
-
} from 'tool-forge/postgres-store'
|
|
142
|
-
import { buildSidecarContext, createSidecarRouter } from 'tool-forge/forge-service'
|
|
141
|
+
} from 'agent-tool-forge/postgres-store'
|
|
142
|
+
import { buildSidecarContext, createSidecarRouter } from 'agent-tool-forge/forge-service'
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
---
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"_comment": "Manual endpoint manifest. Add endpoints here when OpenAPI discovery is unavailable. Forge uses this to propose tools.",
|
|
4
|
+
"baseUrl": "${API_BASE_URL}",
|
|
5
|
+
"endpoints": [
|
|
6
|
+
{
|
|
7
|
+
"path": "/api/v1/example",
|
|
8
|
+
"method": "GET",
|
|
9
|
+
"name": "get_example",
|
|
10
|
+
"description": "Retrieves example data from the API. Use when the user asks for examples.",
|
|
11
|
+
"params": {
|
|
12
|
+
"id": { "type": "string", "description": "Optional filter by ID" }
|
|
13
|
+
},
|
|
14
|
+
"requiresConfirmation": false
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"_comment": "Optional configuration that front-loads answers to common skill questions. Delete fields you don't need — all are optional. The skills work via dialogue alone without this file.",
|
|
4
|
+
|
|
5
|
+
"project": {
|
|
6
|
+
"name": "my-project",
|
|
7
|
+
"toolsDir": "src/tools",
|
|
8
|
+
"testsDir": "src/tools/__tests__",
|
|
9
|
+
"evalsDir": "evals/dataset",
|
|
10
|
+
"barrelsFile": "src/tools/tools.exports.ts"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"api": {
|
|
14
|
+
"baseUrl": "http://localhost:3000",
|
|
15
|
+
"_baseUrlComment": "Base URL for MCP tool routing. Tool mcpRouting.endpoint paths are appended to this.",
|
|
16
|
+
"discovery": {
|
|
17
|
+
"type": "openapi",
|
|
18
|
+
"url": "http://localhost:3333/api-json",
|
|
19
|
+
"_comment": "Or file: { \"type\": \"openapi\", \"file\": \"openapi.json\" }"
|
|
20
|
+
},
|
|
21
|
+
"manifestPath": "api-endpoints.json"
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"language": "typescript",
|
|
25
|
+
|
|
26
|
+
"validation": {
|
|
27
|
+
"library": "zod",
|
|
28
|
+
"_alternatives": ["pydantic", "joi", "json-schema", "struct-tags"]
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"testing": {
|
|
32
|
+
"framework": "jest",
|
|
33
|
+
"_alternatives": ["vitest", "pytest", "go-test", "mocha"],
|
|
34
|
+
"command": "npx jest --passWithNoTests"
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"typeCheck": {
|
|
38
|
+
"command": "npx tsc --noEmit",
|
|
39
|
+
"_comment": "Set to null if your stack doesn't have a type checker"
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
"auth": {
|
|
43
|
+
"contextField": "context.auth",
|
|
44
|
+
"type": "jwt",
|
|
45
|
+
"_alternatives": ["api-key", "oauth", "service-account"]
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"client": {
|
|
49
|
+
"contextField": "context.client",
|
|
50
|
+
"type": "http",
|
|
51
|
+
"_comment": "The API client your tools use. Could be HTTP, gRPC, SDK wrapper, etc."
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"hitl": {
|
|
55
|
+
"enabled": false,
|
|
56
|
+
"framework": null,
|
|
57
|
+
"_comment": "Set to true and specify framework (e.g., 'langgraph') if you use human-in-the-loop confirmation for write tools"
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"mcp": {
|
|
61
|
+
"defaultTransport": "stdio",
|
|
62
|
+
"_alternatives": ["streamable-http"],
|
|
63
|
+
"serverPrefix": "my-project",
|
|
64
|
+
"_comment": "Used by /forge-mcp to name the generated MCP server"
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"evals": {
|
|
68
|
+
"goldenDir": "evals/dataset/golden",
|
|
69
|
+
"labeledDir": "evals/dataset/labeled",
|
|
70
|
+
"overlapMapFile": "evals/tool-overlap-map.json",
|
|
71
|
+
"seedManifestFile": "evals/seed-manifest.json",
|
|
72
|
+
"_comment": "Paths are relative to project root",
|
|
73
|
+
"defaultMix": {
|
|
74
|
+
"golden": { "total": 10 },
|
|
75
|
+
"labeled": { "straightforward": 3, "ambiguous": 3, "edge": 2, "adversarial": 2 }
|
|
76
|
+
},
|
|
77
|
+
"multiPass": { "passes": 3 },
|
|
78
|
+
"randomSample": { "aggression": "standard" }
|
|
79
|
+
},
|
|
80
|
+
"drift": {
|
|
81
|
+
"threshold": 0.1,
|
|
82
|
+
"windowSize": 5
|
|
83
|
+
},
|
|
84
|
+
"modelMatrix": [],
|
|
85
|
+
"_modelMatrixComment": "Add model names to compare during eval runs, e.g. ['gpt-4o-mini', 'gemini-2.0-flash', 'claude-haiku-4-5-20251001']",
|
|
86
|
+
"costs": {
|
|
87
|
+
"claude-haiku-4-5-20251001": { "input": 0.80, "output": 4.00 },
|
|
88
|
+
"claude-sonnet-4-6": { "input": 3.00, "output": 15.00 },
|
|
89
|
+
"claude-opus-4-6": { "input": 15.00, "output": 75.00 },
|
|
90
|
+
"gpt-4o": { "input": 2.50, "output": 10.00 },
|
|
91
|
+
"gpt-4o-mini": { "input": 0.15, "output": 0.60 },
|
|
92
|
+
"o1": { "input": 15.00, "output": 60.00 },
|
|
93
|
+
"o3-mini": { "input": 1.10, "output": 4.40 },
|
|
94
|
+
"gemini-2.0-flash": { "input": 0.10, "output": 0.40 },
|
|
95
|
+
"gemini-2.5-pro-exp": { "input": 1.25, "output": 10.00 },
|
|
96
|
+
"deepseek-chat": { "input": 0.27, "output": 1.10 }
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
"verification": {
|
|
100
|
+
"enabled": true,
|
|
101
|
+
"verifiersDir": "src/verification",
|
|
102
|
+
"barrelsFile": "src/verification/verifiers.exports.ts",
|
|
103
|
+
"orderPrefix": "A-",
|
|
104
|
+
"_comment": "Order categories: A=attribution, C=compliance, I=interface, R=risk, U=uncertainty"
|
|
105
|
+
}
|
|
106
|
+
}
|
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,
|
|
@@ -14,7 +14,7 @@ export const CONFIG_DEFAULTS = {
|
|
|
14
14
|
adminKey: null,
|
|
15
15
|
database: { type: 'sqlite', url: null },
|
|
16
16
|
conversation: { store: 'sqlite', window: 25, redis: {} },
|
|
17
|
-
sidecar: {
|
|
17
|
+
sidecar: { port: 8001 }, // port: used in direct-run mode only (node lib/forge-service.js)
|
|
18
18
|
agents: [],
|
|
19
19
|
rateLimit: {
|
|
20
20
|
enabled: 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/config.d.ts
CHANGED
|
@@ -23,9 +23,13 @@ export interface DatabaseConfig {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface AuthConfig {
|
|
26
|
-
mode?: 'trust' | 'verify';
|
|
27
|
-
signingKey?: string;
|
|
26
|
+
mode?: 'trust' | 'verify' | 'none';
|
|
27
|
+
signingKey?: string | null;
|
|
28
28
|
claimsPath?: string;
|
|
29
|
+
/** Admin Bearer token. Replaces top-level `adminKey`. Supports `${VAR}` env references. */
|
|
30
|
+
adminToken?: string | null;
|
|
31
|
+
/** Metrics scrape token for /metrics. Supports `${VAR}` env references. */
|
|
32
|
+
metricsToken?: string | null;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface AgentConfig {
|
|
@@ -43,6 +47,26 @@ export interface AgentConfig {
|
|
|
43
47
|
enabled?: number;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
export interface AgentRouterConfig {
|
|
51
|
+
endpoint?: string | null;
|
|
52
|
+
method?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
inputField?: string;
|
|
55
|
+
outputField?: string;
|
|
56
|
+
sessionField?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GatesConfig {
|
|
60
|
+
passRate?: number | null;
|
|
61
|
+
maxCost?: number | null;
|
|
62
|
+
p95LatencyMs?: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FixturesConfig {
|
|
66
|
+
dir?: string;
|
|
67
|
+
ttlDays?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
export interface SidecarConfig {
|
|
47
71
|
auth?: AuthConfig;
|
|
48
72
|
defaultModel?: string;
|
|
@@ -50,14 +74,19 @@ export interface SidecarConfig {
|
|
|
50
74
|
allowUserModelSelect?: boolean;
|
|
51
75
|
allowUserHitlConfig?: boolean;
|
|
52
76
|
systemPrompt?: string;
|
|
53
|
-
|
|
77
|
+
/** @deprecated Use `auth.adminToken` instead. */
|
|
78
|
+
adminKey?: string | null;
|
|
54
79
|
conversation?: ConversationConfig;
|
|
55
80
|
rateLimit?: RateLimitConfig;
|
|
56
81
|
verification?: VerificationConfig;
|
|
57
82
|
database?: DatabaseConfig;
|
|
58
|
-
|
|
83
|
+
/** `port` is used in direct-run mode only (`node lib/forge-service.js`). `createSidecar()` uses `SidecarOptions.port`. */
|
|
84
|
+
sidecar?: { port?: number };
|
|
59
85
|
agents?: AgentConfig[];
|
|
60
86
|
costs?: Record<string, { input: number; output: number }>;
|
|
87
|
+
agent?: AgentRouterConfig;
|
|
88
|
+
gates?: GatesConfig;
|
|
89
|
+
fixtures?: FixturesConfig;
|
|
61
90
|
}
|
|
62
91
|
|
|
63
92
|
export const CONFIG_DEFAULTS: SidecarConfig;
|
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';
|
|
@@ -67,7 +67,14 @@ const PROJECT_ROOT = resolve(__dirname, '..');
|
|
|
67
67
|
* @returns {Promise<{ auth, promptStore, preferenceStore, conversationStore, hitlEngine, verifierRunner, agentRegistry, db, config, env, rateLimiter, configPath, evalStore, chatAuditStore, verifierStore, pgStore, _redisClient, _pgPool }>}
|
|
68
68
|
*/
|
|
69
69
|
export async function buildSidecarContext(config, db, env = {}, opts = {}) {
|
|
70
|
-
|
|
70
|
+
// Resolve ${VAR} references in auth token fields at startup, not per-request
|
|
71
|
+
const resolvedAuth = config.auth ? {
|
|
72
|
+
...config.auth,
|
|
73
|
+
signingKey: resolveSecret(config.auth.signingKey, env) ?? config.auth.signingKey ?? null,
|
|
74
|
+
adminToken: resolveSecret(config.auth.adminToken, env),
|
|
75
|
+
metricsToken: resolveSecret(config.auth.metricsToken, env),
|
|
76
|
+
} : config.auth;
|
|
77
|
+
const auth = createAuth(resolvedAuth);
|
|
71
78
|
|
|
72
79
|
let redisClient = null;
|
|
73
80
|
let pgPool = null;
|
|
@@ -103,6 +110,7 @@ export async function buildSidecarContext(config, db, env = {}, opts = {}) {
|
|
|
103
110
|
idleTimeoutMillis: 30000,
|
|
104
111
|
max: 10
|
|
105
112
|
});
|
|
113
|
+
pgPool.on('error', err => process.stderr.write(`[forge] pg pool error: ${err.message}\n`));
|
|
106
114
|
await pgPool.query(SCHEMA); // ensure all tables exist
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -142,9 +150,14 @@ export async function buildSidecarContext(config, db, env = {}, opts = {}) {
|
|
|
142
150
|
// project directory, not into the installed package.
|
|
143
151
|
const configPath = opts?.configPath ?? resolve(process.cwd(), 'forge.config.json');
|
|
144
152
|
|
|
153
|
+
// Return resolved auth config so applyRouteAuth sees literal tokens (not ${VAR})
|
|
154
|
+
const resolvedConfig = resolvedAuth !== config.auth
|
|
155
|
+
? { ...config, auth: resolvedAuth }
|
|
156
|
+
: config;
|
|
157
|
+
|
|
145
158
|
return {
|
|
146
159
|
auth, promptStore, preferenceStore, conversationStore, hitlEngine, verifierRunner,
|
|
147
|
-
agentRegistry, db, config, env, rateLimiter, configPath,
|
|
160
|
+
agentRegistry, db, config: resolvedConfig, env, rateLimiter, configPath,
|
|
148
161
|
evalStore, chatAuditStore, verifierStore, pgStore,
|
|
149
162
|
_redisClient: redisClient, _pgPool: pgPool
|
|
150
163
|
};
|
|
@@ -200,6 +213,53 @@ function serveWidgetFile(req, res, widgetDir, errorFn) {
|
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Determine the auth tier for a given request path.
|
|
218
|
+
* 0 = open, 1 = app (JWT), 2 = admin (Bearer token), 3 = scrape (metrics token)
|
|
219
|
+
* @param {string} path — normalized sidecarPath
|
|
220
|
+
* @returns {0|1|2|3}
|
|
221
|
+
*/
|
|
222
|
+
function getRouteTier(path) {
|
|
223
|
+
if (path === '/health') return 0;
|
|
224
|
+
if (path.startsWith('/forge-admin/') || path.startsWith('/agent-api/evals/')) return 2;
|
|
225
|
+
if (path === '/metrics') return 3;
|
|
226
|
+
return 1; // widget, mcp, agent-api/* → app tier
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Apply tier-based auth to a request.
|
|
231
|
+
* @param {import('http').IncomingMessage} req
|
|
232
|
+
* @param {object} ctx — sidecar context
|
|
233
|
+
* @param {0|1|2|3} tier
|
|
234
|
+
* @returns {{ ok: boolean, status?: number, error?: string, userId?: string, claims?: object }}
|
|
235
|
+
*/
|
|
236
|
+
function applyRouteAuth(req, ctx, tier) {
|
|
237
|
+
const { config, env, auth } = ctx;
|
|
238
|
+
if (config?.auth?.mode === 'none') return { ok: true };
|
|
239
|
+
if (tier === 0) return { ok: true };
|
|
240
|
+
|
|
241
|
+
if (tier === 2) {
|
|
242
|
+
const token = resolveSecret(config?.auth?.adminToken, env)
|
|
243
|
+
?? resolveSecret(config?.adminKey, env);
|
|
244
|
+
if (!token) return { ok: false, status: 503, error: 'Admin credentials not configured' };
|
|
245
|
+
const r = authenticateAdmin(req, token);
|
|
246
|
+
return r.authenticated ? { ok: true } : { ok: false, status: 401, error: r.error };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (tier === 3) {
|
|
250
|
+
const token = resolveSecret(config?.auth?.metricsToken, env);
|
|
251
|
+
if (!token) return { ok: true }; // open when metricsToken not set
|
|
252
|
+
const r = authenticateAdmin(req, token);
|
|
253
|
+
return r.authenticated ? { ok: true } : { ok: false, status: 401, error: r.error };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// tier 1 — JWT
|
|
257
|
+
const r = auth.authenticate(req);
|
|
258
|
+
return r.authenticated
|
|
259
|
+
? { ok: true, userId: r.userId, claims: r.claims }
|
|
260
|
+
: { ok: false, status: 401, error: r.error };
|
|
261
|
+
}
|
|
262
|
+
|
|
203
263
|
/**
|
|
204
264
|
* Create an HTTP request handler for all sidecar routes.
|
|
205
265
|
*
|
|
@@ -219,23 +279,31 @@ export function createSidecarRouter(ctx, options = {}) {
|
|
|
219
279
|
return async (req, res) => {
|
|
220
280
|
const url = new URL(req.url, 'http://localhost');
|
|
221
281
|
|
|
282
|
+
// ── Normalise /agent-api/v1/* → /agent-api/* ──────────────────────────
|
|
283
|
+
const sidecarPath = url.pathname.startsWith('/agent-api/v1/')
|
|
284
|
+
? '/agent-api/' + url.pathname.slice('/agent-api/v1/'.length)
|
|
285
|
+
: url.pathname;
|
|
286
|
+
|
|
287
|
+
// ── Auth gate ──────────────────────────────────────────────────────────
|
|
288
|
+
const tier = getRouteTier(sidecarPath);
|
|
289
|
+
const authCheck = applyRouteAuth(req, ctx, tier);
|
|
290
|
+
if (!authCheck.ok) {
|
|
291
|
+
if (authCheck.status === 401) res.setHeader('WWW-Authenticate', 'Bearer');
|
|
292
|
+
return sendJson(res, authCheck.status, { error: authCheck.error });
|
|
293
|
+
}
|
|
294
|
+
|
|
222
295
|
// ── /mcp route (optional) ──────────────────────────────────────────────
|
|
223
|
-
if (mcpHandler &&
|
|
296
|
+
if (mcpHandler && sidecarPath.startsWith('/mcp')) {
|
|
224
297
|
return mcpHandler(req, res);
|
|
225
298
|
}
|
|
226
299
|
|
|
227
300
|
// ── /health ────────────────────────────────────────────────────────────
|
|
228
|
-
if (req.method === 'GET' &&
|
|
301
|
+
if (req.method === 'GET' && sidecarPath === '/health') {
|
|
229
302
|
sendJson(res, 200, { status: 'ok' });
|
|
230
303
|
return;
|
|
231
304
|
}
|
|
232
305
|
|
|
233
306
|
// ── 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
307
|
|
|
240
308
|
if (sidecarPath === '/agent-api/chat' && req.method === 'POST') {
|
|
241
309
|
return handleChat(req, res, ctx);
|
|
@@ -249,7 +317,8 @@ export function createSidecarRouter(ctx, options = {}) {
|
|
|
249
317
|
if (sidecarPath === '/agent-api/user/preferences') {
|
|
250
318
|
if (req.method === 'GET') return handleGetPreferences(req, res, ctx);
|
|
251
319
|
if (req.method === 'PUT') return handlePutPreferences(req, res, ctx);
|
|
252
|
-
|
|
320
|
+
sendJson(res, 405, { error: 'Method not allowed' });
|
|
321
|
+
return;
|
|
253
322
|
}
|
|
254
323
|
if (sidecarPath.startsWith('/agent-api/conversations')) {
|
|
255
324
|
return handleConversations(req, res, ctx);
|
|
@@ -319,8 +388,14 @@ export function createSidecarRouter(ctx, options = {}) {
|
|
|
319
388
|
|
|
320
389
|
// ── Custom routes (consumer-provided) ─────────────────────────────────
|
|
321
390
|
if (customRoutes) {
|
|
322
|
-
|
|
323
|
-
|
|
391
|
+
try {
|
|
392
|
+
const handled = await customRoutes(req, res, ctx);
|
|
393
|
+
if (handled) return;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
process.stderr.write(`[forge] customRoutes error: ${err.message}\n`);
|
|
396
|
+
sendJson(res, 500, { error: 'Internal server error' });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
324
399
|
}
|
|
325
400
|
|
|
326
401
|
// ── 404 fallback ───────────────────────────────────────────────────────
|
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' });
|
package/lib/hitl-engine.d.ts
CHANGED
|
@@ -37,9 +37,15 @@ export class HitlEngine {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Retrieve and consume the paused state for a resume token.
|
|
40
|
-
*
|
|
40
|
+
* Returns null if the token has expired or does not exist (does not throw).
|
|
41
41
|
*/
|
|
42
|
-
resume(resumeToken: string): Promise<unknown>;
|
|
42
|
+
resume(resumeToken: string): Promise<unknown | null>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tear down any backend connections (Redis subscriber, Postgres pool, etc.).
|
|
46
|
+
* Call on graceful shutdown.
|
|
47
|
+
*/
|
|
48
|
+
destroy(): Promise<void>;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/**
|
package/lib/sidecar.d.ts
CHANGED
|
@@ -44,9 +44,18 @@ export interface SidecarInstance {
|
|
|
44
44
|
|
|
45
45
|
export function createSidecar(config?: Partial<SidecarConfig>, options?: SidecarOptions): Promise<SidecarInstance>;
|
|
46
46
|
|
|
47
|
+
export interface SidecarRouterOptions {
|
|
48
|
+
/** Absolute path to serve static files from for /widget/* routes. Defaults to package widget/. */
|
|
49
|
+
widgetDir?: string;
|
|
50
|
+
/** Optional async handler for /mcp routes. */
|
|
51
|
+
mcpHandler?: (req: object, res: object) => Promise<void> | void;
|
|
52
|
+
/** Called before the 404 fallback. Return true if the request was handled. */
|
|
53
|
+
customRoutes?: (req: object, res: object, ctx: SidecarContext) => Promise<boolean> | boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
// Advanced consumers
|
|
48
|
-
export function buildSidecarContext(config: SidecarConfig, db: object, env?: Record<string, string>, opts?:
|
|
49
|
-
export function createSidecarRouter(ctx: SidecarContext, opts?:
|
|
57
|
+
export function buildSidecarContext(config: SidecarConfig, db: object, env?: Record<string, string>, opts?: { configPath?: string }): Promise<SidecarContext>;
|
|
58
|
+
export function createSidecarRouter(ctx: SidecarContext, opts?: SidecarRouterOptions): (req: object, res: object) => Promise<void>;
|
|
50
59
|
|
|
51
60
|
export { createAuth } from './auth.js';
|
|
52
61
|
export type { AuthResult, AuthConfig, Authenticator } from './auth.js';
|
|
@@ -82,7 +91,7 @@ export class AgentRegistry {
|
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
export class VerifierRunner {
|
|
85
|
-
constructor(db: object, config?: object, workerPool?: object);
|
|
94
|
+
constructor(db: object, config?: object, pgPool?: object | null, workerPool?: object | null);
|
|
86
95
|
loadFromDb(db: object): Promise<void>;
|
|
87
96
|
run(toolName: string, args: object, result: unknown): Promise<Array<{ outcome: 'pass' | 'warn' | 'block'; message: string | null; verifier: string }>>;
|
|
88
97
|
destroy(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tool-forge",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"lib",
|
|
31
31
|
"widget",
|
|
32
|
+
"config",
|
|
32
33
|
"!lib/**/*.test.js",
|
|
33
34
|
"!lib/__fixtures__",
|
|
34
35
|
"!widget/**/*.test.js"
|