@inetafrica/open-claudia 3.4.7 → 3.4.12
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/CHANGELOG.md +28 -0
- package/channels/kazee/call/access.js +86 -0
- package/channels/kazee/call/audio.js +1 -1
- package/channels/kazee/call/janus-client.js +3 -1
- package/channels/kazee/call/lease.js +70 -0
- package/channels/kazee/call/manager.js +152 -106
- package/channels/kazee/call/opus-codec.js +44 -0
- package/channels/kazee/call/session.js +111 -8
- package/package.json +2 -2
- package/test-cursor-removal.js +4 -1
- package/test-kazee-call-audio.js +22 -0
- package/test-kazee-call-janus.js +7 -2
- package/test-kazee-call-manager.js +165 -7
- package/test-kazee-call-session.js +120 -1
- package/test-provider-enforcer.js +29 -6
- package/test-provider-language.js +2 -2
|
@@ -19,7 +19,7 @@ class FakeJanus {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
class FakePC {
|
|
22
|
-
constructor() { this.onTrack = { subscribe: () => {} }; }
|
|
22
|
+
constructor(options) { this.options = options; this.onTrack = { subscribe: () => {} }; }
|
|
23
23
|
addTrack() { return { codec: { payloadType: 111 } }; }
|
|
24
24
|
async createOffer() { return {}; }
|
|
25
25
|
async setLocalDescription() {}
|
|
@@ -79,6 +79,28 @@ async function testSpeakSerialization() {
|
|
|
79
79
|
await session.close();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
async function testMeasuredMediaReady() {
|
|
83
|
+
const { session } = makeSession();
|
|
84
|
+
let ready = 0, sent = 0;
|
|
85
|
+
session.onMediaReady = () => { ready++; };
|
|
86
|
+
await session._subscribe(901, "peer");
|
|
87
|
+
const codec = new OpusAudioCodec();
|
|
88
|
+
session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
|
|
89
|
+
assert.strictEqual(ready, 0, "receive-only media is insufficient");
|
|
90
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
91
|
+
assert.strictEqual(ready, 0, "missing publisher track cannot count as sent media");
|
|
92
|
+
session.publisher = { track: { writeRtp: () => { sent++; } }, sender: { codec: { payloadType: 111 } } };
|
|
93
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
94
|
+
assert.strictEqual(ready, 1);
|
|
95
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
96
|
+
session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
|
|
97
|
+
assert.strictEqual(ready, 1, "media-ready is reported once per session");
|
|
98
|
+
await session.close();
|
|
99
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
100
|
+
assert.strictEqual(sent, 2, "closed sessions cannot send or activate media");
|
|
101
|
+
codec.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
async function testSpeakCancellation() {
|
|
83
105
|
let frames = 0;
|
|
84
106
|
const synthCalls = [];
|
|
@@ -421,9 +443,106 @@ async function testCloseResolvesSpeech() {
|
|
|
421
443
|
assert.strictEqual(await session.speak("after"), false, "speak after close is a no-op");
|
|
422
444
|
}
|
|
423
445
|
|
|
446
|
+
async function testManagedRoomCredentials() {
|
|
447
|
+
class SignedJanus extends FakeJanus {
|
|
448
|
+
constructor(options) { super(); this.options = options; this.calls = []; }
|
|
449
|
+
async message(handle, body) {
|
|
450
|
+
this.calls.push(body);
|
|
451
|
+
if (body.request === "join" && body.ptype === "publisher") return { plugindata: { data: { videoroom: "joined", id: 41, private_id: 999, publishers: [] } } };
|
|
452
|
+
return {};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
let refreshes = 0;
|
|
456
|
+
const session = new KazeeCallSession({ roomId: 7, chatId: "chat", callerId: "owner", botUserId: "bot", janusUrl: "ws://test",
|
|
457
|
+
janusToken: "root-token", roomToken: "publisher-token", roomTokenExpiresAt: Date.now() + 300000, managedRoom: true, asCaller: true, iceServers: [{ urls: "turn:relay:3478", username: "fixture", credential: "fixture" }], iceCredentialsExpiresAt: Date.now() + 3600000,
|
|
458
|
+
refreshRoomAccess: async () => { refreshes += 1; return { iceServers: [{ urls: "turn:relay:3478", username: "fresh", credential: "fresh" }], iceCredentialsExpiresAt: Date.now() + 3600000, janusToken: "fresh-root", janusTokenExpiresAt: Date.now() + 86400000, roomToken: "subscriber-token", roomTokenExpiresAt: Date.now() + 300000 }; },
|
|
459
|
+
JanusClientImpl: SignedJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
|
|
460
|
+
session._maybePublish = async () => {};
|
|
461
|
+
await session.start();
|
|
462
|
+
assert.strictEqual(session.janus.options.token, "root-token");
|
|
463
|
+
assert.strictEqual(session.janus.calls[0].token, "publisher-token");
|
|
464
|
+
await session._consumePublisherFrame({ plugindata: { data: { publishers: [{ id: 41, display: 'canonical-bot@calling.invalid' }, { id: 42, display: 'open-claudia:legacy-bot' }] } } });
|
|
465
|
+
assert.strictEqual(session.subscribers.size, 0, 'own joined publisher ID is excluded independently of canonical display; legacy bot exclusion remains');
|
|
466
|
+
|
|
467
|
+
session.roomTokenExpiresAt = Date.now() - 1;
|
|
468
|
+
await Promise.all([session._subscribe(17, "owner"), session._subscribe(18, "other")]);
|
|
469
|
+
const subJoin = session.janus.calls.find(body => body.ptype === "subscriber");
|
|
470
|
+
assert.strictEqual(subJoin.token, "subscriber-token");
|
|
471
|
+
assert.strictEqual(subJoin.private_id, 999);
|
|
472
|
+
assert.strictEqual(refreshes, 1, "simultaneous subscriber joins share one token refresh");
|
|
473
|
+
session.janusTokenExpiresAt = Date.now() + 50000;
|
|
474
|
+
const handlesBefore = session.janus.handles;
|
|
475
|
+
await session._enforceLimits();
|
|
476
|
+
await wait(0);
|
|
477
|
+
assert.strictEqual(session.janus.token, "fresh-root");
|
|
478
|
+
assert.strictEqual(refreshes, 2, "root renewal does not wait for a new subscriber");
|
|
479
|
+
assert.strictEqual(session.janus.handles, handlesBefore, "renewal keeps existing session and handles");
|
|
480
|
+
session.janusTokenExpiresAt = Date.now() - 1;
|
|
481
|
+
await session._enforceLimits();
|
|
482
|
+
assert.strictEqual(session.closed, true, "expired transport credentials close media");
|
|
483
|
+
await session.close();
|
|
484
|
+
assert.ok(!session.janus.calls.some(body => ["create", "destroy"].includes(body.request)), "server allocated room ownership never transfers to the bot");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function testAssignedRelayTransportLifetime() {
|
|
488
|
+
const { RTCPeerConnection } = require('werift');
|
|
489
|
+
const originalExpiry = Date.now() + 3600000;
|
|
490
|
+
const assigned = (username, expiresAt) => ({ iceServers: [{ urls: 'turn:127.0.0.1:3478', username, credential: username + '-password' }], iceCredentialsExpiresAt: expiresAt });
|
|
491
|
+
const session = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
|
|
492
|
+
...assigned('original-fixture', originalExpiry), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
|
|
493
|
+
refreshRoomAccess: async () => ({ ...assigned('renewed-fixture', originalExpiry + 3600000), roomToken: 'renewed-room', roomTokenExpiresAt: Date.now() + 300000, janusToken: 'renewed-root', janusTokenExpiresAt: Date.now() + 300000 }),
|
|
494
|
+
JanusClientImpl: FakeJanus, PeerConnectionImpl: RTCPeerConnection, SpeechStreamImpl: FakeSpeechStream });
|
|
495
|
+
try {
|
|
496
|
+
const old = session._newPeerConnection(); old.addTransceiver('audio');
|
|
497
|
+
assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
|
|
498
|
+
// Calling Werift's public setter cannot refresh an already-created ICE
|
|
499
|
+
// transport. Verify the installed implementation, without network gathering.
|
|
500
|
+
old.setConfiguration({ iceServers: assigned('setter-fixture', originalExpiry).iceServers });
|
|
501
|
+
assert.strictEqual(old.getConfiguration().iceServers[0].username, 'setter-fixture');
|
|
502
|
+
assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
|
|
503
|
+
await session._roomAuth(true);
|
|
504
|
+
assert.strictEqual(session._transportIceExpiry.get(old), originalExpiry, 'new access never extends an existing allocation deadline');
|
|
505
|
+
assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
|
|
506
|
+
const fresh = session._newPeerConnection(); fresh.addTransceiver('audio');
|
|
507
|
+
assert.strictEqual(fresh.iceTransports[0].connection.options.turnUsername, 'renewed-fixture');
|
|
508
|
+
assert.strictEqual(session._transportIceExpiry.get(fresh), originalExpiry + 3600000);
|
|
509
|
+
} finally { await session.close(); }
|
|
510
|
+
assert.strictEqual(session._transportExpiryTimer, null);
|
|
511
|
+
assert.strictEqual(session._transportIceExpiry.size, 0);
|
|
512
|
+
|
|
513
|
+
let ended;
|
|
514
|
+
const bounded = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
|
|
515
|
+
...assigned('short-fixture', Date.now() + 1500), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
|
|
516
|
+
refreshRoomAccess: async () => ({ ...assigned('fresh-fixture', Date.now() + 3600000), roomToken: 'fresh', roomTokenExpiresAt: Date.now() + 300000 }),
|
|
517
|
+
onEnded: reason => { ended = reason; }, JanusClientImpl: FakeJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
|
|
518
|
+
try {
|
|
519
|
+
const old = bounded._newPeerConnection();
|
|
520
|
+
const deadline = bounded._transportIceExpiry.get(old);
|
|
521
|
+
await bounded._roomAuth(true);
|
|
522
|
+
assert.strictEqual(bounded._transportIceExpiry.get(old), deadline);
|
|
523
|
+
const until = Date.now() + 2000;
|
|
524
|
+
while (!bounded.closed && Date.now() < until) await wait(25);
|
|
525
|
+
assert.strictEqual(ended, 'relay-credentials-expired', 'existing transport closes on its original expiry despite successful refresh');
|
|
526
|
+
} finally { await bounded.close(); }
|
|
527
|
+
|
|
528
|
+
let publications = 0;
|
|
529
|
+
class PublishingPC extends FakePC { constructor(options) { super(options); publications += 1; } }
|
|
530
|
+
class PublishingJanus extends FakeJanus { async message() { return { jsep: { type: 'answer', sdp: 'fixture' } }; } }
|
|
531
|
+
const concurrent = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
|
|
532
|
+
...assigned('original-fixture', Date.now() + 3600000), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
|
|
533
|
+
JanusClientImpl: PublishingJanus, PeerConnectionImpl: PublishingPC });
|
|
534
|
+
try { await Promise.all([concurrent._publish(), concurrent._publish()]); assert.strictEqual(publications, 1, 'concurrent publisher events cannot race across async room authorization'); } finally { await concurrent.close(); }
|
|
535
|
+
|
|
536
|
+
const missing = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true, JanusClientImpl: FakeJanus, PeerConnectionImpl: FakePC });
|
|
537
|
+
try { assert.throws(() => missing._newPeerConnection(), /relay credentials unavailable/); } finally { await missing.close(); }
|
|
538
|
+
}
|
|
539
|
+
|
|
424
540
|
(async () => {
|
|
541
|
+
await testAssignedRelayTransportLifetime();
|
|
542
|
+
await testManagedRoomCredentials();
|
|
425
543
|
await testSpeakSerialization();
|
|
426
544
|
await testSpeakCancellation();
|
|
545
|
+
await testMeasuredMediaReady();
|
|
427
546
|
await testEchoGateAndBargeIn();
|
|
428
547
|
await testPrebufferAndCommit();
|
|
429
548
|
await testHoldForWords();
|
|
@@ -6,7 +6,8 @@ const assert = require("assert");
|
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const path = require("path");
|
|
9
|
-
const { spawnSync } = require("child_process");
|
|
9
|
+
const { spawn, spawnSync } = require("child_process");
|
|
10
|
+
const { killProcessTree } = require("./core/process-tree");
|
|
10
11
|
|
|
11
12
|
const RESULT_PREFIX = "__PROVIDER_ENFORCER_RESULT__";
|
|
12
13
|
|
|
@@ -73,7 +74,6 @@ function auditSink() {
|
|
|
73
74
|
|
|
74
75
|
async function providerProbe(kind) {
|
|
75
76
|
const root = isolate(kind);
|
|
76
|
-
const timeoutSignal = path.join(root, `${kind}-timeout-signal.json`);
|
|
77
77
|
const controls = [
|
|
78
78
|
{ FAKE_AGENT_TEXT: '{"decision":"allow","reason":"inside mandate"}' },
|
|
79
79
|
{ FAKE_AGENT_TEXT: '{"decision":"block","reason":"outside mandate"}' },
|
|
@@ -82,16 +82,27 @@ async function providerProbe(kind) {
|
|
|
82
82
|
// The guard-reliability fix retries ONCE on a transient timeout before
|
|
83
83
|
// failing closed: BOTH attempts hang, so the verdict still escalates —
|
|
84
84
|
// proving the retry is bounded and never fails open at the transport level.
|
|
85
|
-
{ FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_SIGNAL_CAPTURE: timeoutSignal },
|
|
86
85
|
{ FAKE_AGENT_SCENARIO: "forced-termination" },
|
|
86
|
+
{ FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_IGNORE_SIGTERM: "1" },
|
|
87
87
|
];
|
|
88
88
|
const entry = makeProvider(kind, root, controls);
|
|
89
89
|
const model = `${kind}-enforcer-exact`;
|
|
90
90
|
const { createUtilityAgentService } = require("./core/utility-agent");
|
|
91
|
+
const children = [];
|
|
92
|
+
const terminations = [];
|
|
91
93
|
const utility = createUtilityAgentService({
|
|
92
94
|
registry: registry([entry]),
|
|
93
95
|
env: { ENFORCER_PROVIDER: kind, [`ENFORCER_MODEL_${kind.toUpperCase()}`]: model },
|
|
94
96
|
killGraceMs: 300,
|
|
97
|
+
spawnProcess(...args) {
|
|
98
|
+
const child = spawn(...args);
|
|
99
|
+
children.push(child);
|
|
100
|
+
return child;
|
|
101
|
+
},
|
|
102
|
+
killTree(pid, signal) {
|
|
103
|
+
terminations.push({ pid, signal });
|
|
104
|
+
return killProcessTree(pid, signal);
|
|
105
|
+
},
|
|
95
106
|
});
|
|
96
107
|
const enforcer = require("./core/enforcer");
|
|
97
108
|
assert.strictEqual(typeof enforcer.createEnforcerService, "function", "enforcer exposes an injectable utility-provider service");
|
|
@@ -114,8 +125,8 @@ async function providerProbe(kind) {
|
|
|
114
125
|
spawnUtilityAgent: utility.spawnUtilityAgent,
|
|
115
126
|
audit,
|
|
116
127
|
// Bounded (this deadline MUST fire — the child hangs by design), but with
|
|
117
|
-
// enough headroom for the hung
|
|
118
|
-
//
|
|
128
|
+
// enough headroom for the hung children to boot on a loaded CI runner
|
|
129
|
+
// before the deadline kills them; the retry deliberately ignores TERM.
|
|
119
130
|
timeoutMs: 5000,
|
|
120
131
|
});
|
|
121
132
|
const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
|
|
@@ -154,7 +165,19 @@ async function providerProbe(kind) {
|
|
|
154
165
|
assert.ok(record.argv.includes(kind === "claude" ? "plan" : "read-only"));
|
|
155
166
|
assert.ok(record.argv.includes(kind === "claude" ? "--json-schema" : "--output-schema"));
|
|
156
167
|
}
|
|
157
|
-
|
|
168
|
+
// Observe shutdown from the surviving parent. A child-written receipt can
|
|
169
|
+
// be truncated by SIGKILL during synchronous I/O (CI pipeline 12017), even
|
|
170
|
+
// though the guard correctly failed closed and waited for process close.
|
|
171
|
+
assert.strictEqual(children.length, 6);
|
|
172
|
+
for (const child of children.slice(4)) {
|
|
173
|
+
const signals = terminations.filter((entry) => entry.pid === child.pid).map((entry) => entry.signal);
|
|
174
|
+
assert.strictEqual(signals[0], "SIGTERM", "both timeout attempts first request graceful termination");
|
|
175
|
+
assert.ok(["SIGTERM", "SIGKILL"].includes(child.signalCode), "the timed-out child has exited by signal before the verdict settles");
|
|
176
|
+
assert.throws(() => process.kill(child.pid, 0), { code: "ESRCH" }, "the timed-out provider must not survive its verdict");
|
|
177
|
+
}
|
|
178
|
+
assert.ok(terminations.some((entry) => entry.pid === children[5].pid && entry.signal === "SIGKILL"), "the retry that ignores TERM is forcibly terminated after the grace period");
|
|
179
|
+
assert.strictEqual(children[5].signalCode, "SIGKILL");
|
|
180
|
+
assert.deepStrictEqual(utility.stats(), { activeGlobal: 0, activeByPurpose: {} }, "both timed-out attempts release their concurrency slots after close");
|
|
158
181
|
|
|
159
182
|
return { kind, decisions: [allow.decision, block.decision, escalate.decision, malformed.decision, timedOut.decision] };
|
|
160
183
|
}
|
|
@@ -463,8 +463,8 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
463
463
|
// Codex turn. Live probe: unified-exec shell calls still reach PreToolUse as
|
|
464
464
|
// tool_name "Bash" and the deny gate caught the probe, so the gate no longer
|
|
465
465
|
// requires unified_exec off. Push approved by Sumeet before release.
|
|
466
|
-
// 3.4.
|
|
467
|
-
assert.strictEqual(pkg.version, "3.4.
|
|
466
|
+
// 3.4.12: Sumeet approved the full calling rollout on 2026-09-05; preserve assigned media configuration across canary changes.
|
|
467
|
+
assert.strictEqual(pkg.version, "3.4.12", "release version must remain unchanged without explicit approval");
|
|
468
468
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
469
469
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
470
470
|
}
|