@evoclock/pi-agentic-driver 0.9.2 → 0.9.3

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.
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Unit tests: single-flight per evalInputDigest (M9) and the global
5
+ // concurrency cap with busy rejection.
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { SingleFlight } from "../janus/single_flight.js";
10
+ import { JanusError } from "../janus/errors.js";
11
+
12
+ test("identical digests coalesce onto one task", async () => {
13
+ const sf = new SingleFlight(4);
14
+ let runs = 0;
15
+ const task = () =>
16
+ sf.run("digest-a", async () => {
17
+ runs += 1;
18
+ await new Promise((r) => setTimeout(r, 50));
19
+ return "result";
20
+ });
21
+
22
+ const [a, b, c] = await Promise.all([task(), task(), task()]);
23
+ assert.equal(runs, 1);
24
+ assert.equal(a, "result");
25
+ assert.equal(b, "result");
26
+ assert.equal(c, "result");
27
+ });
28
+
29
+ test("distinct digests run as separate tasks", async () => {
30
+ const sf = new SingleFlight(4);
31
+ let runs = 0;
32
+ const make = (digest: string) =>
33
+ sf.run(digest, async () => {
34
+ runs += 1;
35
+ return digest;
36
+ });
37
+ await Promise.all([make("d1"), make("d2"), make("d3")]);
38
+ assert.equal(runs, 3);
39
+ });
40
+
41
+ test("global cap rejects distinct digests with busy", async () => {
42
+ const sf = new SingleFlight(2);
43
+ const release: (() => void)[] = [];
44
+ const make = (digest: string) =>
45
+ sf.run(digest, () => new Promise<string>((resolvePromise) => release.push(() => resolvePromise(digest))));
46
+
47
+ void make("d1");
48
+ void make("d2");
49
+ assert.throws(() => make("d3"), (error: unknown) => {
50
+ return error instanceof JanusError && error.code === "busy";
51
+ });
52
+ for (const r of release) r();
53
+ });
54
+
55
+ test("digests are released after completion", async () => {
56
+ const sf = new SingleFlight(1);
57
+ await sf.run("d1", async () => "x");
58
+ // Cap of 1 must allow the next request after the first completes.
59
+ assert.equal(await sf.run("d2", async () => "y"), "y");
60
+ assert.equal(await sf.run("d1", async () => "z"), "z");
61
+ });
62
+
63
+ test("failed flights release their digest", async () => {
64
+ const sf = new SingleFlight(1);
65
+ await assert.rejects(
66
+ sf.run("d1", async () => {
67
+ throw new Error("boom");
68
+ }),
69
+ /boom/,
70
+ );
71
+ assert.equal(await sf.run("d1", async () => "recovered"), "recovered");
72
+ });
73
+
74
+ test("coalesced waiters share a failed flight's rejection", async () => {
75
+ const sf = new SingleFlight(4);
76
+ const task = () =>
77
+ sf.run("digest-fail", async () => {
78
+ await new Promise((r) => setTimeout(r, 20));
79
+ throw new Error("gateway down");
80
+ });
81
+ const results = await Promise.allSettled([task(), task()]);
82
+ assert.equal(results[0]?.status, "rejected");
83
+ assert.equal(results[1]?.status, "rejected");
84
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noImplicitOverride": true,
13
+ "verbatimModuleSyntax": true,
14
+ "isolatedModules": true,
15
+ "skipLibCheck": true,
16
+ "noEmit": true
17
+ },
18
+ "include": ["janus/**/*.ts", "tests/**/*.ts"]
19
+ }
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!--
4
+ janus launchd template — a STARTING POINT, not a finished service file.
5
+ Everything marked ADJUST below is user-specific by design; the template
6
+ ships with placeholders because the label, paths, and log locations belong
7
+ to the user's machine, not to this repository.
8
+
9
+ Install:
10
+ 1. Copy the template somewhere out of the repo:
11
+ cp janus/user.janus.plist ~/Library/LaunchAgents/user.janus.plist
12
+ 2. ADJUST the values marked below (Label, WorkingDirectory, log paths).
13
+ 3. Load it:
14
+ launchctl load ~/Library/LaunchAgents/user.janus.plist
15
+
16
+ The service binds 127.0.0.1 only and reads the gateway key from the
17
+ Keychain at runtime; no environment variables are required.
18
+ -->
19
+ <plist version="1.0">
20
+ <dict>
21
+ <key>Label</key>
22
+ <!-- ADJUST: any label you like; the file name should match it. -->
23
+ <string>user.janus</string>
24
+ <key>ProgramArguments</key>
25
+ <array>
26
+ <!-- ADJUST if your tooling lives elsewhere (e.g. /usr/local/bin/npx). -->
27
+ <string>/opt/homebrew/bin/npx</string>
28
+ <string>tsx</string>
29
+ <string>janus/server.ts</string>
30
+ </array>
31
+ <key>WorkingDirectory</key>
32
+ <!-- ADJUST: the janus/ directory of your checkout. -->
33
+ <string>/path/to/pi-agentic-driver/janus</string>
34
+ <key>RunAtLoad</key>
35
+ <true/>
36
+ <key>KeepAlive</key>
37
+ <true/>
38
+ <key>StandardOutPath</key>
39
+ <!-- ADJUST: where stdout goes. -->
40
+ <string>/tmp/janus.stdout.log</string>
41
+ <key>StandardErrorPath</key>
42
+ <!-- ADJUST: where stderr goes. -->
43
+ <string>/tmp/janus.stderr.log</string>
44
+ <key>EnvironmentVariables</key>
45
+ <dict>
46
+ <key>PATH</key>
47
+ <!-- ADJUST if your npx lives on a different PATH. -->
48
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
49
+ </dict>
50
+ </dict>
51
+ </plist>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoclock/pi-agentic-driver",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Guardrail extensions for Agentic Driver: advisory review, bounded Herdr communication, and guarded worker lifecycle.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -80,7 +80,8 @@
80
80
  "scripts/enforcement/router_replacement_pi.js",
81
81
  "scripts/enforcement/router_runtime_pi.js",
82
82
  ".agentic-driver/router.defaults.json",
83
- "skills/pulse/SKILL.md"
83
+ "skills/pulse/SKILL.md",
84
+ "janus"
84
85
  ],
85
86
  "pi": {
86
87
  "extensions": [
@@ -28,9 +28,16 @@ export const QUOTA_OBSERVATION_SCHEMA = "agentic-driver.quota-observation.v1";
28
28
  // ---------------------------------------------------------------------------
29
29
 
30
30
  export const SEAT_KINDS = Object.freeze(["local", "hosted-api", "subscription"]);
31
- export const PROVIDERS = Object.freeze([
32
- "llama-server", "merge-gateway", "vercel-ai-gateway", "anthropic", "openai",
31
+ // Built-in provider registry: the most common API routers plus the local
32
+ // serving adapter. Users extend this list through the config's
33
+ // `providers` section — the registry is OPEN by configuration, closed by
34
+ // default. Nothing in the code branches on specific provider names.
35
+ export const BUILTIN_PROVIDERS = Object.freeze([
36
+ "openrouter", "vercel-ai-gateway", "opencode", "fireworks-ai", "baseten",
37
+ "particle-ai", "cerebras", "together-ai", "merge-gateway", "anthropic",
38
+ "openai", "llama-server",
33
39
  ]);
40
+ export const PROVIDERS = BUILTIN_PROVIDERS;
34
41
  export const CAPABILITIES = Object.freeze([
35
42
  "implement", "review", "final-verification", "design", "research",
36
43
  ]);
@@ -73,7 +80,7 @@ function isPosInt(value) {
73
80
  // §2 Seat record: agentic-driver.seat.v1
74
81
  // ---------------------------------------------------------------------------
75
82
 
76
- export function wellFormedSeat(seat, { endpoints = null } = {}) {
83
+ export function wellFormedSeat(seat, { endpoints = null, providerRegistry = BUILTIN_PROVIDERS } = {}) {
77
84
  if (!isPlainObject(seat) || !exactKeys(seat, [
78
85
  "schema", "seatId", "kind", "provider", "accountId", "endpointRef",
79
86
  "model", "capabilities", "containmentTier", "maxConcurrency",
@@ -88,8 +95,8 @@ export function wellFormedSeat(seat, { endpoints = null } = {}) {
88
95
  if (!SEAT_KINDS.includes(seat.kind)) {
89
96
  return { ok: false, reason: `seat kind "${seat.kind}" is not one of ${SEAT_KINDS.join(", ")}` };
90
97
  }
91
- if (!PROVIDERS.includes(seat.provider)) {
92
- return { ok: false, reason: `seat provider "${seat.provider}" is not in the closed provider registry` };
98
+ if (!providerRegistry.includes(seat.provider)) {
99
+ return { ok: false, reason: `seat provider "${seat.provider}" is not in the provider registry (built-ins plus configured extensions)` };
93
100
  }
94
101
  // accountId is REQUIRED iff kind is subscription (§2).
95
102
  if (seat.kind === "subscription") {
@@ -191,11 +198,24 @@ export const ELIGIBILITY_RULES = Object.freeze([
191
198
 
192
199
  export function validateRouterConfig(config) {
193
200
  if (!isPlainObject(config)) return { ok: false, errors: ["router config must be an object"] };
194
- if (!exactKeys(config, [
201
+ // exactKeys closed, with one governed extension point: the optional
202
+ // "providers" section (user-added provider ids). Built-ins always apply;
203
+ // extensions add to them, never remove.
204
+ const requiredKeys = [
195
205
  "schema", "revision", "endpoints", "seats", "models", "collectors",
196
206
  "eligibility", "preferences", "ranking", "localHealth", "secrets",
197
- ])) {
198
- return { ok: false, errors: ["router config has unknown or missing fields (exactKeys closed)"] };
207
+ ];
208
+ const optionalKeys = ["providers"];
209
+ const actualKeys = Object.keys(config).sort();
210
+ const allowed = [...requiredKeys, ...optionalKeys].sort();
211
+ const missing = requiredKeys.filter((key) => !actualKeys.includes(key));
212
+ const unknown = actualKeys.filter((key) => !allowed.includes(key));
213
+ if (missing.length || unknown.length) {
214
+ return { ok: false, errors: [
215
+ `router config has unknown or missing fields (exactKeys closed)` +
216
+ (missing.length ? `; missing: ${missing.join(", ")}` : "") +
217
+ (unknown.length ? `; unknown: ${unknown.join(", ")}` : ""),
218
+ ] };
199
219
  }
200
220
  const errors = [];
201
221
  if (config.schema !== ROUTER_CONFIG_SCHEMA) {
@@ -204,6 +224,36 @@ export function validateRouterConfig(config) {
204
224
  if (!isPosInt(config.revision)) {
205
225
  errors.push("config revision must be a positive integer");
206
226
  }
227
+ // providers{}: optional user extension of the registry. Each entry is a
228
+ // slug; entries add to the built-ins, never shadow or remove them.
229
+ let providerRegistry = BUILTIN_PROVIDERS;
230
+ if (config.providers !== undefined) {
231
+ if (!isPlainObject(config.providers) || Object.keys(config.providers).length === 0) {
232
+ errors.push("providers must be a non-empty object keyed by provider slug when present");
233
+ } else {
234
+ const extended = [];
235
+ for (const [name, entry] of Object.entries(config.providers)) {
236
+ if (!SLUG_RE.test(name) || name.length > 64) {
237
+ errors.push(`provider "${name}" must be a slug (max 64 chars)`);
238
+ continue;
239
+ }
240
+ if (BUILTIN_PROVIDERS.includes(name)) {
241
+ errors.push(`provider "${name}" shadows a built-in provider`);
242
+ continue;
243
+ }
244
+ // Entry shape is closed: {auth} — how the provider authenticates.
245
+ // "keychain" (default posture) is the only supported auth kind at
246
+ // launch; the entry exists so provider addition needs no code change.
247
+ if (!isPlainObject(entry) || !exactKeys(entry, ["auth"])
248
+ || entry.auth !== "keychain") {
249
+ errors.push(`provider "${name}" must have exactly {auth: "keychain"}`);
250
+ continue;
251
+ }
252
+ extended.push(name);
253
+ }
254
+ providerRegistry = Object.freeze([...BUILTIN_PROVIDERS, ...extended]);
255
+ }
256
+ }
207
257
  // endpoints{}: named, closed-shape endpoint records. URLs are validated
208
258
  // structurally (http/https); there is no unrestricted-URL escape hatch.
209
259
  if (!isPlainObject(config.endpoints)) {
@@ -222,8 +272,8 @@ export function validateRouterConfig(config) {
222
272
  || !/^https?:\/\/[A-Za-z0-9._~:/?#[\]@!$&'()*+,;=%-]+$/.test(endpoint.url)) {
223
273
  errors.push(`endpoint "${name}" url must be an http(s) URL`);
224
274
  }
225
- if (!PROVIDERS.includes(endpoint.kind)) {
226
- errors.push(`endpoint "${name}" kind "${endpoint.kind}" is not in the closed provider registry`);
275
+ if (!providerRegistry.includes(endpoint.kind)) {
276
+ errors.push(`endpoint "${name}" kind "${endpoint.kind}" is not in the provider registry (built-ins plus configured extensions)`);
227
277
  }
228
278
  }
229
279
  }
@@ -233,7 +283,7 @@ export function validateRouterConfig(config) {
233
283
  } else {
234
284
  const seen = new Set();
235
285
  for (const seat of config.seats) {
236
- const check = wellFormedSeat(seat, { endpoints: config.endpoints });
286
+ const check = wellFormedSeat(seat, { endpoints: config.endpoints, providerRegistry });
237
287
  if (!check.ok) {
238
288
  errors.push(`seat ${seat?.seatId ?? "(unidentified)"}: ${check.reason}`);
239
289
  continue;
@@ -408,8 +458,8 @@ export function validateRouterConfig(config) {
408
458
  errors.push("secrets must be an object keyed by provider id");
409
459
  } else {
410
460
  for (const [provider, ref] of Object.entries(config.secrets)) {
411
- if (!PROVIDERS.includes(provider)) {
412
- errors.push(`secrets key "${provider}" is not in the closed provider registry`);
461
+ if (!providerRegistry.includes(provider)) {
462
+ errors.push(`secrets key "${provider}" is not in the provider registry (built-ins plus configured extensions)`);
413
463
  continue;
414
464
  }
415
465
  if (!isPlainObject(ref) || !exactKeys(ref, SECRET_REF_FIELDS)