ollama-agent-router 0.1.7 → 0.1.8
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 +53 -2
- package/dist/cli.js +77 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +82 -1
- package/dist/index.js +78 -13
- package/dist/index.js.map +1 -1
- package/examples/gex44-secured.yaml +181 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ Start with:
|
|
|
94
94
|
ollama-agent-router serve --config examples/gex44.yaml
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
`examples/gex44-secured.yaml` is the same hardware profile with the standalone plane locked down: API key required, anonymous access rejected, per-key rate limits, and the admin plane enabled on localhost. Use it as a starting point when the router is exposed beyond a single user or process.
|
|
98
|
+
|
|
97
99
|
## Config Reference
|
|
98
100
|
|
|
99
101
|
Lookup order:
|
|
@@ -252,17 +254,22 @@ This prevents the admin API from changing the rules that protect itself. When `a
|
|
|
252
254
|
|
|
253
255
|
Admin API:
|
|
254
256
|
|
|
257
|
+
**Read the current managed access config**
|
|
258
|
+
|
|
255
259
|
```bash
|
|
256
260
|
curl http://127.0.0.1:11435/v1/admin/access/config \
|
|
257
261
|
-H 'authorization: Bearer admin-secret'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Replace the entire managed access config** (planes + all keys at once)
|
|
258
265
|
|
|
266
|
+
```bash
|
|
259
267
|
curl -X PUT http://127.0.0.1:11435/v1/admin/access/config \
|
|
260
268
|
-H 'authorization: Bearer admin-secret' \
|
|
261
269
|
-H 'content-type: application/json' \
|
|
262
270
|
-d '{
|
|
263
271
|
"expectedVersion": 1,
|
|
264
272
|
"config": {
|
|
265
|
-
"version": 1,
|
|
266
273
|
"planes": {
|
|
267
274
|
"standalone": {
|
|
268
275
|
"enabled": true,
|
|
@@ -280,7 +287,51 @@ curl -X PUT http://127.0.0.1:11435/v1/admin/access/config \
|
|
|
280
287
|
}'
|
|
281
288
|
```
|
|
282
289
|
|
|
283
|
-
|
|
290
|
+
`expectedVersion` enables optimistic concurrency. If present and the value does not match the active managed config version, the router returns `409`.
|
|
291
|
+
|
|
292
|
+
**Add an API key**
|
|
293
|
+
|
|
294
|
+
Generate a key and its SHA-256 hash first:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
node -e "
|
|
298
|
+
const c = require('crypto'), k = 'onr-' + c.randomBytes(20).toString('hex');
|
|
299
|
+
console.log('key: ', k);
|
|
300
|
+
console.log('hash: sha256:' + c.createHash('sha256').update(k).digest('hex'));
|
|
301
|
+
"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Then add the key:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl -X POST http://127.0.0.1:11435/v1/admin/access/keys \
|
|
308
|
+
-H 'authorization: Bearer admin-secret' \
|
|
309
|
+
-H 'content-type: application/json' \
|
|
310
|
+
-d '{
|
|
311
|
+
"id": "user-alice",
|
|
312
|
+
"name": "Alice",
|
|
313
|
+
"keyHash": "sha256:<hash>",
|
|
314
|
+
"scopes": ["standalone"],
|
|
315
|
+
"limits": {
|
|
316
|
+
"standalone": {"requests": 100, "windowSeconds": 60}
|
|
317
|
+
}
|
|
318
|
+
}'
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Returns `201` with the created key entry. Returns `409` if the `id` is already in use.
|
|
322
|
+
|
|
323
|
+
`scopes` controls which planes accept the key. Valid values are `standalone`, `runtimeAgent`, or both. `limits` is optional; when omitted, the plane's `defaultLimit` applies.
|
|
324
|
+
|
|
325
|
+
**Revoke an API key**
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
curl -X DELETE http://127.0.0.1:11435/v1/admin/access/keys/user-alice \
|
|
329
|
+
-H 'authorization: Bearer admin-secret'
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Returns `200 { "revoked": { ... } }` with the removed key entry. Returns `404` if the id is not found. The change takes effect immediately without a restart.
|
|
333
|
+
|
|
334
|
+
All admin operations are written atomically to `access.managedConfigPath` and appended to the audit log when `auditLog: true`.
|
|
284
335
|
|
|
285
336
|
For admin client certificate checks, enable HTTPS, configure `server.https.caPath`, and set:
|
|
286
337
|
|
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,17 @@ var protectedPlaneConfigSchema = z.object({
|
|
|
44
44
|
}).default({ requireApiKey: false, anonymous: "allow" }),
|
|
45
45
|
defaultLimit: trafficLimitSchema.optional()
|
|
46
46
|
});
|
|
47
|
+
var apiKeySchema = z.object({
|
|
48
|
+
id: z.string().min(1).regex(/^[a-zA-Z0-9._:-]+$/, "api key id may contain only letters, numbers, dots, underscores, colons, and dashes"),
|
|
49
|
+
name: z.string().min(1).optional(),
|
|
50
|
+
keyHash: z.string().regex(/^sha256:[a-fA-F0-9]{64}$/, "keyHash must use sha256:<64 hex chars>"),
|
|
51
|
+
enabled: z.boolean().default(true),
|
|
52
|
+
scopes: z.array(z.enum(["standalone", "runtimeAgent"])).min(1),
|
|
53
|
+
limits: z.object({
|
|
54
|
+
standalone: trafficLimitSchema.optional(),
|
|
55
|
+
runtimeAgent: trafficLimitSchema.optional()
|
|
56
|
+
}).optional()
|
|
57
|
+
});
|
|
47
58
|
var managedAccessConfigSchema = z.object({
|
|
48
59
|
version: z.number().int().nonnegative().default(1),
|
|
49
60
|
updatedAt: z.string().datetime().optional(),
|
|
@@ -60,19 +71,7 @@ var managedAccessConfigSchema = z.object({
|
|
|
60
71
|
standalone: { enabled: true, auth: { requireApiKey: false, anonymous: "allow" } },
|
|
61
72
|
runtimeAgent: { enabled: true, auth: { requireApiKey: false, anonymous: "allow" } }
|
|
62
73
|
}),
|
|
63
|
-
apiKeys: z.array(
|
|
64
|
-
z.object({
|
|
65
|
-
id: z.string().min(1).regex(/^[a-zA-Z0-9._:-]+$/, "api key id may contain only letters, numbers, dots, underscores, colons, and dashes"),
|
|
66
|
-
name: z.string().min(1).optional(),
|
|
67
|
-
keyHash: z.string().regex(/^sha256:[a-fA-F0-9]{64}$/, "keyHash must use sha256:<64 hex chars>"),
|
|
68
|
-
enabled: z.boolean().default(true),
|
|
69
|
-
scopes: z.array(z.enum(["standalone", "runtimeAgent"])).min(1),
|
|
70
|
-
limits: z.object({
|
|
71
|
-
standalone: trafficLimitSchema.optional(),
|
|
72
|
-
runtimeAgent: trafficLimitSchema.optional()
|
|
73
|
-
}).optional()
|
|
74
|
-
})
|
|
75
|
-
).default([])
|
|
74
|
+
apiKeys: z.array(apiKeySchema).default([])
|
|
76
75
|
});
|
|
77
76
|
var defaultManagedAccessConfig = managedAccessConfigSchema.parse({});
|
|
78
77
|
var adminPlaneConfigSchema = z.object({
|
|
@@ -1652,6 +1651,51 @@ var AccessControlStore = class {
|
|
|
1652
1651
|
});
|
|
1653
1652
|
return structuredClone(updated);
|
|
1654
1653
|
}
|
|
1654
|
+
async addApiKey(input2) {
|
|
1655
|
+
const key = apiKeySchema.parse(input2);
|
|
1656
|
+
await this.enqueueWrite(async () => {
|
|
1657
|
+
if (this.managed.apiKeys.some((k) => k.id === key.id)) {
|
|
1658
|
+
throw new AccessHttpError(409, `API key with id '${key.id}' already exists`);
|
|
1659
|
+
}
|
|
1660
|
+
if (!this.access.managedConfigPath) {
|
|
1661
|
+
throw new AccessHttpError(500, "access.managedConfigPath is not configured");
|
|
1662
|
+
}
|
|
1663
|
+
const next = {
|
|
1664
|
+
...this.managed,
|
|
1665
|
+
version: this.managed.version + 1,
|
|
1666
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1667
|
+
apiKeys: [...this.managed.apiKeys, key]
|
|
1668
|
+
};
|
|
1669
|
+
await writeManagedAccessConfig(this.access.managedConfigPath, next);
|
|
1670
|
+
this.managed = next;
|
|
1671
|
+
this.access.managed = next;
|
|
1672
|
+
});
|
|
1673
|
+
return structuredClone(key);
|
|
1674
|
+
}
|
|
1675
|
+
async revokeApiKey(id) {
|
|
1676
|
+
let removed;
|
|
1677
|
+
await this.enqueueWrite(async () => {
|
|
1678
|
+
const idx = this.managed.apiKeys.findIndex((k) => k.id === id);
|
|
1679
|
+
if (idx === -1) {
|
|
1680
|
+
throw new AccessHttpError(404, `API key '${id}' not found`);
|
|
1681
|
+
}
|
|
1682
|
+
if (!this.access.managedConfigPath) {
|
|
1683
|
+
throw new AccessHttpError(500, "access.managedConfigPath is not configured");
|
|
1684
|
+
}
|
|
1685
|
+
removed = this.managed.apiKeys[idx];
|
|
1686
|
+
const next = {
|
|
1687
|
+
...this.managed,
|
|
1688
|
+
version: this.managed.version + 1,
|
|
1689
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1690
|
+
apiKeys: this.managed.apiKeys.filter((_, i) => i !== idx)
|
|
1691
|
+
};
|
|
1692
|
+
await writeManagedAccessConfig(this.access.managedConfigPath, next);
|
|
1693
|
+
this.managed = next;
|
|
1694
|
+
this.access.managed = next;
|
|
1695
|
+
this.limiter.clear();
|
|
1696
|
+
});
|
|
1697
|
+
return structuredClone(removed);
|
|
1698
|
+
}
|
|
1655
1699
|
publicMiddleware(planeOrPlanes) {
|
|
1656
1700
|
return (req, res, next) => {
|
|
1657
1701
|
const planes = Array.isArray(planeOrPlanes) ? planeOrPlanes : [planeOrPlanes];
|
|
@@ -1962,6 +2006,26 @@ function createApp(config, deps) {
|
|
|
1962
2006
|
next(error);
|
|
1963
2007
|
}
|
|
1964
2008
|
});
|
|
2009
|
+
api.post("/v1/admin/access/keys", adminAccess, async (req, res, next) => {
|
|
2010
|
+
try {
|
|
2011
|
+
const key = await access3.addApiKey(req.body);
|
|
2012
|
+
auditAdmin(config.access.admin, req, "success", "key_added", res.locals.admin?.remoteIp, key.id);
|
|
2013
|
+
res.status(201).json(key);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
auditAdmin(config.access.admin, req, "failure", error instanceof Error ? error.message : String(error), res.locals.admin?.remoteIp);
|
|
2016
|
+
next(error);
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
api.delete("/v1/admin/access/keys/:id", adminAccess, async (req, res, next) => {
|
|
2020
|
+
try {
|
|
2021
|
+
const revoked = await access3.revokeApiKey(req.params.id);
|
|
2022
|
+
auditAdmin(config.access.admin, req, "success", "key_revoked", res.locals.admin?.remoteIp, revoked.id);
|
|
2023
|
+
res.json({ revoked });
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
auditAdmin(config.access.admin, req, "failure", error instanceof Error ? error.message : String(error), res.locals.admin?.remoteIp);
|
|
2026
|
+
next(error);
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
1965
2029
|
api.get("/v1/router/capabilities", runtimeAgentAccess, (_req, res) => {
|
|
1966
2030
|
res.json(buildCapabilities(config));
|
|
1967
2031
|
});
|