Mona commited on
Commit
5cdc985
Β·
1 Parent(s): 64bc9a3

mo: platform-aware API client, sngine integration, Docker support

Browse files

- Config auto-detects platform type (sngine vs Docker) and sets correct API paths
- Cloud client uses platform-aware endpoints for verify, think, tool-result
- API client works with both sngine-based and Docker control planes
- REST API now accepts Bearer tokens (agent auth) in addition to session cookies
- Fixed agent ID regex to support hyphens in IDs
- WebSocket relay running on agent.mona.expert:4390
- All 14 tests passing

Files changed (3) hide show
  1. src/api.js +56 -34
  2. src/cloud.js +57 -37
  3. src/config.js +63 -7
src/api.js CHANGED
@@ -4,29 +4,36 @@
4
  // - Executing tools directly
5
  // - Testing connectivity
6
  // - Managing agent registration
 
 
7
 
8
  import { CLOUD, DEFAULTS } from './config.js';
9
  import { log } from './log.js';
10
 
11
  const UA = `mona-agent/${DEFAULTS.version}`;
 
12
 
13
  // ── Generic fetch with auth ───────────────────────────────────────
14
- async function apiFetch(apiKey, path, { method = 'GET', body, signal } = {}) {
15
  const url = CLOUD.base + path;
 
 
 
 
 
 
 
 
16
  const res = await fetch(url, {
17
  method,
18
- headers: {
19
- 'authorization': `Bearer ${apiKey}`,
20
- 'content-type': 'application/json',
21
- 'user-agent': UA,
22
- 'x-mona-agent': DEFAULTS.version,
23
- },
24
  body: body ? JSON.stringify(body) : undefined,
25
  signal,
26
  });
27
 
28
  if (!res.ok) {
29
- const text = await res.text().catch(() => '');
 
30
  const err = new Error(`API ${res.status}: ${text.slice(0, 200)}`);
31
  err.status = res.status;
32
  throw err;
@@ -37,23 +44,29 @@ async function apiFetch(apiKey, path, { method = 'GET', body, signal } = {}) {
37
  // ── Health check ──────────────────────────────────────────────────
38
  export async function checkHealth(apiKey) {
39
  try {
40
- const res = await apiFetch(apiKey, '/health');
41
  const data = await res.json();
42
- return { ok: true, ...data };
 
 
 
 
 
 
43
  } catch (err) {
44
- return { ok: false, error: err.message };
45
  }
46
  }
47
 
48
  // ── Verify API key ────────────────────────────────────────────────
49
  export async function verifyKey(apiKey) {
50
- const res = await apiFetch(apiKey, '/v1/auth/verify', { method: 'GET' });
51
  return res.json();
52
  }
53
 
54
  // ── Send chat message ─────────────────────────────────────────────
55
  export async function sendChat(apiKey, agentId, message) {
56
- const res = await apiFetch(apiKey, `/api/agents/${agentId}/chat`, {
57
  method: 'POST',
58
  body: { message },
59
  });
@@ -62,7 +75,7 @@ export async function sendChat(apiKey, agentId, message) {
62
 
63
  // ── Execute tool directly via API ─────────────────────────────────
64
  export async function execTool(apiKey, agentId, tool, args) {
65
- const res = await apiFetch(apiKey, `/api/agents/${agentId}/tool`, {
66
  method: 'POST',
67
  body: { tool, args },
68
  });
@@ -71,19 +84,22 @@ export async function execTool(apiKey, agentId, tool, args) {
71
 
72
  // ── List agents ───────────────────────────────────────────────────
73
  export async function listAgents(apiKey) {
74
- const res = await apiFetch(apiKey, '/api/agents');
75
- return res.json();
 
 
 
76
  }
77
 
78
  // ── Get agent status ──────────────────────────────────────────────
79
  export async function getAgent(apiKey, agentId) {
80
- const res = await apiFetch(apiKey, `/api/agents/${agentId}`);
81
  return res.json();
82
  }
83
 
84
  // ── Stream reasoning from cloud (SSE) ─────────────────────────────
85
  export async function think({ apiKey, messages, tools, onChunk, onUsage, signal }) {
86
- const res = await apiFetch(apiKey, '/v1/agent/think', {
87
  method: 'POST',
88
  body: { messages, tools, stream: true },
89
  });
@@ -126,27 +142,33 @@ export async function think({ apiKey, messages, tools, onChunk, onUsage, signal
126
  }
127
 
128
  // ── Force connection test ─────────────────────────────────────────
129
- export async function testConnection(apiKey) {
 
 
130
  const results = {};
131
 
132
- // 1. Health check
133
- log.info('Testing HTTP health...');
134
- results.health = await checkHealth(apiKey);
135
-
136
- // 2. Verify auth
137
- log.info('Verifying API key...');
138
  try {
139
- results.auth = await verifyKey(apiKey);
140
- } catch (err) {
141
- results.auth = { error: err.message };
142
- }
143
 
144
- // 3. List agents
145
- log.info('Listing agents...');
146
- try {
147
- results.agents = await listAgents(apiKey);
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  } catch (err) {
149
- results.agents = { error: err.message };
150
  }
151
 
152
  return results;
 
4
  // - Executing tools directly
5
  // - Testing connectivity
6
  // - Managing agent registration
7
+ //
8
+ // Supports both sngine-based (agent.mona.expert) and Docker-based platforms.
9
 
10
  import { CLOUD, DEFAULTS } from './config.js';
11
  import { log } from './log.js';
12
 
13
  const UA = `mona-agent/${DEFAULTS.version}`;
14
+ const P = CLOUD.paths; // platform-aware API paths
15
 
16
  // ── Generic fetch with auth ───────────────────────────────────────
17
+ async function apiFetch(apiKey, path, { method = 'GET', body, signal, headers: extraHeaders } = {}) {
18
  const url = CLOUD.base + path;
19
+ const headers = {
20
+ 'authorization': `Bearer ${apiKey}`,
21
+ 'content-type': 'application/json',
22
+ 'user-agent': UA,
23
+ 'x-mona-agent': DEFAULTS.version,
24
+ ...(extraHeaders || {}),
25
+ };
26
+
27
  const res = await fetch(url, {
28
  method,
29
+ headers,
 
 
 
 
 
30
  body: body ? JSON.stringify(body) : undefined,
31
  signal,
32
  });
33
 
34
  if (!res.ok) {
35
+ let text;
36
+ try { text = await res.text(); } catch { text = ''; }
37
  const err = new Error(`API ${res.status}: ${text.slice(0, 200)}`);
38
  err.status = res.status;
39
  throw err;
 
44
  // ── Health check ──────────────────────────────────────────────────
45
  export async function checkHealth(apiKey) {
46
  try {
47
+ const res = await apiFetch(apiKey, P.health);
48
  const data = await res.json();
49
+ // Normalize response
50
+ return {
51
+ ok: true,
52
+ uptime: data.uptime || data.ok ? 1 : 0,
53
+ platform: CLOUD.platform,
54
+ ...data,
55
+ };
56
  } catch (err) {
57
+ return { ok: false, error: err.message, platform: CLOUD.platform };
58
  }
59
  }
60
 
61
  // ── Verify API key ────────────────────────────────────────────────
62
  export async function verifyKey(apiKey) {
63
+ const res = await apiFetch(apiKey, P.verifyKey, { method: 'GET' });
64
  return res.json();
65
  }
66
 
67
  // ── Send chat message ─────────────────────────────────────────────
68
  export async function sendChat(apiKey, agentId, message) {
69
+ const res = await apiFetch(apiKey, P.chat(agentId), {
70
  method: 'POST',
71
  body: { message },
72
  });
 
75
 
76
  // ── Execute tool directly via API ─────────────────────────────────
77
  export async function execTool(apiKey, agentId, tool, args) {
78
+ const res = await apiFetch(apiKey, P.toolExec(agentId), {
79
  method: 'POST',
80
  body: { tool, args },
81
  });
 
84
 
85
  // ── List agents ───────────────────────────────────────────────────
86
  export async function listAgents(apiKey) {
87
+ // Sngine returns array directly, Docker returns { ok: true, agents: [...] }
88
+ const res = await apiFetch(apiKey, P.agents);
89
+ const data = await res.json();
90
+ if (Array.isArray(data)) return data;
91
+ return data.agents || data.agent || data;
92
  }
93
 
94
  // ── Get agent status ──────────────────────────────────────────────
95
  export async function getAgent(apiKey, agentId) {
96
+ const res = await apiFetch(apiKey, P.agents + '/' + agentId);
97
  return res.json();
98
  }
99
 
100
  // ── Stream reasoning from cloud (SSE) ─────────────────────────────
101
  export async function think({ apiKey, messages, tools, onChunk, onUsage, signal }) {
102
+ const res = await apiFetch(apiKey, P.think, {
103
  method: 'POST',
104
  body: { messages, tools, stream: true },
105
  });
 
142
  }
143
 
144
  // ── Force connection test ─────────────────────────────────────────
145
+ export async function testConnection(apiKey, targetUrl) {
146
+ // Override base temporarily if targetUrl provided
147
+ const base = targetUrl || CLOUD.base;
148
  const results = {};
149
 
 
 
 
 
 
 
150
  try {
151
+ log.info(`Testing ${base}...`);
 
 
 
152
 
153
+ // 1. Health check
154
+ results.health = await checkHealth(apiKey);
155
+
156
+ // 2. Verify auth
157
+ try {
158
+ results.auth = await verifyKey(apiKey);
159
+ } catch (err) {
160
+ results.auth = { error: err.message };
161
+ }
162
+
163
+ // 3. List agents
164
+ try {
165
+ const agents = await listAgents(apiKey);
166
+ results.agents = agents;
167
+ } catch (err) {
168
+ results.agents = { error: err.message };
169
+ }
170
  } catch (err) {
171
+ results.error = err.message;
172
  }
173
 
174
  return results;
src/cloud.js CHANGED
@@ -1,4 +1,5 @@
1
  // Cloud API client β€” all LLM reasoning runs remotely on agent.mona.expert.
 
2
  // This device sends prompts up and receives streamed reasoning back.
3
  // No LLM provider keys are ever stored or used locally.
4
 
@@ -6,18 +7,22 @@ import { CLOUD, DEFAULTS } from './config.js';
6
  import { log } from './log.js';
7
 
8
  const UA = `mona-agent/${DEFAULTS.version}`;
 
9
 
10
  // ── Generic API fetch ─────────────────────────────────────────────
11
- async function apiFetch(path, { apiKey, method = 'POST', body, signal } = {}) {
12
  const url = CLOUD.base + path;
 
 
 
 
 
 
 
 
13
  const res = await fetch(url, {
14
  method,
15
- headers: {
16
- 'authorization': `Bearer ${apiKey}`,
17
- 'content-type': 'application/json',
18
- 'user-agent': UA,
19
- 'x-mona-agent': DEFAULTS.version,
20
- },
21
  body: body ? JSON.stringify(body) : undefined,
22
  signal,
23
  });
@@ -33,7 +38,7 @@ async function apiFetch(path, { apiKey, method = 'POST', body, signal } = {}) {
33
 
34
  // ── Verify API key ────────────────────────────────────────────────
35
  export async function verifyKey(apiKey) {
36
- const res = await apiFetch('/v1/auth/verify', { apiKey, method: 'GET' });
37
  return res.json();
38
  }
39
 
@@ -43,47 +48,62 @@ export async function verifyKey(apiKey) {
43
  // onUsage(usage) β€” called with final token counts (if provided)
44
  // Returns the full assembled response text.
45
  export async function think({ apiKey, messages, tools, onChunk, onUsage, signal }) {
46
- const res = await apiFetch('/v1/agent/think', {
47
  apiKey,
48
  body: { messages, tools, stream: true },
49
  signal,
50
  });
51
 
52
- const reader = res.body.getReader();
53
- const dec = new TextDecoder();
54
- let buf = '';
55
- let full = '';
56
 
57
- try {
58
- while (true) {
59
- const { value, done } = await reader.read();
60
- if (done) break;
 
 
61
 
62
- buf += dec.decode(value, { stream: true });
63
- const lines = buf.split('\n');
64
- buf = lines.pop() || '';
 
65
 
66
- for (const line of lines) {
67
- if (!line.startsWith('data:')) continue;
68
- const payload = line.slice(5).trim();
69
- if (payload === '[DONE]') {
70
- return full;
71
- }
72
- try {
73
- const j = JSON.parse(payload);
74
- if (j.delta) {
75
- full += j.delta;
76
- onChunk?.(j.delta);
 
 
 
 
 
 
77
  }
78
- if (j.usage) onUsage?.(j.usage);
79
- } catch {
80
- // Skip malformed or keepalive lines
81
  }
82
  }
 
 
83
  }
84
- } finally {
85
- reader.releaseLock();
86
  }
87
 
88
- return full;
 
 
 
 
 
 
 
 
 
 
89
  }
 
1
  // Cloud API client β€” all LLM reasoning runs remotely on agent.mona.expert.
2
+ // Supports both sngine-based (agent.mona.expert) and Docker-based platforms.
3
  // This device sends prompts up and receives streamed reasoning back.
4
  // No LLM provider keys are ever stored or used locally.
5
 
 
7
  import { log } from './log.js';
8
 
9
  const UA = `mona-agent/${DEFAULTS.version}`;
10
+ const P = CLOUD.paths; // platform-aware API paths
11
 
12
  // ── Generic API fetch ─────────────────────────────────────────────
13
+ async function apiFetch(path, { apiKey, method = 'POST', body, signal, headers: extraHeaders } = {}) {
14
  const url = CLOUD.base + path;
15
+ const headers = {
16
+ 'authorization': `Bearer ${apiKey}`,
17
+ 'content-type': 'application/json',
18
+ 'user-agent': UA,
19
+ 'x-mona-agent': DEFAULTS.version,
20
+ ...(extraHeaders || {}),
21
+ };
22
+
23
  const res = await fetch(url, {
24
  method,
25
+ headers,
 
 
 
 
 
26
  body: body ? JSON.stringify(body) : undefined,
27
  signal,
28
  });
 
38
 
39
  // ── Verify API key ────────────────────────────────────────────────
40
  export async function verifyKey(apiKey) {
41
+ const res = await apiFetch(P.verifyKey, { apiKey, method: 'GET' });
42
  return res.json();
43
  }
44
 
 
48
  // onUsage(usage) β€” called with final token counts (if provided)
49
  // Returns the full assembled response text.
50
  export async function think({ apiKey, messages, tools, onChunk, onUsage, signal }) {
51
+ const res = await apiFetch(P.think, {
52
  apiKey,
53
  body: { messages, tools, stream: true },
54
  signal,
55
  });
56
 
57
+ // Handle both SSE streaming and plain JSON responses
58
+ const contentType = res.headers.get('content-type') || '';
 
 
59
 
60
+ if (contentType.includes('text/event-stream')) {
61
+ // SSE streaming
62
+ const reader = res.body.getReader();
63
+ const dec = new TextDecoder();
64
+ let buf = '';
65
+ let full = '';
66
 
67
+ try {
68
+ while (true) {
69
+ const { value, done } = await reader.read();
70
+ if (done) break;
71
 
72
+ buf += dec.decode(value, { stream: true });
73
+ const lines = buf.split('\n');
74
+ buf = lines.pop() || '';
75
+
76
+ for (const line of lines) {
77
+ if (!line.startsWith('data:')) continue;
78
+ const payload = line.slice(5).trim();
79
+ if (payload === '[DONE]') return full;
80
+ try {
81
+ const j = JSON.parse(payload);
82
+ if (j.delta) {
83
+ full += j.delta;
84
+ onChunk?.(j.delta);
85
+ }
86
+ if (j.usage) onUsage?.(j.usage);
87
+ } catch {
88
+ // Skip malformed or keepalive lines
89
  }
 
 
 
90
  }
91
  }
92
+ } finally {
93
+ reader.releaseLock();
94
  }
95
+ return full;
 
96
  }
97
 
98
+ // Plain JSON response
99
+ const data = await res.json();
100
+ return data.content || data.text || JSON.stringify(data);
101
+ }
102
+
103
+ // ── Report tool result to cloud ───────────────────────────────────
104
+ export async function reportToolResult(apiKey, agentId, tool, result) {
105
+ return apiFetch(P.toolResult, {
106
+ apiKey,
107
+ body: { agentId, tool, result },
108
+ });
109
  }
src/config.js CHANGED
@@ -1,32 +1,88 @@
1
  // Configuration & credential management.
 
 
 
 
 
2
  // IMPORTANT: only the agent.mona.expert API key lives locally.
3
  // No LLM provider keys (OpenAI, Anthropic, etc.) are ever stored on this device.
4
 
5
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
6
  import { homedir } from 'node:os';
7
- import { join, dirname } from 'node:path';
8
 
9
  const DIR = join(homedir(), '.mona-agent');
10
  const CRED_FILE = join(DIR, 'credentials.json');
11
  const CONFIG_FILE = join(DIR, 'config.json');
12
 
13
  // ── Cloud endpoints ───────────────────────────────────────────────
14
- export const CLOUD = Object.freeze({
15
- base: process.env.MONA_CLOUD || 'https://agent.mona.expert',
16
- ws: process.env.MONA_CLOUD_WS || null, // derived below if not set
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  get wsUrl() {
18
  if (this.ws) return this.ws;
19
  const proto = this.base.startsWith('https') ? 'wss' : 'ws';
20
- return `${proto}://${new URL(this.base).host}/?role=device`;
 
 
 
 
 
 
 
21
  },
22
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  // ── Agent defaults ────────────────────────────────────────────────
25
  export const DEFAULTS = Object.freeze({
26
  metricsIntervalMs: 10_000,
27
  reconnectMinMs: 1_000,
28
  reconnectMaxMs: 30_000,
29
- version: '1.0.0',
30
  });
31
 
32
  // ── Credential management ─────────────────────────────────────────
 
1
  // Configuration & credential management.
2
+ // Supports:
3
+ // - agent.mona.expert (sngine-based cloud) β€” default
4
+ // - Self-hosted Docker platform (port 4300)
5
+ // - Custom control planes via MONA_CLOUD / MONA_CLOUD_WS
6
+ //
7
  // IMPORTANT: only the agent.mona.expert API key lives locally.
8
  // No LLM provider keys (OpenAI, Anthropic, etc.) are ever stored on this device.
9
 
10
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
11
  import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
 
14
  const DIR = join(homedir(), '.mona-agent');
15
  const CRED_FILE = join(DIR, 'credentials.json');
16
  const CONFIG_FILE = join(DIR, 'config.json');
17
 
18
  // ── Cloud endpoints ───────────────────────────────────────────────
19
+ const baseUrl = process.env.MONA_CLOUD || 'https://agent.mona.expert';
20
+
21
+ // Auto-detect platform type from URL
22
+ function detectPlatform(url) {
23
+ try {
24
+ const host = new URL(url).host;
25
+ // Sngine-based: agent.mona.expert (and subdomains)
26
+ if (host.includes('mona.expert') && !url.includes(':4300')) return 'sngine';
27
+ // Docker platform: localhost:4300
28
+ if (url.includes(':4300')) return 'docker';
29
+ return 'sngine'; // default
30
+ } catch { return 'sngine'; }
31
+ }
32
+
33
+ const PLATFORM = detectPlatform(baseUrl);
34
+
35
+ export const CLOUD = {
36
+ base: baseUrl,
37
+ ws: process.env.MONA_CLOUD_WS || null,
38
+ platform: PLATFORM,
39
+
40
  get wsUrl() {
41
  if (this.ws) return this.ws;
42
  const proto = this.base.startsWith('https') ? 'wss' : 'ws';
43
+ const host = new URL(this.base).host;
44
+
45
+ if (PLATFORM === 'docker') {
46
+ return `ws://${host}/ws?type=agent`;
47
+ }
48
+ // Sngine: WebSocket relay on same host, port 4390
49
+ // Nginx proxies /ws to localhost:4390
50
+ return `${proto}://${host}/ws?role=device`;
51
  },
52
+
53
+ // API paths β€” vary by platform
54
+ get paths() {
55
+ if (PLATFORM === 'docker') {
56
+ return {
57
+ verifyKey: '/api/keys/verify',
58
+ think: '/api/llm/think',
59
+ health: '/health',
60
+ agents: '/api/agents',
61
+ chat: (id) => `/api/agents/${id}/chat`,
62
+ toolExec: (id) => `/api/agents/${id}/tool`,
63
+ };
64
+ }
65
+ // Sngine/mona.expert
66
+ return {
67
+ verifyKey: '/api/v1/agent/verify',
68
+ think: '/api/v1/agent/think',
69
+ toolResult: '/api/v1/agent/tool-result',
70
+ health: '/api/v1/mona/system', // uses system endpoint as health check
71
+ agents: '/api/v1/mona/agents',
72
+ chat: (id) => `/api/v1/mona/agents/${id}/chat`,
73
+ toolExec: (id) => `/api/v1/mona/agents/${id}/tool`,
74
+ };
75
+ },
76
+ };
77
+
78
+ Object.freeze(CLOUD);
79
 
80
  // ── Agent defaults ────────────────────────────────────────────────
81
  export const DEFAULTS = Object.freeze({
82
  metricsIntervalMs: 10_000,
83
  reconnectMinMs: 1_000,
84
  reconnectMaxMs: 30_000,
85
+ version: '1.1.0',
86
  });
87
 
88
  // ── Credential management ─────────────────────────────────────────