@shykaruu/jarvis-brain 0.4.0 → 0.4.2
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 +5 -1
- package/package.json +2 -2
- package/src/comms/websocket.test.ts +33 -0
- package/src/comms/websocket.ts +2 -5
- package/src/config/loader.test.ts +2 -0
- package/src/config/loader.ts +4 -0
- package/src/config/types.ts +2 -0
- package/src/daemon/index.ts +5 -2
- package/src/sidecar/manager.test.ts +60 -0
- package/src/sidecar/manager.ts +50 -10
- package/ui/dist/index-8bxfq0vd.js +112865 -0
- package/ui/dist/index-d9jke6v6.js +112901 -0
- package/ui/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ This means you can run the daemon on an always-on server and still interact with
|
|
|
204
204
|
**Via bun:**
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
bun install -g @
|
|
207
|
+
bun install -g @shykaruu/jarvis-sidecar
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
**Or download the binary** from [GitHub Releases](https://github.com/vierisid/jarvis/releases) for your platform (macOS, Linux, Windows).
|
|
@@ -232,6 +232,8 @@ jarvis-sidecar
|
|
|
232
232
|
|
|
233
233
|
Once connected, the sidecar appears as online in the Settings page where you can configure its capabilities (terminal, filesystem, desktop, browser, clipboard, screenshot, awareness).
|
|
234
234
|
|
|
235
|
+
If your daemon is hosted remotely behind DNS/TLS or a reverse proxy, set `daemon.brain_url` in `~/.jarvis/config.yaml` so sidecar enrollment tokens point at the correct WebSocket endpoint. If unset, JARVIS preserves the existing localhost fallback.
|
|
236
|
+
|
|
235
237
|
---
|
|
236
238
|
|
|
237
239
|
## 🧠 Core Capabilities
|
|
@@ -291,6 +293,8 @@ daemon:
|
|
|
291
293
|
port: 3142
|
|
292
294
|
data_dir: "~/.jarvis"
|
|
293
295
|
db_path: "~/.jarvis/jarvis.db"
|
|
296
|
+
# Optional full sidecar WebSocket URL for remote deployments
|
|
297
|
+
# brain_url: "wss://your-domain.example/sidecar"
|
|
294
298
|
|
|
295
299
|
llm:
|
|
296
300
|
primary: "anthropic"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shykaruu/jarvis-brain",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "J.A.R.V.I.S. — Just A Rather Very Intelligent System. An always-on autonomous AI daemon.",
|
|
5
5
|
"module": "src/daemon/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/
|
|
15
|
+
"url": "git+https://github.com/Shykaruu/jarvis.git"
|
|
16
16
|
},
|
|
17
17
|
"license": "SEE LICENSE IN LICENSE",
|
|
18
18
|
"keywords": ["jarvis", "ai", "daemon", "assistant", "cli"],
|
|
@@ -363,6 +363,39 @@ test('WebSocketServer - WebSocket allowed with auth cookie', async () => {
|
|
|
363
363
|
}
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
test('WebSocketServer - sidecar websocket accepts /sidecar path with bearer auth', async () => {
|
|
367
|
+
const authServer = new WebSocketServer(3156);
|
|
368
|
+
authServer.setSidecarManager({
|
|
369
|
+
async validateToken(token: string) {
|
|
370
|
+
if (token === 'sidecar-token') {
|
|
371
|
+
return { sid: 'sidecar-123' } as any;
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
},
|
|
375
|
+
handleSidecarConnect() {},
|
|
376
|
+
handleSidecarDisconnect() {},
|
|
377
|
+
} as any);
|
|
378
|
+
authServer.start();
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const ws = new WebSocket('ws://localhost:3156/sidecar', {
|
|
382
|
+
headers: { Authorization: 'Bearer sidecar-token' },
|
|
383
|
+
} as any);
|
|
384
|
+
|
|
385
|
+
const connected = await new Promise<boolean>((resolve) => {
|
|
386
|
+
ws.onopen = () => resolve(true);
|
|
387
|
+
ws.onerror = () => resolve(false);
|
|
388
|
+
setTimeout(() => resolve(false), 2000);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(connected).toBe(true);
|
|
392
|
+
ws.close();
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
394
|
+
} finally {
|
|
395
|
+
authServer.stop();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
366
399
|
test('WebSocketServer - sendToClient unicasts JSON', async () => {
|
|
367
400
|
let serverWsRef: any = null;
|
|
368
401
|
|
package/src/comms/websocket.ts
CHANGED
|
@@ -38,6 +38,7 @@ const TOKEN_STRIP_SCRIPT = `<script>(function(){var h=location.hash,i=h.indexOf(
|
|
|
38
38
|
function isPublicRoute(pathname: string, method: string): boolean {
|
|
39
39
|
return (
|
|
40
40
|
pathname === '/health' ||
|
|
41
|
+
pathname === '/sidecar' ||
|
|
41
42
|
pathname === '/sidecar/connect' ||
|
|
42
43
|
pathname === '/api/sidecars/.well-known/jwks.json' ||
|
|
43
44
|
pathname === '/api/auth/login' ||
|
|
@@ -157,7 +158,7 @@ export class WebSocketServer {
|
|
|
157
158
|
const pathname = url.pathname;
|
|
158
159
|
|
|
159
160
|
// 0. Sidecar WebSocket upgrade (has its own JWT auth)
|
|
160
|
-
if (pathname === '/sidecar/connect' && self.sidecarManager) {
|
|
161
|
+
if ((pathname === '/sidecar' || pathname === '/sidecar/connect') && self.sidecarManager) {
|
|
161
162
|
const authHeader = req.headers.get('Authorization');
|
|
162
163
|
const token = authHeader?.startsWith('Bearer ')
|
|
163
164
|
? authHeader.slice(7)
|
|
@@ -257,10 +258,6 @@ export class WebSocketServer {
|
|
|
257
258
|
// (e.g., dev server iframes on different ports attempting ws://localhost:3142/ws)
|
|
258
259
|
if (pathname === '/ws') {
|
|
259
260
|
const origin = req.headers.get('origin');
|
|
260
|
-
const expectedOrigin = self.corsOrigin || `http://localhost:${self.port}`;
|
|
261
|
-
if (origin && origin !== expectedOrigin) {
|
|
262
|
-
return new Response('Forbidden: origin mismatch', { status: 403 });
|
|
263
|
-
}
|
|
264
261
|
const success = server.upgrade(req, { data: {} });
|
|
265
262
|
if (success) return undefined;
|
|
266
263
|
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
@@ -30,6 +30,7 @@ describe('Config Loader', () => {
|
|
|
30
30
|
test('can save and load config', async () => {
|
|
31
31
|
const testConfig = structuredClone(DEFAULT_CONFIG);
|
|
32
32
|
testConfig.daemon.port = 9999;
|
|
33
|
+
testConfig.daemon.brain_url = 'wss://axiom-er.ddns.net/sidecar';
|
|
33
34
|
testConfig.llm.primary = 'openai';
|
|
34
35
|
testConfig.dashboard = { password_hash: '$2b$test-hash' };
|
|
35
36
|
|
|
@@ -38,6 +39,7 @@ describe('Config Loader', () => {
|
|
|
38
39
|
|
|
39
40
|
const loaded = await loadConfig(TEST_CONFIG_PATH);
|
|
40
41
|
expect(loaded.daemon.port).toBe(9999);
|
|
42
|
+
expect(loaded.daemon.brain_url).toBe('wss://axiom-er.ddns.net/sidecar');
|
|
41
43
|
expect(loaded.llm.primary).toBe('openai');
|
|
42
44
|
expect(loaded.dashboard?.password_hash).toBe('$2b$test-hash');
|
|
43
45
|
});
|
package/src/config/loader.ts
CHANGED
|
@@ -89,6 +89,10 @@ function applyEnvOverrides(config: JarvisConfig): void {
|
|
|
89
89
|
config.daemon.brain_domain = env.JARVIS_BRAIN_DOMAIN;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
if (env.JARVIS_BRAIN_URL) {
|
|
93
|
+
config.daemon.brain_url = env.JARVIS_BRAIN_URL;
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
if (env.JARVIS_AUTH_TOKEN) {
|
|
93
97
|
if (!config.auth) config.auth = {};
|
|
94
98
|
config.auth.token = env.JARVIS_AUTH_TOKEN;
|
package/src/config/types.ts
CHANGED
|
@@ -139,6 +139,8 @@ export type JarvisConfig = {
|
|
|
139
139
|
port: number;
|
|
140
140
|
data_dir: string;
|
|
141
141
|
db_path: string;
|
|
142
|
+
/** Full sidecar WebSocket URL (preferred). Example: wss://host/sidecar */
|
|
143
|
+
brain_url?: string;
|
|
142
144
|
/** External domain for the brain (used in sidecar JWT tokens). Env: JARVIS_BRAIN_DOMAIN */
|
|
143
145
|
brain_domain?: string;
|
|
144
146
|
};
|
package/src/daemon/index.ts
CHANGED
|
@@ -321,8 +321,11 @@ export async function startDaemon(userConfig?: Partial<DaemonConfig>): Promise<v
|
|
|
321
321
|
|
|
322
322
|
// 6c. Create sidecar manager
|
|
323
323
|
const sidecarManager = new SidecarManager(jarvisConfig.daemon.data_dir.replace('~', os.homedir()));
|
|
324
|
-
const
|
|
325
|
-
|
|
324
|
+
const configuredBrainUrl =
|
|
325
|
+
jarvisConfig.daemon.brain_url ??
|
|
326
|
+
jarvisConfig.daemon.brain_domain ??
|
|
327
|
+
`localhost:${config.port}`;
|
|
328
|
+
sidecarManager.setBrainUrl(configuredBrainUrl);
|
|
326
329
|
|
|
327
330
|
// 6d. Wire sidecar manager to WebSocket server for WS routing
|
|
328
331
|
wsService.getServer().setSidecarManager(sidecarManager);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { rm } from 'node:fs/promises';
|
|
5
|
+
import { closeDb, initDatabase } from '../vault/schema.ts';
|
|
6
|
+
import { resolveSidecarEndpoints, SidecarManager } from './manager.ts';
|
|
7
|
+
|
|
8
|
+
describe('resolveSidecarEndpoints', () => {
|
|
9
|
+
test('preserves configured websocket URL and derives JWKS URL', () => {
|
|
10
|
+
const endpoints = resolveSidecarEndpoints('wss://axiom-er.ddns.net/sidecar');
|
|
11
|
+
expect(endpoints.brainWs).toBe('wss://axiom-er.ddns.net/sidecar');
|
|
12
|
+
expect(endpoints.jwksUrl).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('derives localhost fallback from host:port', () => {
|
|
16
|
+
const endpoints = resolveSidecarEndpoints('localhost:3142');
|
|
17
|
+
expect(endpoints.brainWs).toBe('ws://localhost:3142/sidecar/connect');
|
|
18
|
+
expect(endpoints.jwksUrl).toBe('http://localhost:3142/api/sidecars/.well-known/jwks.json');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('defaults a bare websocket host to /sidecar/connect', () => {
|
|
22
|
+
const endpoints = resolveSidecarEndpoints('wss://axiom-er.ddns.net');
|
|
23
|
+
expect(endpoints.brainWs).toBe('wss://axiom-er.ddns.net/sidecar/connect');
|
|
24
|
+
expect(endpoints.jwksUrl).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('SidecarManager enrollment', () => {
|
|
29
|
+
let dataDir: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
initDatabase(':memory:');
|
|
33
|
+
dataDir = path.join(os.tmpdir(), `jarvis-sidecar-test-${crypto.randomUUID()}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
closeDb();
|
|
38
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('embeds configured brain_url into enrollment tokens', async () => {
|
|
42
|
+
const manager = new SidecarManager(dataDir);
|
|
43
|
+
manager.setBrainUrl('wss://axiom-er.ddns.net/sidecar');
|
|
44
|
+
await manager.start();
|
|
45
|
+
|
|
46
|
+
const result = await manager.enrollSidecar('remote-laptop');
|
|
47
|
+
|
|
48
|
+
expect(result.brain_url).toBe('wss://axiom-er.ddns.net/sidecar');
|
|
49
|
+
|
|
50
|
+
const payload = JSON.parse(Buffer.from(result.token.split('.')[1]!, 'base64url').toString('utf8')) as {
|
|
51
|
+
brain: string;
|
|
52
|
+
jwks: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(payload.brain).toBe('wss://axiom-er.ddns.net/sidecar');
|
|
56
|
+
expect(payload.jwks).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
|
|
57
|
+
|
|
58
|
+
await manager.stop();
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/sidecar/manager.ts
CHANGED
|
@@ -63,7 +63,11 @@ export class SidecarManager implements Service {
|
|
|
63
63
|
* Example: "shiny-panda.domain.com" or "localhost:3142"
|
|
64
64
|
*/
|
|
65
65
|
setBrainUrl(url: string): void {
|
|
66
|
-
this.brainUrl = url;
|
|
66
|
+
this.brainUrl = url.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getConfiguredBrainUrl(): string {
|
|
70
|
+
return this.brainUrl;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// --------------- Service Interface ---------------
|
|
@@ -227,7 +231,7 @@ export class SidecarManager implements Service {
|
|
|
227
231
|
/**
|
|
228
232
|
* Enroll a new sidecar. Returns the signed JWT enrollment token.
|
|
229
233
|
*/
|
|
230
|
-
async enrollSidecar(name: string): Promise<{ token: string; sidecar: SidecarRecord }> {
|
|
234
|
+
async enrollSidecar(name: string): Promise<{ token: string; sidecar: SidecarRecord; brain_url: string }> {
|
|
231
235
|
if (!this.privateKey) throw new Error('SidecarManager not started');
|
|
232
236
|
if (!this.brainUrl) throw new Error('Brain URL not configured — call setBrainUrl() first');
|
|
233
237
|
|
|
@@ -250,13 +254,7 @@ export class SidecarManager implements Service {
|
|
|
250
254
|
const id = generateId();
|
|
251
255
|
const tokenId = generateId();
|
|
252
256
|
|
|
253
|
-
|
|
254
|
-
const isSecure = !this.brainUrl.includes('localhost') && !this.brainUrl.match(/:\d+$/);
|
|
255
|
-
const wsProtocol = isSecure ? 'wss' : 'ws';
|
|
256
|
-
const httpProtocol = isSecure ? 'https' : 'http';
|
|
257
|
-
|
|
258
|
-
const brainWs = `${wsProtocol}://${this.brainUrl}/sidecar/connect`;
|
|
259
|
-
const jwksUrl = `${httpProtocol}://${this.brainUrl}/api/sidecars/.well-known/jwks.json`;
|
|
257
|
+
const { brainWs, jwksUrl } = resolveSidecarEndpoints(this.brainUrl);
|
|
260
258
|
|
|
261
259
|
// Sign JWT
|
|
262
260
|
const token = await new SignJWT({
|
|
@@ -280,7 +278,7 @@ export class SidecarManager implements Service {
|
|
|
280
278
|
const sidecar = db.query('SELECT * FROM sidecars WHERE id = ?').get(id) as SidecarRecord;
|
|
281
279
|
console.log(`[SidecarManager] Enrolled sidecar "${trimmed}" (${id})`);
|
|
282
280
|
|
|
283
|
-
return { token, sidecar };
|
|
281
|
+
return { token, sidecar, brain_url: brainWs };
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
// --------------- Registry (DB queries) ---------------
|
|
@@ -540,3 +538,45 @@ export class SidecarManager implements Service {
|
|
|
540
538
|
}
|
|
541
539
|
}
|
|
542
540
|
|
|
541
|
+
export function resolveSidecarEndpoints(configuredBrainUrl: string): { brainWs: string; jwksUrl: string } {
|
|
542
|
+
const raw = configuredBrainUrl.trim();
|
|
543
|
+
if (!raw) {
|
|
544
|
+
throw new Error('Brain URL not configured');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!raw.includes('://')) {
|
|
548
|
+
const isLocalHost = raw.startsWith('localhost') || raw.startsWith('127.0.0.1') || raw.startsWith('[::1]');
|
|
549
|
+
const isSecure = !isLocalHost && !raw.match(/:\d+$/);
|
|
550
|
+
const protocol = isSecure ? 'wss' : 'ws';
|
|
551
|
+
const httpProtocol = isSecure ? 'https' : 'http';
|
|
552
|
+
return {
|
|
553
|
+
brainWs: `${protocol}://${raw}/sidecar/connect`,
|
|
554
|
+
jwksUrl: `${httpProtocol}://${raw}/api/sidecars/.well-known/jwks.json`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const brainWsUrl = new URL(raw);
|
|
559
|
+
if (brainWsUrl.protocol === 'http:') {
|
|
560
|
+
brainWsUrl.protocol = 'ws:';
|
|
561
|
+
} else if (brainWsUrl.protocol === 'https:') {
|
|
562
|
+
brainWsUrl.protocol = 'wss:';
|
|
563
|
+
} else if (brainWsUrl.protocol !== 'ws:' && brainWsUrl.protocol !== 'wss:') {
|
|
564
|
+
throw new Error(`Unsupported brain_url protocol: ${brainWsUrl.protocol}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!brainWsUrl.pathname || brainWsUrl.pathname === '/') {
|
|
568
|
+
brainWsUrl.pathname = '/sidecar/connect';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const jwksUrl = new URL(brainWsUrl.toString());
|
|
572
|
+
jwksUrl.protocol = brainWsUrl.protocol === 'wss:' ? 'https:' : 'http:';
|
|
573
|
+
jwksUrl.pathname = '/api/sidecars/.well-known/jwks.json';
|
|
574
|
+
jwksUrl.search = '';
|
|
575
|
+
jwksUrl.hash = '';
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
brainWs: brainWsUrl.toString(),
|
|
579
|
+
jwksUrl: jwksUrl.toString(),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|