habi-agent 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/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud.d.ts +50 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +147 -0
- package/dist/cloud.js.map +1 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +172 -0
- package/dist/context.js.map +1 -0
- package/dist/hardware.d.ts +38 -0
- package/dist/hardware.d.ts.map +1 -0
- package/dist/hardware.js +130 -0
- package/dist/hardware.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +276 -0
- package/dist/server.js.map +1 -0
- package/package.json +24 -0
- package/src/cli.ts +264 -0
- package/src/cloud.ts +165 -0
- package/src/context.ts +186 -0
- package/src/hardware.ts +114 -0
- package/src/index.ts +5 -0
- package/src/server.ts +255 -0
- package/tsconfig.json +10 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HABI Local Identity Server
|
|
3
|
+
* Runs on localhost:7432 exclusively — never binds to 0.0.0.0
|
|
4
|
+
*
|
|
5
|
+
* Pure Node.js http — zero external dependencies.
|
|
6
|
+
* Security: localhost-only binding is the enforced hardware boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as http from 'http';
|
|
10
|
+
import * as crypto from 'crypto';
|
|
11
|
+
import { readHardwareIdentity, generateSessionFingerprint, sha256, HardwareIdentity } from './hardware';
|
|
12
|
+
import { findContextFile, loadContext, assertOperation, assertMachineAuthorized, LoadedContext } from './context';
|
|
13
|
+
|
|
14
|
+
const PORT = 7432;
|
|
15
|
+
const BIND_HOST = '127.0.0.1'; // NEVER 0.0.0.0
|
|
16
|
+
|
|
17
|
+
interface SessionState {
|
|
18
|
+
fingerprint: string;
|
|
19
|
+
hardwareId: string;
|
|
20
|
+
contextHash: string;
|
|
21
|
+
boundAt: number;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function json(res: http.ServerResponse, status: number, body: unknown): void {
|
|
26
|
+
const payload = JSON.stringify(body);
|
|
27
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
28
|
+
res.end(payload);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
let raw = '';
|
|
34
|
+
req.on('data', (chunk: Buffer) => raw += chunk.toString());
|
|
35
|
+
req.on('end', () => {
|
|
36
|
+
try { resolve(raw ? JSON.parse(raw) : {}); }
|
|
37
|
+
catch { reject(new Error('Invalid JSON body')); }
|
|
38
|
+
});
|
|
39
|
+
req.on('error', reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class HabiLocalServer {
|
|
44
|
+
private hardware: HardwareIdentity | null = null;
|
|
45
|
+
private context: LoadedContext | null = null;
|
|
46
|
+
private session: SessionState | null = null;
|
|
47
|
+
private server: http.Server | null = null;
|
|
48
|
+
|
|
49
|
+
// ── Security: reject anything not from localhost ────────────────────────
|
|
50
|
+
private isLocalhost(req: http.IncomingMessage): boolean {
|
|
51
|
+
const ip = req.socket.remoteAddress;
|
|
52
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Route handlers ──────────────────────────────────────────────────────
|
|
56
|
+
private handleHealth(res: http.ServerResponse): void {
|
|
57
|
+
json(res, 200, { ok: true, agent: 'habi-agent', version: '0.1.0', port: PORT });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private handleIdentity(res: http.ServerResponse): void {
|
|
61
|
+
try {
|
|
62
|
+
const hw = this.getHardware();
|
|
63
|
+
const sess = this.getOrCreateSession();
|
|
64
|
+
json(res, 200, {
|
|
65
|
+
ok: true,
|
|
66
|
+
hardware: {
|
|
67
|
+
fingerprint: hw.fingerprint,
|
|
68
|
+
type: hw.type,
|
|
69
|
+
iface: hw.iface,
|
|
70
|
+
platform: hw.platform,
|
|
71
|
+
hostname: hw.hostname,
|
|
72
|
+
},
|
|
73
|
+
session: {
|
|
74
|
+
fingerprint: sess.fingerprint,
|
|
75
|
+
boundAt: sess.boundAt,
|
|
76
|
+
expiresAt: sess.expiresAt,
|
|
77
|
+
active: Date.now() < sess.expiresAt,
|
|
78
|
+
},
|
|
79
|
+
agent: { version: '0.1.0', port: PORT },
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
json(res, 500, { ok: false, error: String(err) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private handleVerify(res: http.ServerResponse): void {
|
|
87
|
+
try {
|
|
88
|
+
const hw = this.getHardware();
|
|
89
|
+
const sess = this.getOrCreateSession();
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (now >= sess.expiresAt) {
|
|
92
|
+
this.session = null;
|
|
93
|
+
json(res, 200, { ok: false, verified: false, reason: 'Session expired — rebind required' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
json(res, 200, {
|
|
97
|
+
ok: true,
|
|
98
|
+
verified: true,
|
|
99
|
+
fingerprint: sess.fingerprint,
|
|
100
|
+
hardwareId: hw.fingerprint,
|
|
101
|
+
hardwareType: hw.type,
|
|
102
|
+
expiresAt: sess.expiresAt,
|
|
103
|
+
expiresIn: Math.floor((sess.expiresAt - now) / 1000),
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
json(res, 500, { ok: false, verified: false, error: String(err) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async handleAssertContext(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
const body = await readBody(req);
|
|
113
|
+
const operation = body.operation as string | undefined;
|
|
114
|
+
if (!operation) { json(res, 400, { ok: false, error: 'operation field required' }); return; }
|
|
115
|
+
|
|
116
|
+
const ctx = this.getContext();
|
|
117
|
+
if (!ctx) {
|
|
118
|
+
json(res, 200, { ok: true, allowed: true, warning: 'No Project Context File — permissive mode. Run: habi-agent init' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hw = this.getHardware();
|
|
123
|
+
const machineCheck = assertMachineAuthorized(hw.fingerprint, ctx.context);
|
|
124
|
+
if (!machineCheck.allowed) {
|
|
125
|
+
json(res, 200, { ok: true, allowed: false, reason: machineCheck.reason, hardwareId: hw.fingerprint });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const opCheck = assertOperation(operation, ctx.context);
|
|
130
|
+
json(res, 200, {
|
|
131
|
+
ok: true,
|
|
132
|
+
allowed: opCheck.allowed,
|
|
133
|
+
reason: opCheck.reason,
|
|
134
|
+
hardwareId: hw.fingerprint,
|
|
135
|
+
contextHash: ctx.contextHash,
|
|
136
|
+
project: ctx.context.project,
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
json(res, 500, { ok: false, error: String(err) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async handleAudit(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
144
|
+
try {
|
|
145
|
+
const body = await readBody(req);
|
|
146
|
+
const hw = this.getHardware();
|
|
147
|
+
const entry = {
|
|
148
|
+
ts: Date.now(),
|
|
149
|
+
hardwareId: hw.fingerprint,
|
|
150
|
+
sessionFingerprint: this.session?.fingerprint ?? 'UNBOUND',
|
|
151
|
+
...body,
|
|
152
|
+
};
|
|
153
|
+
const entryHash = sha256(JSON.stringify(entry));
|
|
154
|
+
console.log(`[HABI AUDIT] ${new Date().toISOString()} ${body.eventType ?? 'EVENT'} ${entryHash.slice(0, 12)}`);
|
|
155
|
+
json(res, 200, { ok: true, entryHash });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
json(res, 500, { ok: false, error: String(err) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Internal helpers ────────────────────────────────────────────────────
|
|
162
|
+
private getHardware(): HardwareIdentity {
|
|
163
|
+
if (!this.hardware) {
|
|
164
|
+
this.hardware = readHardwareIdentity();
|
|
165
|
+
console.log(`[HABI] Hardware: ${this.hardware.fingerprint} (${this.hardware.type}) on ${this.hardware.iface}`);
|
|
166
|
+
}
|
|
167
|
+
return this.hardware;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private getContext(): LoadedContext | null {
|
|
171
|
+
if (!this.context) {
|
|
172
|
+
const p = findContextFile();
|
|
173
|
+
if (p) {
|
|
174
|
+
this.context = loadContext(p);
|
|
175
|
+
console.log(`[HABI] Context: ${this.context.context.project} (${this.context.contextHash.slice(0, 12)}...)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return this.context;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private getOrCreateSession(): SessionState {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
if (this.session && now < this.session.expiresAt) return this.session;
|
|
184
|
+
|
|
185
|
+
const hw = this.getHardware();
|
|
186
|
+
const ctx = this.getContext();
|
|
187
|
+
const contextHash = ctx?.contextHash ?? sha256('no-context');
|
|
188
|
+
const fingerprint = generateSessionFingerprint(hw.fingerprint, contextHash, now);
|
|
189
|
+
|
|
190
|
+
this.session = {
|
|
191
|
+
fingerprint,
|
|
192
|
+
hardwareId: hw.fingerprint,
|
|
193
|
+
contextHash,
|
|
194
|
+
boundAt: now,
|
|
195
|
+
expiresAt: now + 15 * 60 * 1000,
|
|
196
|
+
};
|
|
197
|
+
console.log(`[HABI] Session: ${fingerprint.slice(0, 16)}... expires ${new Date(this.session.expiresAt).toISOString()}`);
|
|
198
|
+
return this.session;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Start / stop ────────────────────────────────────────────────────────
|
|
202
|
+
start(): Promise<void> {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
try {
|
|
205
|
+
this.getHardware();
|
|
206
|
+
this.getContext();
|
|
207
|
+
this.getOrCreateSession();
|
|
208
|
+
|
|
209
|
+
this.server = http.createServer(async (req, res) => {
|
|
210
|
+
// Localhost enforcement
|
|
211
|
+
if (!this.isLocalhost(req)) {
|
|
212
|
+
json(res, 403, { error: 'HABI agent is localhost-only' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const url = req.url ?? '/';
|
|
217
|
+
const method = req.method ?? 'GET';
|
|
218
|
+
|
|
219
|
+
if (url === '/health' && method === 'GET') return this.handleHealth(res);
|
|
220
|
+
if (url === '/identity' && method === 'GET') return this.handleIdentity(res);
|
|
221
|
+
if (url === '/verify' && method === 'POST') return this.handleVerify(res);
|
|
222
|
+
if (url === '/assert-context' && method === 'POST') return this.handleAssertContext(req, res);
|
|
223
|
+
if (url === '/audit' && method === 'POST') return this.handleAudit(req, res);
|
|
224
|
+
|
|
225
|
+
json(res, 404, { error: `Unknown route: ${method} ${url}` });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.server.listen(PORT, BIND_HOST, () => {
|
|
229
|
+
const hw = this.hardware!;
|
|
230
|
+
console.log(`\n╔══════════════════════════════════════════════════╗`);
|
|
231
|
+
console.log(`║ HABI Agent running on ${BIND_HOST}:${PORT} ║`);
|
|
232
|
+
console.log(`║ Hardware: ${hw.fingerprint.padEnd(34)} ║`);
|
|
233
|
+
console.log(`║ Type: ${hw.type.padEnd(34)} ║`);
|
|
234
|
+
console.log(`╚══════════════════════════════════════════════════╝\n`);
|
|
235
|
+
resolve();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
this.server.on('error', (err: NodeJS.ErrnoException) => {
|
|
239
|
+
if (err.code === 'EADDRINUSE') {
|
|
240
|
+
reject(new Error(`HABI: Port ${PORT} already in use. Another instance may be running.`));
|
|
241
|
+
} else {
|
|
242
|
+
reject(err);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
reject(err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
stop(): void {
|
|
252
|
+
this.server?.close();
|
|
253
|
+
console.log('[HABI] Agent stopped.');
|
|
254
|
+
}
|
|
255
|
+
}
|