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.
@@ -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;
@@ -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;