@agentmeshhq/agent 0.1.4 → 0.1.6
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/dist/__tests__/jwt.test.d.ts +1 -0
- package/dist/__tests__/jwt.test.js +83 -0
- package/dist/__tests__/jwt.test.js.map +1 -0
- package/dist/__tests__/loader.test.d.ts +1 -0
- package/dist/__tests__/loader.test.js +148 -0
- package/dist/__tests__/loader.test.js.map +1 -0
- package/dist/cli/index.js +37 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/token.d.ts +1 -0
- package/dist/cli/token.js +146 -0
- package/dist/cli/token.js.map +1 -0
- package/dist/cli/whoami.d.ts +1 -0
- package/dist/cli/whoami.js +98 -0
- package/dist/cli/whoami.js.map +1 -0
- package/dist/config/loader.d.ts +2 -1
- package/dist/config/loader.js +13 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/core/daemon.js +50 -6
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/heartbeat.d.ts +8 -0
- package/dist/core/heartbeat.js +50 -1
- package/dist/core/heartbeat.js.map +1 -1
- package/dist/core/tmux.d.ts +7 -1
- package/dist/core/tmux.js +31 -2
- package/dist/core/tmux.js.map +1 -1
- package/dist/utils/jwt.d.ts +36 -0
- package/dist/utils/jwt.js +70 -0
- package/dist/utils/jwt.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/jwt.test.ts +112 -0
- package/src/__tests__/loader.test.ts +191 -0
- package/src/cli/index.ts +38 -5
- package/src/cli/token.ts +188 -0
- package/src/cli/whoami.ts +113 -0
- package/src/config/loader.ts +20 -7
- package/src/core/daemon.ts +64 -18
- package/src/core/heartbeat.ts +62 -1
- package/src/core/tmux.ts +46 -9
- package/src/utils/jwt.ts +87 -0
- package/vitest.config.ts +12 -0
package/src/core/daemon.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { createSession, destroySession, sessionExists, getSessionName } from "./tmux.js";
|
|
2
|
-
import { AgentWebSocket } from "./websocket.js";
|
|
3
|
-
import { Heartbeat } from "./heartbeat.js";
|
|
4
|
-
import { registerAgent, checkInbox } from "./registry.js";
|
|
5
|
-
import { injectStartupMessage, handleWebSocketEvent } from "./injector.js";
|
|
6
1
|
import {
|
|
7
2
|
addAgentToState,
|
|
8
|
-
removeAgentFromState,
|
|
9
|
-
loadConfig,
|
|
10
3
|
getAgentState,
|
|
4
|
+
loadConfig,
|
|
5
|
+
removeAgentFromState,
|
|
6
|
+
updateAgentInState,
|
|
11
7
|
} from "../config/loader.js";
|
|
12
|
-
import type {
|
|
8
|
+
import type { AgentConfig, Config } from "../config/schema.js";
|
|
9
|
+
import { Heartbeat } from "./heartbeat.js";
|
|
10
|
+
import { handleWebSocketEvent, injectStartupMessage } from "./injector.js";
|
|
11
|
+
import { checkInbox, registerAgent } from "./registry.js";
|
|
12
|
+
import {
|
|
13
|
+
createSession,
|
|
14
|
+
destroySession,
|
|
15
|
+
getSessionName,
|
|
16
|
+
sessionExists,
|
|
17
|
+
updateSessionEnvironment,
|
|
18
|
+
} from "./tmux.js";
|
|
19
|
+
import { AgentWebSocket } from "./websocket.js";
|
|
13
20
|
|
|
14
21
|
export interface DaemonOptions {
|
|
15
22
|
name: string;
|
|
@@ -32,9 +39,7 @@ export class AgentDaemon {
|
|
|
32
39
|
constructor(options: DaemonOptions) {
|
|
33
40
|
const config = loadConfig();
|
|
34
41
|
if (!config) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
"No config found. Run 'agentmesh init' first."
|
|
37
|
-
);
|
|
42
|
+
throw new Error("No config found. Run 'agentmesh init' first.");
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
this.config = config;
|
|
@@ -78,7 +83,7 @@ export class AgentDaemon {
|
|
|
78
83
|
const created = createSession(
|
|
79
84
|
this.agentName,
|
|
80
85
|
this.agentConfig.command,
|
|
81
|
-
this.agentConfig.workdir
|
|
86
|
+
this.agentConfig.workdir,
|
|
82
87
|
);
|
|
83
88
|
|
|
84
89
|
if (!created) {
|
|
@@ -106,6 +111,13 @@ export class AgentDaemon {
|
|
|
106
111
|
|
|
107
112
|
console.log(`Registered as: ${this.agentId}`);
|
|
108
113
|
|
|
114
|
+
// Inject environment variables into tmux session
|
|
115
|
+
console.log("Injecting environment variables...");
|
|
116
|
+
updateSessionEnvironment(this.agentName, {
|
|
117
|
+
AGENT_TOKEN: this.token,
|
|
118
|
+
AGENTMESH_AGENT_ID: this.agentId,
|
|
119
|
+
});
|
|
120
|
+
|
|
109
121
|
// Save state
|
|
110
122
|
addAgentToState({
|
|
111
123
|
name: this.agentName,
|
|
@@ -116,15 +128,53 @@ export class AgentDaemon {
|
|
|
116
128
|
token: this.token,
|
|
117
129
|
});
|
|
118
130
|
|
|
119
|
-
// Start heartbeat
|
|
131
|
+
// Start heartbeat with auto-refresh
|
|
120
132
|
console.log("Starting heartbeat...");
|
|
121
133
|
this.heartbeat = new Heartbeat({
|
|
122
134
|
url: this.config.hubUrl,
|
|
123
135
|
token: this.token,
|
|
124
136
|
intervalMs: 30000,
|
|
137
|
+
agentName: this.agentName,
|
|
138
|
+
agentId: this.agentId,
|
|
139
|
+
apiKey: this.config.apiKey,
|
|
140
|
+
workspace: this.config.workspace,
|
|
125
141
|
onError: (error) => {
|
|
126
142
|
console.error("Heartbeat error:", error.message);
|
|
127
143
|
},
|
|
144
|
+
onTokenRefresh: (newToken) => {
|
|
145
|
+
this.token = newToken;
|
|
146
|
+
// Update state file
|
|
147
|
+
updateAgentInState(this.agentName, { token: newToken });
|
|
148
|
+
// Update tmux environment
|
|
149
|
+
updateSessionEnvironment(this.agentName, {
|
|
150
|
+
AGENT_TOKEN: newToken,
|
|
151
|
+
AGENTMESH_AGENT_ID: this.agentId!,
|
|
152
|
+
});
|
|
153
|
+
// Reconnect WebSocket with new token
|
|
154
|
+
if (this.ws) {
|
|
155
|
+
this.ws.disconnect();
|
|
156
|
+
const wsUrl = this.config.hubUrl
|
|
157
|
+
.replace("https://", "wss://")
|
|
158
|
+
.replace("http://", "ws://");
|
|
159
|
+
this.ws = new AgentWebSocket({
|
|
160
|
+
url: `${wsUrl}/ws/v1`,
|
|
161
|
+
token: newToken,
|
|
162
|
+
onMessage: (event) => {
|
|
163
|
+
handleWebSocketEvent(this.agentName, event);
|
|
164
|
+
},
|
|
165
|
+
onConnect: () => {
|
|
166
|
+
console.log("WebSocket reconnected with new token");
|
|
167
|
+
},
|
|
168
|
+
onDisconnect: () => {
|
|
169
|
+
console.log("WebSocket disconnected");
|
|
170
|
+
},
|
|
171
|
+
onError: (error) => {
|
|
172
|
+
console.error("WebSocket error:", error.message);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
this.ws.connect();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
128
178
|
});
|
|
129
179
|
this.heartbeat.start();
|
|
130
180
|
|
|
@@ -153,11 +203,7 @@ export class AgentDaemon {
|
|
|
153
203
|
// Check inbox and auto-nudge
|
|
154
204
|
console.log("Checking inbox...");
|
|
155
205
|
try {
|
|
156
|
-
const inboxItems = await checkInbox(
|
|
157
|
-
this.config.hubUrl,
|
|
158
|
-
this.config.workspace,
|
|
159
|
-
this.token
|
|
160
|
-
);
|
|
206
|
+
const inboxItems = await checkInbox(this.config.hubUrl, this.config.workspace, this.token);
|
|
161
207
|
injectStartupMessage(this.agentName, inboxItems.length);
|
|
162
208
|
} catch (error) {
|
|
163
209
|
console.error("Failed to check inbox:", error);
|
package/src/core/heartbeat.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { getTokenTimeRemaining, isTokenExpiringSoon } from "../utils/jwt.js";
|
|
2
|
+
|
|
3
|
+
// Refresh token when less than 24 hours remain
|
|
4
|
+
const TOKEN_REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
1
6
|
export interface HeartbeatConfig {
|
|
2
7
|
url: string;
|
|
3
8
|
token: string;
|
|
4
9
|
intervalMs: number;
|
|
10
|
+
agentName: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
workspace: string;
|
|
5
14
|
onError?: (error: Error) => void;
|
|
15
|
+
onTokenRefresh?: (newToken: string) => void;
|
|
6
16
|
}
|
|
7
17
|
|
|
8
18
|
export class Heartbeat {
|
|
9
19
|
private config: HeartbeatConfig;
|
|
20
|
+
private currentToken: string;
|
|
10
21
|
private intervalId: NodeJS.Timeout | null = null;
|
|
11
22
|
|
|
12
23
|
constructor(config: HeartbeatConfig) {
|
|
13
24
|
this.config = config;
|
|
25
|
+
this.currentToken = config.token;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
start(): void {
|
|
@@ -34,22 +46,71 @@ export class Heartbeat {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
getToken(): string {
|
|
50
|
+
return this.currentToken;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
private async sendHeartbeat(): Promise<void> {
|
|
38
54
|
try {
|
|
55
|
+
// Check if token needs refresh before sending heartbeat
|
|
56
|
+
if (isTokenExpiringSoon(this.currentToken, TOKEN_REFRESH_THRESHOLD_MS)) {
|
|
57
|
+
const remaining = getTokenTimeRemaining(this.currentToken);
|
|
58
|
+
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
|
59
|
+
console.log(`Token expiring in ${hours} hours, refreshing...`);
|
|
60
|
+
await this.refreshToken();
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
const response = await fetch(`${this.config.url}/api/v1/agents/heartbeat`, {
|
|
40
64
|
method: "POST",
|
|
41
65
|
headers: {
|
|
42
|
-
Authorization: `Bearer ${this.
|
|
66
|
+
Authorization: `Bearer ${this.currentToken}`,
|
|
43
67
|
"Content-Type": "application/json",
|
|
44
68
|
},
|
|
45
69
|
body: JSON.stringify({}),
|
|
46
70
|
});
|
|
47
71
|
|
|
48
72
|
if (!response.ok) {
|
|
73
|
+
// If unauthorized, try to refresh token
|
|
74
|
+
if (response.status === 401) {
|
|
75
|
+
console.log("Token rejected, attempting refresh...");
|
|
76
|
+
await this.refreshToken();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
49
79
|
throw new Error(`Heartbeat failed: ${response.status}`);
|
|
50
80
|
}
|
|
51
81
|
} catch (error) {
|
|
52
82
|
this.config.onError?.(error as Error);
|
|
53
83
|
}
|
|
54
84
|
}
|
|
85
|
+
|
|
86
|
+
private async refreshToken(): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${this.config.url}/api/v1/agents/register`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"x-agentmesh-secret": this.config.apiKey,
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
agent_id: this.config.agentId,
|
|
96
|
+
workspace: this.config.workspace,
|
|
97
|
+
display_name: this.config.agentName,
|
|
98
|
+
model: "claude-sonnet-4",
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = (await response.json()) as { token: string };
|
|
107
|
+
this.currentToken = data.token;
|
|
108
|
+
|
|
109
|
+
console.log("Token refreshed successfully");
|
|
110
|
+
this.config.onTokenRefresh?.(data.token);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("Failed to refresh token:", (error as Error).message);
|
|
113
|
+
this.config.onError?.(error as Error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
55
116
|
}
|
package/src/core/tmux.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync, spawn
|
|
1
|
+
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
const SESSION_PREFIX = "agentmesh-";
|
|
4
4
|
|
|
@@ -15,10 +15,16 @@ export function sessionExists(sessionName: string): boolean {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface SessionEnv {
|
|
19
|
+
AGENT_TOKEN?: string;
|
|
20
|
+
AGENTMESH_AGENT_ID?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export function createSession(
|
|
19
24
|
agentName: string,
|
|
20
25
|
command: string,
|
|
21
|
-
workdir?: string
|
|
26
|
+
workdir?: string,
|
|
27
|
+
env?: SessionEnv,
|
|
22
28
|
): boolean {
|
|
23
29
|
const sessionName = getSessionName(agentName);
|
|
24
30
|
|
|
@@ -37,6 +43,12 @@ export function createSession(
|
|
|
37
43
|
args.push(command);
|
|
38
44
|
|
|
39
45
|
execSync(`tmux ${args.join(" ")}`);
|
|
46
|
+
|
|
47
|
+
// Inject environment variables after session creation
|
|
48
|
+
if (env) {
|
|
49
|
+
setSessionEnvironment(sessionName, env);
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
return true;
|
|
41
53
|
} catch (error) {
|
|
42
54
|
console.error(`Failed to create tmux session: ${error}`);
|
|
@@ -44,6 +56,34 @@ export function createSession(
|
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
export function setSessionEnvironment(sessionName: string, env: SessionEnv): boolean {
|
|
60
|
+
try {
|
|
61
|
+
if (env.AGENT_TOKEN) {
|
|
62
|
+
execSync(`tmux set-environment -t "${sessionName}" AGENT_TOKEN "${env.AGENT_TOKEN}"`);
|
|
63
|
+
}
|
|
64
|
+
if (env.AGENTMESH_AGENT_ID) {
|
|
65
|
+
execSync(
|
|
66
|
+
`tmux set-environment -t "${sessionName}" AGENTMESH_AGENT_ID "${env.AGENTMESH_AGENT_ID}"`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`Failed to set session environment: ${error}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function updateSessionEnvironment(agentName: string, env: SessionEnv): boolean {
|
|
77
|
+
const sessionName = getSessionName(agentName);
|
|
78
|
+
|
|
79
|
+
if (!sessionExists(sessionName)) {
|
|
80
|
+
console.error(`Session ${sessionName} does not exist`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return setSessionEnvironment(sessionName, env);
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
export function destroySession(agentName: string): boolean {
|
|
48
88
|
const sessionName = getSessionName(agentName);
|
|
49
89
|
|
|
@@ -119,9 +159,7 @@ export function listSessions(): string[] {
|
|
|
119
159
|
}
|
|
120
160
|
}
|
|
121
161
|
|
|
122
|
-
export function getSessionInfo(
|
|
123
|
-
agentName: string
|
|
124
|
-
): { exists: boolean; command?: string } {
|
|
162
|
+
export function getSessionInfo(agentName: string): { exists: boolean; command?: string } {
|
|
125
163
|
const sessionName = getSessionName(agentName);
|
|
126
164
|
|
|
127
165
|
if (!sessionExists(sessionName)) {
|
|
@@ -129,10 +167,9 @@ export function getSessionInfo(
|
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
try {
|
|
132
|
-
const command = execSync(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
).trim();
|
|
170
|
+
const command = execSync(`tmux list-panes -t "${sessionName}" -F "#{pane_current_command}"`, {
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
}).trim();
|
|
136
173
|
|
|
137
174
|
return { exists: true, command };
|
|
138
175
|
} catch {
|
package/src/utils/jwt.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT utilities for token management
|
|
3
|
+
* Note: We only decode the payload, we don't verify signatures here
|
|
4
|
+
* (verification happens on the hub side)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TokenPayload {
|
|
8
|
+
sub: string; // Agent ID
|
|
9
|
+
actorType: string; // "agent"
|
|
10
|
+
workspaceScopes: string[];
|
|
11
|
+
iat: number; // Issued at (Unix timestamp)
|
|
12
|
+
exp: number; // Expiration (Unix timestamp)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decode a JWT token without verifying the signature
|
|
17
|
+
*/
|
|
18
|
+
export function decodeToken(token: string): TokenPayload | null {
|
|
19
|
+
try {
|
|
20
|
+
const parts = token.split(".");
|
|
21
|
+
if (parts.length !== 3) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const payload = parts[1];
|
|
26
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
27
|
+
return JSON.parse(decoded) as TokenPayload;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the expiration date from a token
|
|
35
|
+
*/
|
|
36
|
+
export function getTokenExpiry(token: string): Date | null {
|
|
37
|
+
const payload = decodeToken(token);
|
|
38
|
+
if (!payload || !payload.exp) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Date(payload.exp * 1000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a token is expired
|
|
47
|
+
*/
|
|
48
|
+
export function isTokenExpired(token: string): boolean {
|
|
49
|
+
const expiry = getTokenExpiry(token);
|
|
50
|
+
if (!expiry) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return expiry.getTime() <= Date.now();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a token will expire within the given milliseconds
|
|
59
|
+
*/
|
|
60
|
+
export function isTokenExpiringSoon(token: string, withinMs: number): boolean {
|
|
61
|
+
const expiry = getTokenExpiry(token);
|
|
62
|
+
if (!expiry) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return expiry.getTime() <= Date.now() + withinMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the agent ID from a token
|
|
71
|
+
*/
|
|
72
|
+
export function getAgentIdFromToken(token: string): string | null {
|
|
73
|
+
const payload = decodeToken(token);
|
|
74
|
+
return payload?.sub ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get time remaining until token expires in milliseconds
|
|
79
|
+
*/
|
|
80
|
+
export function getTokenTimeRemaining(token: string): number {
|
|
81
|
+
const expiry = getTokenExpiry(token);
|
|
82
|
+
if (!expiry) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Math.max(0, expiry.getTime() - Date.now());
|
|
87
|
+
}
|
package/vitest.config.ts
ADDED