@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/health.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import http, { type IncomingMessage, type ServerResponse } from 'http';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
4
|
+
import { runDiscordToolAction } from './channels/discord/runtime.js';
|
|
5
|
+
import type { DiscordToolActionRequest } from './channels/discord/tool-actions.js';
|
|
6
|
+
import {
|
|
7
|
+
GATEWAY_API_TOKEN,
|
|
8
|
+
HEALTH_HOST,
|
|
9
|
+
HEALTH_PORT,
|
|
10
|
+
WEB_API_TOKEN,
|
|
11
|
+
} from './config.js';
|
|
6
12
|
import {
|
|
13
|
+
type GatewayChatRequest,
|
|
14
|
+
type GatewayCommandRequest,
|
|
7
15
|
getGatewayHistory,
|
|
8
16
|
getGatewayStatus,
|
|
9
17
|
handleGatewayCommand,
|
|
10
18
|
handleGatewayMessage,
|
|
11
|
-
type GatewayCommandRequest,
|
|
12
|
-
type GatewayChatRequest,
|
|
13
19
|
} from './gateway-service.js';
|
|
14
|
-
import {
|
|
15
|
-
import { type DiscordToolActionRequest } from './channels/discord/tool-actions.js';
|
|
16
|
-
import { runDiscordToolAction } from './channels/discord/runtime.js';
|
|
17
|
-
import { type ToolProgressEvent } from './types.js';
|
|
20
|
+
import type { GatewayChatRequestBody } from './gateway-types.js';
|
|
18
21
|
import { logger } from './logger.js';
|
|
22
|
+
import type { ToolProgressEvent } from './types.js';
|
|
19
23
|
|
|
20
24
|
const SITE_DIR = path.resolve(process.cwd(), 'docs');
|
|
21
25
|
const MAX_REQUEST_BYTES = 1_000_000; // 1MB
|
|
@@ -40,7 +44,8 @@ function isLoopbackAddress(address: string | undefined): boolean {
|
|
|
40
44
|
|
|
41
45
|
function hasApiAuth(req: IncomingMessage): boolean {
|
|
42
46
|
const authHeader = req.headers.authorization || '';
|
|
43
|
-
const gatewayTokenMatch =
|
|
47
|
+
const gatewayTokenMatch =
|
|
48
|
+
Boolean(GATEWAY_API_TOKEN) && authHeader === `Bearer ${GATEWAY_API_TOKEN}`;
|
|
44
49
|
|
|
45
50
|
if (!WEB_API_TOKEN) {
|
|
46
51
|
return gatewayTokenMatch || isLoopbackAddress(req.socket.remoteAddress);
|
|
@@ -49,8 +54,14 @@ function hasApiAuth(req: IncomingMessage): boolean {
|
|
|
49
54
|
return gatewayTokenMatch;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function sendJson(
|
|
53
|
-
res
|
|
57
|
+
function sendJson(
|
|
58
|
+
res: ServerResponse,
|
|
59
|
+
statusCode: number,
|
|
60
|
+
payload: unknown,
|
|
61
|
+
): void {
|
|
62
|
+
res.writeHead(statusCode, {
|
|
63
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
64
|
+
});
|
|
54
65
|
res.end(JSON.stringify(payload, null, 2));
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -81,12 +92,15 @@ function resolveSiteFile(pathname: string): string | null {
|
|
|
81
92
|
const normalized = path.normalize(cleanPath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
82
93
|
const candidate = path.resolve(SITE_DIR, `.${normalized}`);
|
|
83
94
|
if (!candidate.startsWith(SITE_DIR)) return null;
|
|
84
|
-
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
|
|
95
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
|
|
96
|
+
return null;
|
|
85
97
|
return candidate;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function serveStatic(pathname: string, res: ServerResponse): boolean {
|
|
89
|
-
const filePath = resolveSiteFile(
|
|
101
|
+
const filePath = resolveSiteFile(
|
|
102
|
+
pathname === '/chat' ? '/chat.html' : pathname,
|
|
103
|
+
);
|
|
90
104
|
if (!filePath) return false;
|
|
91
105
|
const ext = path.extname(filePath).toLowerCase();
|
|
92
106
|
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
@@ -95,8 +109,11 @@ function serveStatic(pathname: string, res: ServerResponse): boolean {
|
|
|
95
109
|
return true;
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
async function handleApiChat(
|
|
99
|
-
|
|
112
|
+
async function handleApiChat(
|
|
113
|
+
req: IncomingMessage,
|
|
114
|
+
res: ServerResponse,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const body = (await readJsonBody(req)) as Partial<ApiChatRequestBody>;
|
|
100
117
|
const wantsStream = body.stream === true;
|
|
101
118
|
|
|
102
119
|
const content = body.content?.trim();
|
|
@@ -177,7 +194,10 @@ async function handleApiChatStream(
|
|
|
177
194
|
error: errorMessage,
|
|
178
195
|
},
|
|
179
196
|
});
|
|
180
|
-
logger.error(
|
|
197
|
+
logger.error(
|
|
198
|
+
{ error, reqUrl: '/api/chat' },
|
|
199
|
+
'Gateway streaming chat failed',
|
|
200
|
+
);
|
|
181
201
|
} finally {
|
|
182
202
|
if (!res.writableEnded) {
|
|
183
203
|
res.end();
|
|
@@ -191,11 +211,18 @@ async function handleApiChatStream(
|
|
|
191
211
|
});
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
async function handleApiCommand(
|
|
195
|
-
|
|
196
|
-
|
|
214
|
+
async function handleApiCommand(
|
|
215
|
+
req: IncomingMessage,
|
|
216
|
+
res: ServerResponse,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const body = (await readJsonBody(req)) as Partial<GatewayCommandRequest>;
|
|
219
|
+
const args = Array.isArray(body.args)
|
|
220
|
+
? body.args.map((value) => String(value))
|
|
221
|
+
: [];
|
|
197
222
|
if (args.length === 0) {
|
|
198
|
-
sendJson(res, 400, {
|
|
223
|
+
sendJson(res, 400, {
|
|
224
|
+
error: 'Missing command. Provide non-empty `args` array.',
|
|
225
|
+
});
|
|
199
226
|
return;
|
|
200
227
|
}
|
|
201
228
|
|
|
@@ -209,12 +236,20 @@ async function handleApiCommand(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
209
236
|
sendJson(res, result.kind === 'error' ? 400 : 200, result);
|
|
210
237
|
}
|
|
211
238
|
|
|
212
|
-
async function handleApiDiscordAction(
|
|
213
|
-
|
|
239
|
+
async function handleApiDiscordAction(
|
|
240
|
+
req: IncomingMessage,
|
|
241
|
+
res: ServerResponse,
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
const body = (await readJsonBody(req)) as ApiDiscordActionRequestBody;
|
|
214
244
|
const action = typeof body.action === 'string' ? body.action.trim() : '';
|
|
215
|
-
if (
|
|
245
|
+
if (
|
|
246
|
+
action !== 'read' &&
|
|
247
|
+
action !== 'member-info' &&
|
|
248
|
+
action !== 'channel-info'
|
|
249
|
+
) {
|
|
216
250
|
sendJson(res, 400, {
|
|
217
|
-
error:
|
|
251
|
+
error:
|
|
252
|
+
'Invalid `action`. Allowed: "read", "member-info", "channel-info".',
|
|
218
253
|
});
|
|
219
254
|
return;
|
|
220
255
|
}
|
|
@@ -268,7 +303,9 @@ export function startHealthServer(): void {
|
|
|
268
303
|
|
|
269
304
|
if (pathname.startsWith('/api/')) {
|
|
270
305
|
if (!hasApiAuth(req)) {
|
|
271
|
-
sendJson(res, 401, {
|
|
306
|
+
sendJson(res, 401, {
|
|
307
|
+
error: 'Unauthorized. Set `Authorization: Bearer <WEB_API_TOKEN>`.',
|
|
308
|
+
});
|
|
272
309
|
return;
|
|
273
310
|
}
|
|
274
311
|
|
|
@@ -312,6 +349,9 @@ export function startHealthServer(): void {
|
|
|
312
349
|
});
|
|
313
350
|
|
|
314
351
|
server.listen(HEALTH_PORT, HEALTH_HOST, () => {
|
|
315
|
-
logger.info(
|
|
352
|
+
logger.info(
|
|
353
|
+
{ host: HEALTH_HOST, port: HEALTH_PORT },
|
|
354
|
+
'Gateway HTTP server started',
|
|
355
|
+
);
|
|
316
356
|
});
|
|
317
357
|
}
|
package/src/heartbeat.ts
CHANGED
|
@@ -2,17 +2,35 @@
|
|
|
2
2
|
* Heartbeat — periodic poll so the agent can proactively check tasks,
|
|
3
3
|
* maintain memory, and reach out when needed.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { runAgent } from './agent.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
emitToolExecutionAuditEvents,
|
|
9
|
+
makeAuditRunId,
|
|
10
|
+
recordAuditEvent,
|
|
11
|
+
} from './audit-events.js';
|
|
12
|
+
import {
|
|
13
|
+
HEARTBEAT_CHANNEL,
|
|
14
|
+
HEARTBEAT_ENABLED,
|
|
15
|
+
HYBRIDAI_CHATBOT_ID,
|
|
16
|
+
HYBRIDAI_ENABLE_RAG,
|
|
17
|
+
HYBRIDAI_MODEL,
|
|
18
|
+
} from './config.js';
|
|
19
|
+
import { buildConversationContext } from './conversation.js';
|
|
20
|
+
import { getTasksForSession } from './db.js';
|
|
8
21
|
import { logger } from './logger.js';
|
|
9
|
-
import {
|
|
22
|
+
import { memoryService } from './memory-service.js';
|
|
23
|
+
import {
|
|
24
|
+
isWithinActiveHours,
|
|
25
|
+
proactiveWindowLabel,
|
|
26
|
+
} from './proactive-policy.js';
|
|
10
27
|
import { maybeCompactSession } from './session-maintenance.js';
|
|
11
28
|
import { appendSessionTranscript } from './session-transcripts.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
import { processSideEffects } from './side-effects.js';
|
|
30
|
+
import {
|
|
31
|
+
estimateTokenCountFromMessages,
|
|
32
|
+
estimateTokenCountFromText,
|
|
33
|
+
} from './token-efficiency.js';
|
|
16
34
|
|
|
17
35
|
const HEARTBEAT_PROMPT =
|
|
18
36
|
'[Heartbeat poll] Check HEARTBEAT.md for periodic tasks. If nothing needs attention, reply HEARTBEAT_OK.';
|
|
@@ -53,7 +71,10 @@ let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
53
71
|
let running = false;
|
|
54
72
|
|
|
55
73
|
function isHeartbeatOk(text: string): boolean {
|
|
56
|
-
const normalized = text
|
|
74
|
+
const normalized = text
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/[^a-z_]/gi, '')
|
|
77
|
+
.toUpperCase();
|
|
57
78
|
return normalized === 'HEARTBEATOK' || normalized.startsWith('HEARTBEATOK');
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -75,7 +96,10 @@ export function startHeartbeat(
|
|
|
75
96
|
return;
|
|
76
97
|
}
|
|
77
98
|
if (!isWithinActiveHours()) {
|
|
78
|
-
logger.debug(
|
|
99
|
+
logger.debug(
|
|
100
|
+
{ activeHours: proactiveWindowLabel() },
|
|
101
|
+
'Heartbeat skipped — outside active hours window',
|
|
102
|
+
);
|
|
79
103
|
return;
|
|
80
104
|
}
|
|
81
105
|
running = true;
|
|
@@ -87,13 +111,24 @@ export function startHeartbeat(
|
|
|
87
111
|
let turnIndex = 1;
|
|
88
112
|
|
|
89
113
|
try {
|
|
90
|
-
const session = getOrCreateSession(
|
|
114
|
+
const session = memoryService.getOrCreateSession(
|
|
115
|
+
sessionId,
|
|
116
|
+
null,
|
|
117
|
+
channelId,
|
|
118
|
+
);
|
|
91
119
|
turnIndex = session.message_count + 1;
|
|
92
120
|
|
|
93
|
-
const history = getConversationHistory(
|
|
121
|
+
const history = memoryService.getConversationHistory(
|
|
122
|
+
sessionId,
|
|
123
|
+
MAX_HEARTBEAT_HISTORY,
|
|
124
|
+
);
|
|
125
|
+
const memoryContext = memoryService.buildPromptMemoryContext({
|
|
126
|
+
session,
|
|
127
|
+
query: HEARTBEAT_PROMPT,
|
|
128
|
+
});
|
|
94
129
|
const { messages } = buildConversationContext({
|
|
95
130
|
agentId,
|
|
96
|
-
sessionSummary:
|
|
131
|
+
sessionSummary: memoryContext.promptSummary,
|
|
97
132
|
history,
|
|
98
133
|
});
|
|
99
134
|
messages.push({ role: 'user', content: HEARTBEAT_PROMPT });
|
|
@@ -141,13 +176,24 @@ export function startHeartbeat(
|
|
|
141
176
|
toolExecutions: output.toolExecutions || [],
|
|
142
177
|
});
|
|
143
178
|
const tokenUsage = output.tokenUsage;
|
|
144
|
-
const estimatedPromptTokens =
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
const estimatedPromptTokens =
|
|
180
|
+
tokenUsage?.estimatedPromptTokens ||
|
|
181
|
+
estimateTokenCountFromMessages(messages);
|
|
182
|
+
const estimatedCompletionTokens =
|
|
183
|
+
tokenUsage?.estimatedCompletionTokens ||
|
|
184
|
+
estimateTokenCountFromText(output.result || '');
|
|
185
|
+
const estimatedTotalTokens =
|
|
186
|
+
tokenUsage?.estimatedTotalTokens ||
|
|
187
|
+
estimatedPromptTokens + estimatedCompletionTokens;
|
|
147
188
|
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
148
189
|
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
149
190
|
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
150
|
-
const apiTotalTokens =
|
|
191
|
+
const apiTotalTokens =
|
|
192
|
+
tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
|
|
193
|
+
const apiCacheUsageAvailable =
|
|
194
|
+
tokenUsage?.apiCacheUsageAvailable === true;
|
|
195
|
+
const apiCacheReadTokens = tokenUsage?.apiCacheReadTokens || 0;
|
|
196
|
+
const apiCacheWriteTokens = tokenUsage?.apiCacheWriteTokens || 0;
|
|
151
197
|
recordAuditEvent({
|
|
152
198
|
sessionId,
|
|
153
199
|
runId,
|
|
@@ -158,9 +204,15 @@ export function startHeartbeat(
|
|
|
158
204
|
durationMs: Date.now() - startedAt,
|
|
159
205
|
toolCallCount: (output.toolExecutions || []).length,
|
|
160
206
|
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
161
|
-
promptTokens: apiUsageAvailable
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
promptTokens: apiUsageAvailable
|
|
208
|
+
? apiPromptTokens
|
|
209
|
+
: estimatedPromptTokens,
|
|
210
|
+
completionTokens: apiUsageAvailable
|
|
211
|
+
? apiCompletionTokens
|
|
212
|
+
: estimatedCompletionTokens,
|
|
213
|
+
totalTokens: apiUsageAvailable
|
|
214
|
+
? apiTotalTokens
|
|
215
|
+
: estimatedTotalTokens,
|
|
164
216
|
estimatedPromptTokens,
|
|
165
217
|
estimatedCompletionTokens,
|
|
166
218
|
estimatedTotalTokens,
|
|
@@ -168,6 +220,17 @@ export function startHeartbeat(
|
|
|
168
220
|
apiPromptTokens,
|
|
169
221
|
apiCompletionTokens,
|
|
170
222
|
apiTotalTokens,
|
|
223
|
+
...(apiCacheUsageAvailable
|
|
224
|
+
? {
|
|
225
|
+
apiCacheUsageAvailable,
|
|
226
|
+
apiCacheReadTokens,
|
|
227
|
+
apiCacheWriteTokens,
|
|
228
|
+
cacheReadTokens: apiCacheReadTokens,
|
|
229
|
+
cacheReadInputTokens: apiCacheReadTokens,
|
|
230
|
+
cacheWriteTokens: apiCacheWriteTokens,
|
|
231
|
+
cacheWriteInputTokens: apiCacheWriteTokens,
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
171
234
|
},
|
|
172
235
|
});
|
|
173
236
|
processSideEffects(output, sessionId, heartbeatChannelId);
|
|
@@ -241,8 +304,19 @@ export function startHeartbeat(
|
|
|
241
304
|
}
|
|
242
305
|
|
|
243
306
|
// Real content — persist and deliver
|
|
244
|
-
|
|
245
|
-
|
|
307
|
+
memoryService.storeTurn({
|
|
308
|
+
sessionId,
|
|
309
|
+
user: {
|
|
310
|
+
userId: 'heartbeat',
|
|
311
|
+
username: 'heartbeat',
|
|
312
|
+
content: HEARTBEAT_PROMPT,
|
|
313
|
+
},
|
|
314
|
+
assistant: {
|
|
315
|
+
userId: 'assistant',
|
|
316
|
+
username: null,
|
|
317
|
+
content: result,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
246
320
|
appendSessionTranscript(agentId, {
|
|
247
321
|
sessionId,
|
|
248
322
|
channelId: heartbeatChannelId,
|
|
@@ -267,7 +341,10 @@ export function startHeartbeat(
|
|
|
267
341
|
model: HYBRIDAI_MODEL,
|
|
268
342
|
channelId: heartbeatChannelId,
|
|
269
343
|
});
|
|
270
|
-
logger.info(
|
|
344
|
+
logger.info(
|
|
345
|
+
{ length: result.length },
|
|
346
|
+
'Heartbeat: agent has something to say',
|
|
347
|
+
);
|
|
271
348
|
recordAuditEvent({
|
|
272
349
|
sessionId,
|
|
273
350
|
runId,
|
package/src/hybridai-bots.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHybridAIApiKey, HYBRIDAI_BASE_URL } from './config.js';
|
|
2
2
|
import type { HybridAIBot } from './types.js';
|
|
3
3
|
|
|
4
4
|
interface BotCacheEntry {
|
|
@@ -10,18 +10,27 @@ let botCache: BotCacheEntry | null = null;
|
|
|
10
10
|
|
|
11
11
|
function normalizeBots(payload: unknown): HybridAIBot[] {
|
|
12
12
|
const data = payload as
|
|
13
|
-
| {
|
|
13
|
+
| {
|
|
14
|
+
data?: Record<string, unknown>[];
|
|
15
|
+
bots?: Record<string, unknown>[];
|
|
16
|
+
items?: Record<string, unknown>[];
|
|
17
|
+
}
|
|
14
18
|
| Record<string, unknown>[];
|
|
15
|
-
const raw = Array.isArray(data)
|
|
19
|
+
const raw = Array.isArray(data)
|
|
20
|
+
? data
|
|
21
|
+
: data.data || data.bots || data.items || [];
|
|
16
22
|
|
|
17
23
|
return raw.map((item) => ({
|
|
18
24
|
id: String(item.id ?? item._id ?? item.chatbot_id ?? item.bot_id ?? ''),
|
|
19
25
|
name: String(item.bot_name ?? item.name ?? 'Unnamed'),
|
|
20
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
item.description != null ? String(item.description) : undefined,
|
|
21
28
|
}));
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
export async function fetchHybridAIBots(options?: {
|
|
31
|
+
export async function fetchHybridAIBots(options?: {
|
|
32
|
+
cacheTtlMs?: number;
|
|
33
|
+
}): Promise<HybridAIBot[]> {
|
|
25
34
|
const cacheTtlMs = Math.max(0, options?.cacheTtlMs ?? 0);
|
|
26
35
|
const now = Date.now();
|
|
27
36
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
interface HybridAIModel {
|
|
2
|
+
id: string;
|
|
3
|
+
contextWindowTokens: number | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Source: ../../examples/pi-mono/packages/ai/src/models.generated.ts
|
|
7
|
+
// Keep this list intentionally small and focused on the GPT-5 family we use.
|
|
8
|
+
const STATIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
9
|
+
// Claude 4.6
|
|
10
|
+
'claude-opus-4-6': 200_000,
|
|
11
|
+
'claude-opus-4.6': 200_000,
|
|
12
|
+
'claude-sonnet-4-6': 200_000,
|
|
13
|
+
'claude-sonnet-4.6': 200_000,
|
|
14
|
+
|
|
15
|
+
// Gemini 3 / 3.1
|
|
16
|
+
'gemini-3': 1_048_576,
|
|
17
|
+
'gemini-3-pro': 1_048_576,
|
|
18
|
+
'gemini-3-flash': 1_048_576,
|
|
19
|
+
'gemini-3.1': 1_048_576,
|
|
20
|
+
'gemini-3.1-pro': 1_048_576,
|
|
21
|
+
'gemini-3.1-pro-high': 1_048_576,
|
|
22
|
+
'gemini-3.1-pro-low': 1_048_576,
|
|
23
|
+
'gemini-3-pro-preview': 1_048_576,
|
|
24
|
+
'gemini-3-flash-preview': 1_048_576,
|
|
25
|
+
'gemini-3.1-pro-preview': 1_048_576,
|
|
26
|
+
|
|
27
|
+
// GPT-5 family
|
|
28
|
+
'gpt-5': 400_000,
|
|
29
|
+
'gpt-5-chat-latest': 128_000,
|
|
30
|
+
'gpt-5-codex': 400_000,
|
|
31
|
+
'gpt-5-mini': 400_000,
|
|
32
|
+
'gpt-5-nano': 400_000,
|
|
33
|
+
'gpt-5-pro': 400_000,
|
|
34
|
+
'gpt-5.1': 400_000,
|
|
35
|
+
'gpt-5.1-chat-latest': 128_000,
|
|
36
|
+
'gpt-5.1-codex': 400_000,
|
|
37
|
+
'gpt-5.1-codex-max': 400_000,
|
|
38
|
+
'gpt-5.1-codex-mini': 400_000,
|
|
39
|
+
'gpt-5.2': 400_000,
|
|
40
|
+
'gpt-5.2-chat-latest': 128_000,
|
|
41
|
+
'gpt-5.2-codex': 400_000,
|
|
42
|
+
'gpt-5.2-pro': 400_000,
|
|
43
|
+
'gpt-5.3-codex': 400_000,
|
|
44
|
+
'gpt-5.3-codex-spark': 128_000,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function resolveModelContextWindowFromList(
|
|
48
|
+
models: HybridAIModel[],
|
|
49
|
+
modelName: string,
|
|
50
|
+
): number | null {
|
|
51
|
+
const normalizeModelIdTail = (modelId: string): string => {
|
|
52
|
+
const normalized = modelId.trim().toLowerCase();
|
|
53
|
+
return normalized.includes('/')
|
|
54
|
+
? (normalized.split('/').at(-1) ?? normalized)
|
|
55
|
+
: normalized;
|
|
56
|
+
};
|
|
57
|
+
const matchesModelFamily = (
|
|
58
|
+
candidateId: string,
|
|
59
|
+
targetId: string,
|
|
60
|
+
): boolean => {
|
|
61
|
+
if (!candidateId || !targetId) return false;
|
|
62
|
+
if (candidateId === targetId) return true;
|
|
63
|
+
const boundary = candidateId.at(targetId.length);
|
|
64
|
+
return (
|
|
65
|
+
candidateId.startsWith(targetId) &&
|
|
66
|
+
(boundary === '-' ||
|
|
67
|
+
boundary === '.' ||
|
|
68
|
+
boundary === ':' ||
|
|
69
|
+
boundary === '/')
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const target = modelName.trim().toLowerCase();
|
|
74
|
+
if (!target) return null;
|
|
75
|
+
|
|
76
|
+
const direct = models.find(
|
|
77
|
+
(entry) =>
|
|
78
|
+
entry.contextWindowTokens != null &&
|
|
79
|
+
entry.id.trim().toLowerCase() === target,
|
|
80
|
+
);
|
|
81
|
+
if (direct?.contextWindowTokens != null) return direct.contextWindowTokens;
|
|
82
|
+
|
|
83
|
+
const targetTail = target.includes('/')
|
|
84
|
+
? (target.split('/').at(-1) ?? '')
|
|
85
|
+
: target;
|
|
86
|
+
if (!targetTail) return null;
|
|
87
|
+
|
|
88
|
+
const tailMatch = models.find((entry) => {
|
|
89
|
+
if (entry.contextWindowTokens == null) return false;
|
|
90
|
+
const normalizedTail = normalizeModelIdTail(entry.id);
|
|
91
|
+
return normalizedTail === targetTail;
|
|
92
|
+
});
|
|
93
|
+
if (tailMatch?.contextWindowTokens != null)
|
|
94
|
+
return tailMatch.contextWindowTokens;
|
|
95
|
+
|
|
96
|
+
const familyMatch = models
|
|
97
|
+
.filter((entry) => entry.contextWindowTokens != null)
|
|
98
|
+
.map((entry) => ({
|
|
99
|
+
contextWindowTokens: entry.contextWindowTokens as number,
|
|
100
|
+
tail: normalizeModelIdTail(entry.id),
|
|
101
|
+
}))
|
|
102
|
+
.filter((entry) => matchesModelFamily(entry.tail, targetTail))
|
|
103
|
+
.sort((a, b) => b.tail.length - a.tail.length)
|
|
104
|
+
.at(0);
|
|
105
|
+
return familyMatch?.contextWindowTokens ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveModelContextWindowFallback(
|
|
109
|
+
modelName: string,
|
|
110
|
+
): number | null {
|
|
111
|
+
const matchesModelFamily = (
|
|
112
|
+
candidateId: string,
|
|
113
|
+
targetId: string,
|
|
114
|
+
): boolean => {
|
|
115
|
+
if (!candidateId || !targetId) return false;
|
|
116
|
+
if (candidateId === targetId) return true;
|
|
117
|
+
const boundary = candidateId.at(targetId.length);
|
|
118
|
+
return (
|
|
119
|
+
candidateId.startsWith(targetId) &&
|
|
120
|
+
(boundary === '-' ||
|
|
121
|
+
boundary === '.' ||
|
|
122
|
+
boundary === ':' ||
|
|
123
|
+
boundary === '/')
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const normalized = modelName.trim().toLowerCase();
|
|
128
|
+
if (!normalized) return null;
|
|
129
|
+
|
|
130
|
+
const direct = STATIC_MODEL_CONTEXT_WINDOWS[normalized];
|
|
131
|
+
if (direct != null) return direct;
|
|
132
|
+
|
|
133
|
+
const slashTail = normalized.includes('/')
|
|
134
|
+
? (normalized.split('/').at(-1) ?? '')
|
|
135
|
+
: normalized;
|
|
136
|
+
if (slashTail && STATIC_MODEL_CONTEXT_WINDOWS[slashTail] != null) {
|
|
137
|
+
return STATIC_MODEL_CONTEXT_WINDOWS[slashTail];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const colonTail = normalized.includes(':')
|
|
141
|
+
? (normalized.split(':').at(-1) ?? '')
|
|
142
|
+
: normalized;
|
|
143
|
+
if (colonTail && STATIC_MODEL_CONTEXT_WINDOWS[colonTail] != null) {
|
|
144
|
+
return STATIC_MODEL_CONTEXT_WINDOWS[colonTail];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Family fallback for versioned ids, e.g. "gpt-5.1-2025-11-13".
|
|
148
|
+
const familyCandidates = [slashTail, colonTail, normalized].filter(Boolean);
|
|
149
|
+
for (const candidate of familyCandidates) {
|
|
150
|
+
const bestMatch = Object.keys(STATIC_MODEL_CONTEXT_WINDOWS)
|
|
151
|
+
.filter((key) => matchesModelFamily(candidate, key))
|
|
152
|
+
.sort((a, b) => b.length - a.length)
|
|
153
|
+
.at(0);
|
|
154
|
+
if (bestMatch) return STATIC_MODEL_CONTEXT_WINDOWS[bestMatch] ?? null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
@@ -22,7 +22,10 @@ function ensureAuditReady(): boolean {
|
|
|
22
22
|
initDatabase({ quiet: true });
|
|
23
23
|
auditReady = true;
|
|
24
24
|
} catch (err) {
|
|
25
|
-
logger.warn(
|
|
25
|
+
logger.warn(
|
|
26
|
+
{ err },
|
|
27
|
+
'Failed to initialize DB for instruction approval audit',
|
|
28
|
+
);
|
|
26
29
|
auditReady = false;
|
|
27
30
|
}
|
|
28
31
|
return auditReady;
|
|
@@ -2,9 +2,18 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
export const INSTRUCTION_FILES = [
|
|
5
|
+
export const INSTRUCTION_FILES = [
|
|
6
|
+
'AGENTS.md',
|
|
7
|
+
'SECURITY.md',
|
|
8
|
+
'TRUST_MODEL.md',
|
|
9
|
+
] as const;
|
|
6
10
|
export const INSTRUCTION_BASELINE_VERSION = 1;
|
|
7
|
-
export const INSTRUCTION_BASELINE_PATH = path.join(
|
|
11
|
+
export const INSTRUCTION_BASELINE_PATH = path.join(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
'data',
|
|
14
|
+
'audit',
|
|
15
|
+
'instruction-hashes.json',
|
|
16
|
+
);
|
|
8
17
|
|
|
9
18
|
export interface InstructionHashBaseline {
|
|
10
19
|
version: number;
|
|
@@ -29,7 +38,9 @@ export interface InstructionIntegrityResult {
|
|
|
29
38
|
files: InstructionFileResult[];
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
export function summarizeInstructionIntegrity(
|
|
41
|
+
export function summarizeInstructionIntegrity(
|
|
42
|
+
result: InstructionIntegrityResult,
|
|
43
|
+
): string {
|
|
33
44
|
if (result.baselineError) return `baseline.invalid (${result.baselineError})`;
|
|
34
45
|
if (!result.baseline) return 'baseline.missing';
|
|
35
46
|
|
|
@@ -62,19 +73,24 @@ export function loadInstructionBaseline(): InstructionHashBaseline | null {
|
|
|
62
73
|
|
|
63
74
|
const raw = fs.readFileSync(INSTRUCTION_BASELINE_PATH, 'utf-8');
|
|
64
75
|
const parsed = JSON.parse(raw) as unknown;
|
|
65
|
-
if (!isRecord(parsed))
|
|
76
|
+
if (!isRecord(parsed))
|
|
77
|
+
throw new Error('Instruction baseline is not a JSON object.');
|
|
66
78
|
|
|
67
79
|
const version = parsed.version;
|
|
68
80
|
const approvedAt = parsed.approvedAt;
|
|
69
81
|
const files = parsed.files;
|
|
70
|
-
if (typeof version !== 'number')
|
|
82
|
+
if (typeof version !== 'number')
|
|
83
|
+
throw new Error('Instruction baseline is missing numeric `version`.');
|
|
71
84
|
if (version !== INSTRUCTION_BASELINE_VERSION) {
|
|
72
|
-
throw new Error(
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Instruction baseline version ${String(version)} is unsupported.`,
|
|
87
|
+
);
|
|
73
88
|
}
|
|
74
89
|
if (typeof approvedAt !== 'string' || !approvedAt.trim()) {
|
|
75
90
|
throw new Error('Instruction baseline is missing `approvedAt`.');
|
|
76
91
|
}
|
|
77
|
-
if (!isRecord(files))
|
|
92
|
+
if (!isRecord(files))
|
|
93
|
+
throw new Error('Instruction baseline is missing `files` object.');
|
|
78
94
|
|
|
79
95
|
const normalizedFiles: Record<string, string> = {};
|
|
80
96
|
for (const relPath of INSTRUCTION_FILES) {
|
|
@@ -95,7 +111,9 @@ export function approveInstructionBaseline(): InstructionHashBaseline {
|
|
|
95
111
|
const hashes = computeCurrentHashes();
|
|
96
112
|
const missing = INSTRUCTION_FILES.filter((relPath) => !hashes[relPath]);
|
|
97
113
|
if (missing.length > 0) {
|
|
98
|
-
throw new Error(
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Approval failed: missing instruction files (${missing.join(', ')}).`,
|
|
116
|
+
);
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
const baseline: InstructionHashBaseline = {
|
|
@@ -165,7 +183,10 @@ export function verifyInstructionBaseline(): InstructionIntegrityResult {
|
|
|
165
183
|
};
|
|
166
184
|
});
|
|
167
185
|
|
|
168
|
-
const ok =
|
|
186
|
+
const ok =
|
|
187
|
+
baselineError === null &&
|
|
188
|
+
baseline !== null &&
|
|
189
|
+
files.every((file) => file.status === 'ok');
|
|
169
190
|
return {
|
|
170
191
|
ok,
|
|
171
192
|
baselinePath: INSTRUCTION_BASELINE_PATH,
|