agentid-cli 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/README.md +132 -0
- package/cli/agentid.js +185 -0
- package/data/academy/congressional-ai/FINAL_REPORT.md +97 -0
- package/data/academy/congressional-ai/commdaaf_prompt_v1.md +358 -0
- package/data/academy/congressional-ai/pilot_25_claude.json +252 -0
- package/data/academy/congressional-ai/pilot_batch_25.json +227 -0
- package/data/academy/index.json +57 -0
- package/data/academy/logs/agentacademy_results.json +1059 -0
- package/data/academy/prompts/commdaaf_global_south.md +75 -0
- package/data/academy/prompts/glm-adversarial.md +69 -0
- package/data/academy/prompts/kimi-adversarial.md +75 -0
- package/data/academy/prompts/primary-analysis.md +59 -0
- package/data/agents.json +13 -0
- package/data/challenges.json +1 -0
- package/data/credentials.json +11 -0
- package/lib/client.js +120 -0
- package/lib/index.js +136 -0
- package/package.json +25 -0
- package/public/index.html +768 -0
- package/server/data-routes.js +248 -0
- package/server/index.js +332 -0
- package/server/lite.js +315 -0
- package/server.log +2 -0
- package/test/run.js +120 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentID Data Access Routes
|
|
3
|
+
* Provides enrolled agents access to AgentAcademy materials
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join, extname } from 'path';
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
const ACADEMY_DATA = process.env.ACADEMY_DATA || './data/academy';
|
|
12
|
+
|
|
13
|
+
// Load agents store (shared with main server)
|
|
14
|
+
function loadAgents() {
|
|
15
|
+
const file = process.env.AGENTID_DATA
|
|
16
|
+
? `${process.env.AGENTID_DATA}/agents.json`
|
|
17
|
+
: './data/agents.json';
|
|
18
|
+
if (!existsSync(file)) return {};
|
|
19
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Middleware: require enrolled agent
|
|
23
|
+
function requireEnrolled(req, res, next) {
|
|
24
|
+
const authHeader = req.headers.authorization;
|
|
25
|
+
|
|
26
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
27
|
+
return res.status(401).json({
|
|
28
|
+
error: 'Missing authorization',
|
|
29
|
+
hint: 'Use: Authorization: Bearer <agent_id>'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const agentId = authHeader.slice(7);
|
|
34
|
+
const agents = loadAgents();
|
|
35
|
+
|
|
36
|
+
if (!agents[agentId]) {
|
|
37
|
+
return res.status(403).json({
|
|
38
|
+
error: 'Agent not enrolled',
|
|
39
|
+
hint: 'Enroll first via POST /api/agents/enroll'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
req.agent = agents[agentId];
|
|
44
|
+
req.agentId = agentId;
|
|
45
|
+
next();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* GET /api/academy
|
|
50
|
+
* Get index of all available materials
|
|
51
|
+
*/
|
|
52
|
+
router.get('/', requireEnrolled, (req, res) => {
|
|
53
|
+
const indexPath = join(ACADEMY_DATA, 'index.json');
|
|
54
|
+
|
|
55
|
+
if (!existsSync(indexPath)) {
|
|
56
|
+
return res.status(500).json({ error: 'Academy index not found' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
60
|
+
|
|
61
|
+
res.json({
|
|
62
|
+
agent_id: req.agentId,
|
|
63
|
+
access_level: 'enrolled',
|
|
64
|
+
...index
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET /api/academy/collections
|
|
70
|
+
* List all collections
|
|
71
|
+
*/
|
|
72
|
+
router.get('/collections', requireEnrolled, (req, res) => {
|
|
73
|
+
const indexPath = join(ACADEMY_DATA, 'index.json');
|
|
74
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
75
|
+
|
|
76
|
+
const collections = Object.entries(index.collections).map(([id, col]) => ({
|
|
77
|
+
id,
|
|
78
|
+
name: col.name,
|
|
79
|
+
description: col.description,
|
|
80
|
+
status: col.status,
|
|
81
|
+
resource_count: Object.keys(col.resources).length
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
res.json({ collections });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* GET /api/academy/collections/:id
|
|
89
|
+
* Get a specific collection with its resources
|
|
90
|
+
*/
|
|
91
|
+
router.get('/collections/:id', requireEnrolled, (req, res) => {
|
|
92
|
+
const indexPath = join(ACADEMY_DATA, 'index.json');
|
|
93
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
94
|
+
|
|
95
|
+
const collection = index.collections[req.params.id];
|
|
96
|
+
|
|
97
|
+
if (!collection) {
|
|
98
|
+
return res.status(404).json({ error: 'Collection not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.json({
|
|
102
|
+
id: req.params.id,
|
|
103
|
+
...collection,
|
|
104
|
+
_links: {
|
|
105
|
+
self: `/api/academy/collections/${req.params.id}`,
|
|
106
|
+
resources: Object.keys(collection.resources).map(key => ({
|
|
107
|
+
name: key,
|
|
108
|
+
url: `/api/academy/files/${collection.resources[key]}`
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* GET /api/academy/prompts
|
|
116
|
+
* List all prompts
|
|
117
|
+
*/
|
|
118
|
+
router.get('/prompts', requireEnrolled, (req, res) => {
|
|
119
|
+
const promptsDir = join(ACADEMY_DATA, 'prompts');
|
|
120
|
+
|
|
121
|
+
if (!existsSync(promptsDir)) {
|
|
122
|
+
return res.json({ prompts: [] });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const prompts = readdirSync(promptsDir)
|
|
126
|
+
.filter(f => f.endsWith('.md'))
|
|
127
|
+
.map(f => ({
|
|
128
|
+
name: f.replace('.md', ''),
|
|
129
|
+
file: f,
|
|
130
|
+
url: `/api/academy/files/prompts/${f}`
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
res.json({ prompts });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* GET /api/academy/logs
|
|
138
|
+
* Get run logs
|
|
139
|
+
*/
|
|
140
|
+
router.get('/logs', requireEnrolled, (req, res) => {
|
|
141
|
+
const logsDir = join(ACADEMY_DATA, 'logs');
|
|
142
|
+
|
|
143
|
+
if (!existsSync(logsDir)) {
|
|
144
|
+
return res.json({ logs: [] });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const logs = readdirSync(logsDir)
|
|
148
|
+
.filter(f => f.endsWith('.json'))
|
|
149
|
+
.map(f => ({
|
|
150
|
+
name: f.replace('.json', ''),
|
|
151
|
+
file: f,
|
|
152
|
+
url: `/api/academy/files/logs/${f}`
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
res.json({ logs });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* GET /api/academy/files/*
|
|
160
|
+
* Get a specific file
|
|
161
|
+
*/
|
|
162
|
+
router.get('/files/*', requireEnrolled, (req, res) => {
|
|
163
|
+
const relativePath = req.params[0];
|
|
164
|
+
|
|
165
|
+
// Security: prevent directory traversal
|
|
166
|
+
if (relativePath.includes('..') || relativePath.startsWith('/')) {
|
|
167
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const filePath = join(ACADEMY_DATA, relativePath);
|
|
171
|
+
|
|
172
|
+
if (!existsSync(filePath)) {
|
|
173
|
+
return res.status(404).json({ error: 'File not found' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const stat = statSync(filePath);
|
|
177
|
+
if (stat.isDirectory()) {
|
|
178
|
+
return res.status(400).json({ error: 'Cannot serve directory' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ext = extname(filePath);
|
|
182
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
183
|
+
|
|
184
|
+
if (ext === '.json') {
|
|
185
|
+
res.json(JSON.parse(content));
|
|
186
|
+
} else if (ext === '.md') {
|
|
187
|
+
res.type('text/markdown').send(content);
|
|
188
|
+
} else {
|
|
189
|
+
res.type('text/plain').send(content);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* GET /api/academy/search
|
|
195
|
+
* Search across materials
|
|
196
|
+
*/
|
|
197
|
+
router.get('/search', requireEnrolled, (req, res) => {
|
|
198
|
+
const query = (req.query.q || '').toLowerCase();
|
|
199
|
+
|
|
200
|
+
if (!query || query.length < 2) {
|
|
201
|
+
return res.status(400).json({ error: 'Query too short (min 2 chars)' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const results = [];
|
|
205
|
+
|
|
206
|
+
// Search through files
|
|
207
|
+
const searchDir = (dir, basePath = '') => {
|
|
208
|
+
if (!existsSync(dir)) return;
|
|
209
|
+
|
|
210
|
+
for (const item of readdirSync(dir)) {
|
|
211
|
+
const itemPath = join(dir, item);
|
|
212
|
+
const relativePath = basePath ? `${basePath}/${item}` : item;
|
|
213
|
+
|
|
214
|
+
const stat = statSync(itemPath);
|
|
215
|
+
|
|
216
|
+
if (stat.isDirectory()) {
|
|
217
|
+
searchDir(itemPath, relativePath);
|
|
218
|
+
} else if (item.endsWith('.md') || item.endsWith('.json')) {
|
|
219
|
+
const content = readFileSync(itemPath, 'utf-8');
|
|
220
|
+
|
|
221
|
+
if (content.toLowerCase().includes(query)) {
|
|
222
|
+
// Find matching lines
|
|
223
|
+
const lines = content.split('\n');
|
|
224
|
+
const matches = lines
|
|
225
|
+
.map((line, i) => ({ line: i + 1, text: line }))
|
|
226
|
+
.filter(l => l.text.toLowerCase().includes(query))
|
|
227
|
+
.slice(0, 3);
|
|
228
|
+
|
|
229
|
+
results.push({
|
|
230
|
+
file: relativePath,
|
|
231
|
+
url: `/api/academy/files/${relativePath}`,
|
|
232
|
+
matches
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
searchDir(ACADEMY_DATA);
|
|
240
|
+
|
|
241
|
+
res.json({
|
|
242
|
+
query,
|
|
243
|
+
count: results.length,
|
|
244
|
+
results: results.slice(0, 20)
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export default router;
|
package/server/index.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentID Server - AgentAcademy Identity Registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { deriveAgentId, verifySignature } from '../lib/index.js';
|
|
9
|
+
import { randomBytes } from 'crypto';
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
const PORT = process.env.AGENTID_PORT || 3847;
|
|
13
|
+
const DB_PATH = process.env.AGENTID_DB || './data/agentid.db';
|
|
14
|
+
|
|
15
|
+
// Middleware
|
|
16
|
+
app.use(cors());
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
|
|
19
|
+
// Initialize database
|
|
20
|
+
const db = new Database(DB_PATH);
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
23
|
+
agent_id TEXT PRIMARY KEY,
|
|
24
|
+
pubkey TEXT UNIQUE NOT NULL,
|
|
25
|
+
name TEXT,
|
|
26
|
+
framework TEXT,
|
|
27
|
+
metadata TEXT,
|
|
28
|
+
enrolled_at TEXT NOT NULL
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
agent_id TEXT NOT NULL,
|
|
34
|
+
skill TEXT NOT NULL,
|
|
35
|
+
level TEXT NOT NULL,
|
|
36
|
+
issued_at TEXT NOT NULL,
|
|
37
|
+
issued_by TEXT,
|
|
38
|
+
metadata TEXT,
|
|
39
|
+
FOREIGN KEY (agent_id) REFERENCES agents(agent_id),
|
|
40
|
+
UNIQUE(agent_id, skill)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS challenges (
|
|
44
|
+
challenge TEXT PRIMARY KEY,
|
|
45
|
+
agent_id TEXT,
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
expires_at TEXT NOT NULL
|
|
48
|
+
);
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
// Prepared statements
|
|
52
|
+
const insertAgent = db.prepare(`
|
|
53
|
+
INSERT INTO agents (agent_id, pubkey, name, framework, metadata, enrolled_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
const getAgent = db.prepare(`SELECT * FROM agents WHERE agent_id = ?`);
|
|
58
|
+
const getAgentByPubkey = db.prepare(`SELECT * FROM agents WHERE pubkey = ?`);
|
|
59
|
+
|
|
60
|
+
const insertCredential = db.prepare(`
|
|
61
|
+
INSERT OR REPLACE INTO credentials (agent_id, skill, level, issued_at, issued_by, metadata)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
const getCredentials = db.prepare(`SELECT * FROM credentials WHERE agent_id = ?`);
|
|
66
|
+
|
|
67
|
+
const insertChallenge = db.prepare(`
|
|
68
|
+
INSERT INTO challenges (challenge, agent_id, created_at, expires_at)
|
|
69
|
+
VALUES (?, ?, ?, ?)
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
const getChallenge = db.prepare(`SELECT * FROM challenges WHERE challenge = ?`);
|
|
73
|
+
const deleteChallenge = db.prepare(`DELETE FROM challenges WHERE challenge = ?`);
|
|
74
|
+
|
|
75
|
+
// Clean expired challenges periodically
|
|
76
|
+
setInterval(() => {
|
|
77
|
+
db.prepare(`DELETE FROM challenges WHERE expires_at < ?`).run(new Date().toISOString());
|
|
78
|
+
}, 60000);
|
|
79
|
+
|
|
80
|
+
// ============ API Routes ============
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST /api/agents/enroll
|
|
84
|
+
* Register a new agent
|
|
85
|
+
*/
|
|
86
|
+
app.post('/api/agents/enroll', (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const { pubkey, agentId, metadata, timestamp, signature } = req.body;
|
|
89
|
+
|
|
90
|
+
if (!pubkey || !signature) {
|
|
91
|
+
return res.status(400).json({ error: 'Missing pubkey or signature' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Verify agent ID derivation
|
|
95
|
+
const expectedAgentId = deriveAgentId(pubkey);
|
|
96
|
+
if (agentId !== expectedAgentId) {
|
|
97
|
+
return res.status(400).json({ error: 'Invalid agent ID derivation' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Verify signature
|
|
101
|
+
const payload = JSON.stringify({ pubkey, agentId, metadata, timestamp });
|
|
102
|
+
if (!verifySignature(payload, signature, pubkey)) {
|
|
103
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if already enrolled
|
|
107
|
+
const existing = getAgentByPubkey.get(pubkey);
|
|
108
|
+
if (existing) {
|
|
109
|
+
return res.status(200).json({
|
|
110
|
+
agent_id: existing.agent_id,
|
|
111
|
+
enrolled_at: existing.enrolled_at,
|
|
112
|
+
already_enrolled: true
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Enroll
|
|
117
|
+
const enrolled_at = new Date().toISOString();
|
|
118
|
+
insertAgent.run(
|
|
119
|
+
agentId,
|
|
120
|
+
pubkey,
|
|
121
|
+
metadata?.name || null,
|
|
122
|
+
metadata?.framework || null,
|
|
123
|
+
JSON.stringify(metadata || {}),
|
|
124
|
+
enrolled_at
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
res.status(201).json({
|
|
128
|
+
agent_id: agentId,
|
|
129
|
+
enrolled_at,
|
|
130
|
+
already_enrolled: false
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Enrollment error:', error);
|
|
135
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* GET /api/agents/:agentId
|
|
141
|
+
* Get agent profile
|
|
142
|
+
*/
|
|
143
|
+
app.get('/api/agents/:agentId', (req, res) => {
|
|
144
|
+
const agent = getAgent.get(req.params.agentId);
|
|
145
|
+
|
|
146
|
+
if (!agent) {
|
|
147
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
res.json({
|
|
151
|
+
agent_id: agent.agent_id,
|
|
152
|
+
name: agent.name,
|
|
153
|
+
framework: agent.framework,
|
|
154
|
+
enrolled_at: agent.enrolled_at
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* GET /api/agents/:agentId/credentials
|
|
160
|
+
* Get agent credentials
|
|
161
|
+
*/
|
|
162
|
+
app.get('/api/agents/:agentId/credentials', (req, res) => {
|
|
163
|
+
const agent = getAgent.get(req.params.agentId);
|
|
164
|
+
|
|
165
|
+
if (!agent) {
|
|
166
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const credentials = getCredentials.all(req.params.agentId);
|
|
170
|
+
|
|
171
|
+
res.json({
|
|
172
|
+
agent_id: agent.agent_id,
|
|
173
|
+
credentials: credentials.map(c => ({
|
|
174
|
+
skill: c.skill,
|
|
175
|
+
level: c.level,
|
|
176
|
+
issued_at: c.issued_at,
|
|
177
|
+
issued_by: c.issued_by
|
|
178
|
+
}))
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* POST /api/agents/challenge
|
|
184
|
+
* Generate a challenge for verification
|
|
185
|
+
*/
|
|
186
|
+
app.post('/api/agents/challenge', (req, res) => {
|
|
187
|
+
const { agent_id } = req.body;
|
|
188
|
+
|
|
189
|
+
if (!agent_id) {
|
|
190
|
+
return res.status(400).json({ error: 'Missing agent_id' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const agent = getAgent.get(agent_id);
|
|
194
|
+
if (!agent) {
|
|
195
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const challenge = randomBytes(32).toString('base64url');
|
|
199
|
+
const created_at = new Date().toISOString();
|
|
200
|
+
const expires_at = new Date(Date.now() + 5 * 60 * 1000).toISOString(); // 5 min
|
|
201
|
+
|
|
202
|
+
insertChallenge.run(challenge, agent_id, created_at, expires_at);
|
|
203
|
+
|
|
204
|
+
res.json({ challenge, expires_at });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* POST /api/agents/verify
|
|
209
|
+
* Verify agent owns their ID via signed challenge
|
|
210
|
+
*/
|
|
211
|
+
app.post('/api/agents/verify', (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const { agentId, challenge, signature } = req.body;
|
|
214
|
+
|
|
215
|
+
if (!agentId || !challenge || !signature) {
|
|
216
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Get challenge
|
|
220
|
+
const challengeRecord = getChallenge.get(challenge);
|
|
221
|
+
if (!challengeRecord) {
|
|
222
|
+
return res.status(400).json({ error: 'Invalid or expired challenge' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check expiry
|
|
226
|
+
if (new Date(challengeRecord.expires_at) < new Date()) {
|
|
227
|
+
deleteChallenge.run(challenge);
|
|
228
|
+
return res.status(400).json({ error: 'Challenge expired' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check agent ID matches
|
|
232
|
+
if (challengeRecord.agent_id !== agentId) {
|
|
233
|
+
return res.status(400).json({ error: 'Challenge not for this agent' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Get agent
|
|
237
|
+
const agent = getAgent.get(agentId);
|
|
238
|
+
if (!agent) {
|
|
239
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Verify signature
|
|
243
|
+
const message = `${agentId}:${challenge}`;
|
|
244
|
+
const valid = verifySignature(message, signature, agent.pubkey);
|
|
245
|
+
|
|
246
|
+
// Clean up challenge
|
|
247
|
+
deleteChallenge.run(challenge);
|
|
248
|
+
|
|
249
|
+
if (!valid) {
|
|
250
|
+
return res.status(401).json({ valid: false, error: 'Invalid signature' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.json({
|
|
254
|
+
valid: true,
|
|
255
|
+
agent_id: agentId,
|
|
256
|
+
verified_at: new Date().toISOString()
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Verification error:', error);
|
|
261
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* POST /api/credentials/issue
|
|
267
|
+
* Issue a credential to an agent (admin only for now)
|
|
268
|
+
*/
|
|
269
|
+
app.post('/api/credentials/issue', (req, res) => {
|
|
270
|
+
try {
|
|
271
|
+
const { agent_id, skill, level, issued_by, metadata } = req.body;
|
|
272
|
+
|
|
273
|
+
// TODO: Add admin auth
|
|
274
|
+
|
|
275
|
+
if (!agent_id || !skill || !level) {
|
|
276
|
+
return res.status(400).json({ error: 'Missing required fields' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const agent = getAgent.get(agent_id);
|
|
280
|
+
if (!agent) {
|
|
281
|
+
return res.status(404).json({ error: 'Agent not found' });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const issued_at = new Date().toISOString();
|
|
285
|
+
insertCredential.run(
|
|
286
|
+
agent_id,
|
|
287
|
+
skill,
|
|
288
|
+
level,
|
|
289
|
+
issued_at,
|
|
290
|
+
issued_by || 'agentacademy',
|
|
291
|
+
JSON.stringify(metadata || {})
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
res.status(201).json({
|
|
295
|
+
agent_id,
|
|
296
|
+
skill,
|
|
297
|
+
level,
|
|
298
|
+
issued_at
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Credential issue error:', error);
|
|
303
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* GET /api/stats
|
|
309
|
+
* Public stats
|
|
310
|
+
*/
|
|
311
|
+
app.get('/api/stats', (req, res) => {
|
|
312
|
+
const agentCount = db.prepare('SELECT COUNT(*) as count FROM agents').get().count;
|
|
313
|
+
const credentialCount = db.prepare('SELECT COUNT(*) as count FROM credentials').get().count;
|
|
314
|
+
|
|
315
|
+
res.json({
|
|
316
|
+
agents_enrolled: agentCount,
|
|
317
|
+
credentials_issued: credentialCount
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Health check
|
|
322
|
+
app.get('/health', (req, res) => {
|
|
323
|
+
res.json({ status: 'ok', service: 'agentid' });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Start server
|
|
327
|
+
app.listen(PORT, () => {
|
|
328
|
+
console.log(`AgentID server running on port ${PORT}`);
|
|
329
|
+
console.log(`Database: ${DB_PATH}`);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
export default app;
|