aicq-codex 0.1.0
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/.codex-plugin/plugin.json +4 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/lib/chat.js +1287 -0
- package/lib/crypto.js +129 -0
- package/lib/database.js +470 -0
- package/lib/handshake.js +200 -0
- package/lib/identity.js +165 -0
- package/lib/package.json +3 -0
- package/lib/server-client.js +405 -0
- package/mcp/server.js +455 -0
- package/mcp.json +16 -0
- package/package.json +56 -0
- package/plugin.json +21 -0
package/lib/handshake.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Handshake Manager — P2P handshake via temp numbers
|
|
3
|
+
*/
|
|
4
|
+
const { computeFingerprint } = require('./crypto');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
class HandshakeManager {
|
|
8
|
+
constructor(identityManager, serverClient, db) {
|
|
9
|
+
this.identity = identityManager;
|
|
10
|
+
this.server = serverClient;
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a temp number and return it for sharing
|
|
16
|
+
*/
|
|
17
|
+
async generateFriendCode(agentId) {
|
|
18
|
+
await this.server.ensureAuth(agentId);
|
|
19
|
+
const result = await this.server.generateTempNumber();
|
|
20
|
+
if (result.number) {
|
|
21
|
+
this.db.saveTempNumber({
|
|
22
|
+
agent_id: agentId,
|
|
23
|
+
number: result.number,
|
|
24
|
+
expires_at: result.expiresAt || result.expires_at,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add a friend using their temp number / friend code
|
|
32
|
+
*/
|
|
33
|
+
async addFriendByCode(agentId, tempNumber) {
|
|
34
|
+
await this.server.ensureAuth(agentId);
|
|
35
|
+
// Resolve the temp number
|
|
36
|
+
const resolved = await this.server.resolveTempNumber(tempNumber);
|
|
37
|
+
if (!resolved) throw new Error('Invalid or expired friend code');
|
|
38
|
+
|
|
39
|
+
// Initiate handshake
|
|
40
|
+
const result = await this.server.initiateHandshake(tempNumber);
|
|
41
|
+
|
|
42
|
+
// If we got the peer's public key, derive session and add friend
|
|
43
|
+
if (resolved.node_id || resolved.public_key) {
|
|
44
|
+
const peerId = resolved.node_id || resolved.id;
|
|
45
|
+
const peerPublicKey = resolved.public_key;
|
|
46
|
+
|
|
47
|
+
// Derive session key
|
|
48
|
+
let sessionKey = null;
|
|
49
|
+
try {
|
|
50
|
+
const identity = this.identity.loadAgent(agentId);
|
|
51
|
+
if (identity && peerPublicKey) {
|
|
52
|
+
const { deriveSessionKey } = require('./crypto');
|
|
53
|
+
sessionKey = deriveSessionKey(identity.exchange_secret_key, peerPublicKey);
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('[Handshake] Session key derivation failed:', e.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add friend locally
|
|
60
|
+
if (peerId) {
|
|
61
|
+
this.db.addFriend({
|
|
62
|
+
agent_id: agentId,
|
|
63
|
+
id: peerId,
|
|
64
|
+
public_key: peerPublicKey || '',
|
|
65
|
+
fingerprint: peerPublicKey ? computeFingerprint(peerPublicKey) : '',
|
|
66
|
+
friend_type: 'ai',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Save session if we have a key
|
|
70
|
+
if (sessionKey) {
|
|
71
|
+
this.db.saveSession({
|
|
72
|
+
agent_id: agentId,
|
|
73
|
+
peer_id: peerId,
|
|
74
|
+
session_key: sessionKey,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List pending handshake requests.
|
|
85
|
+
*
|
|
86
|
+
* Tries the server first (aicq.me-style friend_requests table).
|
|
87
|
+
* Falls back to local pending_requests table for legacy mode.
|
|
88
|
+
*/
|
|
89
|
+
async getPendingRequests(agentId) {
|
|
90
|
+
try {
|
|
91
|
+
await this.server.ensureAuth(agentId);
|
|
92
|
+
const result = await this.server.listFriendRequests();
|
|
93
|
+
const received = Array.isArray(result?.received) ? result.received : [];
|
|
94
|
+
// Normalise to the same shape as local pending_requests rows.
|
|
95
|
+
return received
|
|
96
|
+
.filter((r) => r.status === 'pending')
|
|
97
|
+
.map((r) => ({
|
|
98
|
+
agent_id: agentId,
|
|
99
|
+
session_id: r.id, // server request id
|
|
100
|
+
requester_id: r.from_id,
|
|
101
|
+
requester_public_key: r.from_public_key || '',
|
|
102
|
+
timestamp: r.created_at,
|
|
103
|
+
message: r.message || '',
|
|
104
|
+
_source: 'server',
|
|
105
|
+
}));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.warn('[Handshake] getPendingRequests: server query failed, falling back to local DB:', e.message);
|
|
108
|
+
return this.db.getPendingRequests(agentId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Accept a pending friend request.
|
|
114
|
+
*
|
|
115
|
+
* Acceptance goes through the server's
|
|
116
|
+
* POST /friends/requests/:id/accept endpoint so that the server
|
|
117
|
+
* creates the bidirectional friendship and notifies the requester.
|
|
118
|
+
* After acceptance we also add the friend locally.
|
|
119
|
+
*/
|
|
120
|
+
async acceptRequest(agentId, requestId) {
|
|
121
|
+
if (!requestId) throw new Error('request_id is required');
|
|
122
|
+
|
|
123
|
+
// Try to fetch the requester's public key from server response
|
|
124
|
+
let requesterPublicKey = '';
|
|
125
|
+
let requesterId = '';
|
|
126
|
+
try {
|
|
127
|
+
const pending = await this.getPendingRequests(agentId);
|
|
128
|
+
const found = pending.find((r) => r.session_id === requestId);
|
|
129
|
+
if (found) {
|
|
130
|
+
requesterPublicKey = found.requester_public_key || '';
|
|
131
|
+
requesterId = found.requester_id || '';
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.warn('[Handshake] acceptRequest: failed to look up pending request:', e.message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Call server accept API
|
|
138
|
+
await this.server.ensureAuth(agentId);
|
|
139
|
+
await this.server.acceptFriendRequest(requestId);
|
|
140
|
+
|
|
141
|
+
// Derive session key (best-effort — only if we have the peer's public key)
|
|
142
|
+
let sessionKey = null;
|
|
143
|
+
try {
|
|
144
|
+
const identity = this.identity.loadAgent(agentId);
|
|
145
|
+
if (identity && requesterPublicKey) {
|
|
146
|
+
const { deriveSessionKey } = require('./crypto');
|
|
147
|
+
sessionKey = deriveSessionKey(identity.exchange_secret_key, requesterPublicKey);
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.warn('[Handshake] Session key derivation failed:', e.message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add friend locally (server already added bidirectionally)
|
|
154
|
+
if (requesterId) {
|
|
155
|
+
const { computeFingerprint } = require('./crypto');
|
|
156
|
+
// Don't duplicate
|
|
157
|
+
const exists = this.db.listFriends(agentId).find((f) => f.id === requesterId);
|
|
158
|
+
if (!exists) {
|
|
159
|
+
this.db.addFriend({
|
|
160
|
+
agent_id: agentId,
|
|
161
|
+
id: requesterId,
|
|
162
|
+
public_key: requesterPublicKey,
|
|
163
|
+
fingerprint: requesterPublicKey ? computeFingerprint(requesterPublicKey) : '',
|
|
164
|
+
friend_type: 'human',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (sessionKey) {
|
|
168
|
+
this.db.saveSession({
|
|
169
|
+
agent_id: agentId,
|
|
170
|
+
peer_id: requesterId,
|
|
171
|
+
session_key: sessionKey,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Remove from local pending_requests (if it was stored there)
|
|
177
|
+
this.db.removePendingRequest(agentId, requestId);
|
|
178
|
+
|
|
179
|
+
return { success: true, friend_id: requesterId };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reject a pending friend request.
|
|
184
|
+
*
|
|
185
|
+
* Calls server's POST /friends/requests/:id/reject endpoint.
|
|
186
|
+
*/
|
|
187
|
+
async rejectRequest(agentId, requestId) {
|
|
188
|
+
if (!requestId) throw new Error('request_id is required');
|
|
189
|
+
try {
|
|
190
|
+
await this.server.ensureAuth(agentId);
|
|
191
|
+
await this.server.rejectFriendRequest(requestId);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.warn('[Handshake] rejectRequest: server call failed:', e.message);
|
|
194
|
+
}
|
|
195
|
+
this.db.removePendingRequest(agentId, requestId);
|
|
196
|
+
return { success: true };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = HandshakeManager;
|
package/lib/identity.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Identity Manager — Ed25519 + X25519 key management
|
|
3
|
+
*/
|
|
4
|
+
const crypto = require('./crypto');
|
|
5
|
+
|
|
6
|
+
class IdentityManager {
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this._cache = {}; // agent_id -> identity
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new agent identity
|
|
14
|
+
*/
|
|
15
|
+
createAgent(agentId, nickname = '') {
|
|
16
|
+
const signing = crypto.generateSigningKeypair();
|
|
17
|
+
const exchange = crypto.generateExchangeKeypair();
|
|
18
|
+
const fingerprint = crypto.computeFingerprint(signing.publicKey);
|
|
19
|
+
|
|
20
|
+
this.db.saveIdentity({
|
|
21
|
+
agent_id: agentId,
|
|
22
|
+
nickname: nickname || agentId,
|
|
23
|
+
signing_public_key: signing.publicKey,
|
|
24
|
+
signing_secret_key: signing.secretKey,
|
|
25
|
+
exchange_public_key: exchange.publicKey,
|
|
26
|
+
exchange_secret_key: exchange.secretKey,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this._cache[agentId] = {
|
|
30
|
+
agent_id: agentId,
|
|
31
|
+
nickname: nickname || agentId,
|
|
32
|
+
signing_public_key: signing.publicKey,
|
|
33
|
+
signing_secret_key: signing.secretKey,
|
|
34
|
+
exchange_public_key: exchange.publicKey,
|
|
35
|
+
exchange_secret_key: exchange.secretKey,
|
|
36
|
+
fingerprint,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return this._cache[agentId];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load an existing identity into cache
|
|
44
|
+
*/
|
|
45
|
+
loadAgent(agentId) {
|
|
46
|
+
if (this._cache[agentId]) return this._cache[agentId];
|
|
47
|
+
const row = this.db.loadIdentity(agentId);
|
|
48
|
+
if (!row) return null;
|
|
49
|
+
row.fingerprint = crypto.computeFingerprint(row.signing_public_key);
|
|
50
|
+
this._cache[agentId] = row;
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get current identity (load first one if agentId not specified)
|
|
56
|
+
*/
|
|
57
|
+
getCurrent(agentId) {
|
|
58
|
+
if (agentId) return this.loadAgent(agentId);
|
|
59
|
+
const identities = this.db.listIdentities();
|
|
60
|
+
if (identities.length === 0) return null;
|
|
61
|
+
return this.loadAgent(identities[0].agent_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List all agent identities
|
|
66
|
+
*/
|
|
67
|
+
listAgents() {
|
|
68
|
+
return this.db.listIdentities();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Delete an agent identity
|
|
73
|
+
*/
|
|
74
|
+
deleteAgent(agentId) {
|
|
75
|
+
delete this._cache[agentId];
|
|
76
|
+
this.db.deleteIdentity(agentId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update agent nickname
|
|
81
|
+
*/
|
|
82
|
+
updateNickname(agentId, nickname) {
|
|
83
|
+
this.db.updateNickname(agentId, nickname);
|
|
84
|
+
if (this._cache[agentId]) {
|
|
85
|
+
this._cache[agentId].nickname = nickname;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sign a message with the agent's signing key
|
|
91
|
+
*/
|
|
92
|
+
sign(agentId, message) {
|
|
93
|
+
const identity = this.loadAgent(agentId);
|
|
94
|
+
if (!identity) throw new Error('Identity not found');
|
|
95
|
+
return crypto.signMessage(message, identity.signing_secret_key);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive a session key with a peer
|
|
100
|
+
*/
|
|
101
|
+
deriveSessionKey(agentId, peerExchangePublicKeyB64) {
|
|
102
|
+
const identity = this.loadAgent(agentId);
|
|
103
|
+
if (!identity) throw new Error('Identity not found');
|
|
104
|
+
return crypto.deriveSessionKey(identity.exchange_secret_key, peerExchangePublicKeyB64);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Rotate keys for an agent
|
|
109
|
+
*/
|
|
110
|
+
rotateKeys(agentId) {
|
|
111
|
+
const oldIdentity = this.loadAgent(agentId);
|
|
112
|
+
if (!oldIdentity) throw new Error('Identity not found');
|
|
113
|
+
|
|
114
|
+
const signing = crypto.generateSigningKeypair();
|
|
115
|
+
const exchange = crypto.generateExchangeKeypair();
|
|
116
|
+
|
|
117
|
+
this.db.saveIdentity({
|
|
118
|
+
agent_id: agentId,
|
|
119
|
+
nickname: oldIdentity.nickname,
|
|
120
|
+
signing_public_key: signing.publicKey,
|
|
121
|
+
signing_secret_key: signing.secretKey,
|
|
122
|
+
exchange_public_key: exchange.publicKey,
|
|
123
|
+
exchange_secret_key: exchange.secretKey,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this._cache[agentId] = {
|
|
127
|
+
...oldIdentity,
|
|
128
|
+
signing_public_key: signing.publicKey,
|
|
129
|
+
signing_secret_key: signing.secretKey,
|
|
130
|
+
exchange_public_key: exchange.publicKey,
|
|
131
|
+
exchange_secret_key: exchange.secretKey,
|
|
132
|
+
fingerprint: crypto.computeFingerprint(signing.publicKey),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return this._cache[agentId];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update agent avatar
|
|
140
|
+
*/
|
|
141
|
+
updateAvatar(agentId, avatarUrl) {
|
|
142
|
+
this.db.updateAvatar(agentId, avatarUrl);
|
|
143
|
+
if (this._cache[agentId]) {
|
|
144
|
+
this._cache[agentId].avatar = avatarUrl;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get identity info (public keys only, no secrets)
|
|
150
|
+
*/
|
|
151
|
+
getInfo(agentId) {
|
|
152
|
+
const identity = this.loadAgent(agentId);
|
|
153
|
+
if (!identity) return null;
|
|
154
|
+
return {
|
|
155
|
+
agent_id: identity.agent_id,
|
|
156
|
+
nickname: identity.nickname,
|
|
157
|
+
avatar: identity.avatar || null,
|
|
158
|
+
signing_public_key: identity.signing_public_key,
|
|
159
|
+
exchange_public_key: identity.exchange_public_key,
|
|
160
|
+
fingerprint: identity.fingerprint,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = IdentityManager;
|
package/lib/package.json
ADDED