@hybridaione/hybridclaw 0.2.2 → 0.2.6
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 +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- 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 +19 -3
- 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 +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- 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 +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- 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 +16 -8
- 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 +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -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 +563 -73
- 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 +179 -24
- 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 +48 -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/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 +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- 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 +29 -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 +146 -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 +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- 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 +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- 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 +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- 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 +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -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 +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- 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 +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -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 +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- 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/mount-security.ts
CHANGED
|
@@ -73,7 +73,11 @@ export function loadMountAllowlist(): MountAllowlist | null {
|
|
|
73
73
|
|
|
74
74
|
cachedAllowlist = allowlist;
|
|
75
75
|
logger.info(
|
|
76
|
-
{
|
|
76
|
+
{
|
|
77
|
+
path: MOUNT_ALLOWLIST_PATH,
|
|
78
|
+
allowedRoots: allowlist.allowedRoots.length,
|
|
79
|
+
blockedPatterns: allowlist.blockedPatterns.length,
|
|
80
|
+
},
|
|
77
81
|
'Mount allowlist loaded',
|
|
78
82
|
);
|
|
79
83
|
return cachedAllowlist;
|
|
@@ -102,7 +106,10 @@ function getRealPath(p: string): string | null {
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
function matchesBlockedPattern(
|
|
109
|
+
function matchesBlockedPattern(
|
|
110
|
+
realPath: string,
|
|
111
|
+
blockedPatterns: string[],
|
|
112
|
+
): string | null {
|
|
106
113
|
const pathParts = realPath.split(path.sep);
|
|
107
114
|
for (const pattern of blockedPatterns) {
|
|
108
115
|
for (const part of pathParts) {
|
|
@@ -113,7 +120,10 @@ function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): str
|
|
|
113
120
|
return null;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
function findAllowedRoot(
|
|
123
|
+
function findAllowedRoot(
|
|
124
|
+
realPath: string,
|
|
125
|
+
allowedRoots: AllowedRoot[],
|
|
126
|
+
): AllowedRoot | null {
|
|
117
127
|
for (const root of allowedRoots) {
|
|
118
128
|
const realRoot = getRealPath(expandPath(root.path));
|
|
119
129
|
if (realRoot === null) continue;
|
|
@@ -144,23 +154,38 @@ export interface MountValidationResult {
|
|
|
144
154
|
export function validateMount(mount: AdditionalMount): MountValidationResult {
|
|
145
155
|
const allowlist = loadMountAllowlist();
|
|
146
156
|
if (allowlist === null) {
|
|
147
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
allowed: false,
|
|
159
|
+
reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`,
|
|
160
|
+
};
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
const containerPath = mount.containerPath || path.basename(mount.hostPath);
|
|
151
164
|
if (!isValidContainerPath(containerPath)) {
|
|
152
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
allowed: false,
|
|
167
|
+
reason: `Invalid container path: "${containerPath}" — must be relative, non-empty, and not contain ".."`,
|
|
168
|
+
};
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
const expandedPath = expandPath(mount.hostPath);
|
|
156
172
|
const realPath = getRealPath(expandedPath);
|
|
157
173
|
if (realPath === null) {
|
|
158
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
allowed: false,
|
|
176
|
+
reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`,
|
|
177
|
+
};
|
|
159
178
|
}
|
|
160
179
|
|
|
161
|
-
const blockedMatch = matchesBlockedPattern(
|
|
180
|
+
const blockedMatch = matchesBlockedPattern(
|
|
181
|
+
realPath,
|
|
182
|
+
allowlist.blockedPatterns,
|
|
183
|
+
);
|
|
162
184
|
if (blockedMatch !== null) {
|
|
163
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
allowed: false,
|
|
187
|
+
reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`,
|
|
188
|
+
};
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots);
|
|
@@ -176,7 +201,10 @@ export function validateMount(mount: AdditionalMount): MountValidationResult {
|
|
|
176
201
|
if (mount.readonly === false && allowedRoot.allowReadWrite) {
|
|
177
202
|
effectiveReadonly = false;
|
|
178
203
|
} else if (mount.readonly === false && !allowedRoot.allowReadWrite) {
|
|
179
|
-
logger.info(
|
|
204
|
+
logger.info(
|
|
205
|
+
{ mount: mount.hostPath, root: allowedRoot.path },
|
|
206
|
+
'Mount forced to read-only — root does not allow read-write',
|
|
207
|
+
);
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
return {
|
|
@@ -195,7 +223,11 @@ export function validateMount(mount: AdditionalMount): MountValidationResult {
|
|
|
195
223
|
export function validateAdditionalMounts(
|
|
196
224
|
mounts: AdditionalMount[],
|
|
197
225
|
): Array<{ hostPath: string; containerPath: string; readonly: boolean }> {
|
|
198
|
-
const validated: Array<{
|
|
226
|
+
const validated: Array<{
|
|
227
|
+
hostPath: string;
|
|
228
|
+
containerPath: string;
|
|
229
|
+
readonly: boolean;
|
|
230
|
+
}> = [];
|
|
199
231
|
|
|
200
232
|
for (const mount of mounts) {
|
|
201
233
|
const result = validateMount(mount);
|
|
@@ -205,12 +237,25 @@ export function validateAdditionalMounts(
|
|
|
205
237
|
containerPath: `/workspace/extra/${result.resolvedContainerPath}`,
|
|
206
238
|
readonly: result.effectiveReadonly!,
|
|
207
239
|
});
|
|
208
|
-
logger.debug(
|
|
240
|
+
logger.debug(
|
|
241
|
+
{
|
|
242
|
+
hostPath: result.realHostPath,
|
|
243
|
+
containerPath: result.resolvedContainerPath,
|
|
244
|
+
readonly: result.effectiveReadonly,
|
|
245
|
+
},
|
|
246
|
+
'Mount validated',
|
|
247
|
+
);
|
|
209
248
|
} else {
|
|
210
|
-
logger.warn(
|
|
249
|
+
logger.warn(
|
|
250
|
+
{
|
|
251
|
+
requestedPath: mount.hostPath,
|
|
252
|
+
containerPath: mount.containerPath,
|
|
253
|
+
reason: result.reason,
|
|
254
|
+
},
|
|
255
|
+
'Additional mount REJECTED',
|
|
256
|
+
);
|
|
211
257
|
}
|
|
212
258
|
}
|
|
213
259
|
|
|
214
260
|
return validated;
|
|
215
261
|
}
|
|
216
|
-
|
|
@@ -133,7 +133,9 @@ function normalizeIngestUrl(baseUrl: string, ingestPath: string): string {
|
|
|
133
133
|
const trimmedPath = ingestPath.trim();
|
|
134
134
|
if (/^https?:\/\//i.test(trimmedPath)) return trimmedPath;
|
|
135
135
|
const normalizedBase = baseUrl.replace(/\/+$/, '');
|
|
136
|
-
const normalizedPath = trimmedPath.startsWith('/')
|
|
136
|
+
const normalizedPath = trimmedPath.startsWith('/')
|
|
137
|
+
? trimmedPath
|
|
138
|
+
: `/${trimmedPath}`;
|
|
137
139
|
return `${normalizedBase}${normalizedPath}`;
|
|
138
140
|
}
|
|
139
141
|
|
|
@@ -141,12 +143,23 @@ function resolveConfig(): ResolvedIngestConfig {
|
|
|
141
143
|
const botId = OBSERVABILITY_BOT_ID.trim() || HYBRIDAI_CHATBOT_ID.trim();
|
|
142
144
|
const label = OBSERVABILITY_LABEL.trim() || os.hostname();
|
|
143
145
|
const environment = OBSERVABILITY_ENVIRONMENT.trim() || 'prod';
|
|
144
|
-
const batchMaxEvents = clampInteger(
|
|
145
|
-
|
|
146
|
+
const batchMaxEvents = clampInteger(
|
|
147
|
+
OBSERVABILITY_BATCH_MAX_EVENTS,
|
|
148
|
+
1,
|
|
149
|
+
PLATFORM_MAX_EVENTS,
|
|
150
|
+
);
|
|
151
|
+
const flushIntervalMs = clampInteger(
|
|
152
|
+
OBSERVABILITY_FLUSH_INTERVAL_MS,
|
|
153
|
+
1_000,
|
|
154
|
+
3_600_000,
|
|
155
|
+
);
|
|
146
156
|
|
|
147
157
|
return {
|
|
148
158
|
enabled: OBSERVABILITY_ENABLED,
|
|
149
|
-
ingestUrl: normalizeIngestUrl(
|
|
159
|
+
ingestUrl: normalizeIngestUrl(
|
|
160
|
+
OBSERVABILITY_BASE_URL,
|
|
161
|
+
OBSERVABILITY_INGEST_PATH,
|
|
162
|
+
),
|
|
150
163
|
tokenAdminUrl: normalizeIngestUrl(OBSERVABILITY_BASE_URL, TOKEN_ADMIN_PATH),
|
|
151
164
|
apiKey: HYBRIDAI_API_KEY.trim(),
|
|
152
165
|
botId,
|
|
@@ -159,12 +172,24 @@ function resolveConfig(): ResolvedIngestConfig {
|
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
function validateConfig(config: ResolvedIngestConfig): {
|
|
175
|
+
function validateConfig(config: ResolvedIngestConfig): {
|
|
176
|
+
ok: boolean;
|
|
177
|
+
reason: string | null;
|
|
178
|
+
} {
|
|
163
179
|
if (!config.enabled) return { ok: false, reason: 'disabled' };
|
|
164
|
-
if (!config.botId)
|
|
165
|
-
|
|
180
|
+
if (!config.botId)
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
reason: 'missing observability.botId (or hybridai.defaultChatbotId)',
|
|
184
|
+
};
|
|
185
|
+
if (!config.agentId)
|
|
186
|
+
return { ok: false, reason: 'missing observability.agentId' };
|
|
166
187
|
if (!config.apiKey) {
|
|
167
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason:
|
|
191
|
+
'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)',
|
|
192
|
+
};
|
|
168
193
|
}
|
|
169
194
|
return { ok: true, reason: null };
|
|
170
195
|
}
|
|
@@ -205,12 +230,18 @@ function parsePayload(raw: string): Record<string, unknown> {
|
|
|
205
230
|
return {};
|
|
206
231
|
}
|
|
207
232
|
|
|
208
|
-
function readNullableString(
|
|
233
|
+
function readNullableString(
|
|
234
|
+
payload: Record<string, unknown>,
|
|
235
|
+
key: string,
|
|
236
|
+
): string | null {
|
|
209
237
|
const value = payload[key];
|
|
210
238
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
211
239
|
}
|
|
212
240
|
|
|
213
|
-
function readNullableInteger(
|
|
241
|
+
function readNullableInteger(
|
|
242
|
+
payload: Record<string, unknown>,
|
|
243
|
+
key: string,
|
|
244
|
+
): number | null {
|
|
214
245
|
const value = payload[key];
|
|
215
246
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
216
247
|
return Math.floor(value);
|
|
@@ -222,7 +253,10 @@ function readNullableInteger(payload: Record<string, unknown>, key: string): num
|
|
|
222
253
|
return null;
|
|
223
254
|
}
|
|
224
255
|
|
|
225
|
-
function readNullableBoolean(
|
|
256
|
+
function readNullableBoolean(
|
|
257
|
+
payload: Record<string, unknown>,
|
|
258
|
+
key: string,
|
|
259
|
+
): boolean | null {
|
|
226
260
|
const value = payload[key];
|
|
227
261
|
if (typeof value === 'boolean') return value;
|
|
228
262
|
if (typeof value === 'string') {
|
|
@@ -240,7 +274,10 @@ function inferDenied(payload: Record<string, unknown>): boolean {
|
|
|
240
274
|
return false;
|
|
241
275
|
}
|
|
242
276
|
|
|
243
|
-
function buildEventUid(
|
|
277
|
+
function buildEventUid(
|
|
278
|
+
config: ResolvedIngestConfig,
|
|
279
|
+
row: StructuredAuditEntry,
|
|
280
|
+
): string {
|
|
244
281
|
const raw = [
|
|
245
282
|
config.botId,
|
|
246
283
|
config.agentId,
|
|
@@ -253,7 +290,10 @@ function buildEventUid(config: ResolvedIngestConfig, row: StructuredAuditEntry):
|
|
|
253
290
|
return createHash('sha256').update(raw).digest('hex');
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
function mapAuditRowToEvent(
|
|
293
|
+
function mapAuditRowToEvent(
|
|
294
|
+
config: ResolvedIngestConfig,
|
|
295
|
+
row: StructuredAuditEntry,
|
|
296
|
+
): Record<string, unknown> {
|
|
257
297
|
const payload = parsePayload(row.payload);
|
|
258
298
|
return {
|
|
259
299
|
session_id: row.session_id,
|
|
@@ -274,9 +314,18 @@ function mapAuditRowToEvent(config: ResolvedIngestConfig, row: StructuredAuditEn
|
|
|
274
314
|
prompt_tokens: readNullableInteger(payload, 'promptTokens'),
|
|
275
315
|
completion_tokens: readNullableInteger(payload, 'completionTokens'),
|
|
276
316
|
total_tokens: readNullableInteger(payload, 'totalTokens'),
|
|
277
|
-
estimated_prompt_tokens: readNullableInteger(
|
|
278
|
-
|
|
279
|
-
|
|
317
|
+
estimated_prompt_tokens: readNullableInteger(
|
|
318
|
+
payload,
|
|
319
|
+
'estimatedPromptTokens',
|
|
320
|
+
),
|
|
321
|
+
estimated_completion_tokens: readNullableInteger(
|
|
322
|
+
payload,
|
|
323
|
+
'estimatedCompletionTokens',
|
|
324
|
+
),
|
|
325
|
+
estimated_total_tokens: readNullableInteger(
|
|
326
|
+
payload,
|
|
327
|
+
'estimatedTotalTokens',
|
|
328
|
+
),
|
|
280
329
|
api_usage_available: readNullableBoolean(payload, 'apiUsageAvailable'),
|
|
281
330
|
api_prompt_tokens: readNullableInteger(payload, 'apiPromptTokens'),
|
|
282
331
|
api_completion_tokens: readNullableInteger(payload, 'apiCompletionTokens'),
|
|
@@ -302,7 +351,11 @@ function buildBatchPayloadText(
|
|
|
302
351
|
});
|
|
303
352
|
}
|
|
304
353
|
|
|
305
|
-
function prepareBatch(
|
|
354
|
+
function prepareBatch(
|
|
355
|
+
config: ResolvedIngestConfig,
|
|
356
|
+
rows: StructuredAuditEntry[],
|
|
357
|
+
currentCursor: number,
|
|
358
|
+
): PreparedBatch {
|
|
306
359
|
const selectedEvents: Record<string, unknown>[] = [];
|
|
307
360
|
const droppedEventIds: number[] = [];
|
|
308
361
|
let lastEventId = currentCursor;
|
|
@@ -366,7 +419,10 @@ function formatGrantError(prefix: string, grant: TokenGrantResult): string {
|
|
|
366
419
|
return `${prefix}: HTTP ${grant.statusCode}`;
|
|
367
420
|
}
|
|
368
421
|
|
|
369
|
-
async function requestIngestToken(
|
|
422
|
+
async function requestIngestToken(
|
|
423
|
+
config: ResolvedIngestConfig,
|
|
424
|
+
rotate = false,
|
|
425
|
+
): Promise<TokenGrantResult> {
|
|
370
426
|
let response: Response;
|
|
371
427
|
try {
|
|
372
428
|
response = await fetch(config.tokenAdminUrl, {
|
|
@@ -411,7 +467,10 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
|
|
|
411
467
|
rotated: rotate,
|
|
412
468
|
token: null,
|
|
413
469
|
message: parseMessage(payload.message),
|
|
414
|
-
errorText:
|
|
470
|
+
errorText:
|
|
471
|
+
parseMessage(payload.message) ||
|
|
472
|
+
rawText ||
|
|
473
|
+
`${response.status} ${response.statusText}`,
|
|
415
474
|
};
|
|
416
475
|
}
|
|
417
476
|
|
|
@@ -419,7 +478,7 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
|
|
|
419
478
|
const successFlagPresent = payload.success != null;
|
|
420
479
|
const success = successFlagPresent
|
|
421
480
|
? parseBoolean(payload.success)
|
|
422
|
-
:
|
|
481
|
+
: statusText === null || statusText === 'ok' || statusText === 'accepted';
|
|
423
482
|
const token = parseMessage(payload.token);
|
|
424
483
|
const created = parseBoolean(payload.created);
|
|
425
484
|
const rotated = parseBoolean(payload.rotated) || rotate;
|
|
@@ -444,7 +503,7 @@ async function requestIngestToken(config: ResolvedIngestConfig, rotate = false):
|
|
|
444
503
|
rotated,
|
|
445
504
|
token,
|
|
446
505
|
message,
|
|
447
|
-
errorText: token ? '' :
|
|
506
|
+
errorText: token ? '' : rawText || '',
|
|
448
507
|
};
|
|
449
508
|
}
|
|
450
509
|
|
|
@@ -457,14 +516,16 @@ async function resolveIngestToken(
|
|
|
457
516
|
ok: false,
|
|
458
517
|
token: null,
|
|
459
518
|
source: null,
|
|
460
|
-
reason:
|
|
519
|
+
reason:
|
|
520
|
+
'missing HYBRIDAI_API_KEY (needed to auto-fetch observability ingest token)',
|
|
461
521
|
};
|
|
462
522
|
}
|
|
463
523
|
|
|
464
524
|
const tokenKey = buildTokenCacheKey(config);
|
|
465
525
|
if (!forceRefresh) {
|
|
466
526
|
const cached = getObservabilityIngestToken(tokenKey);
|
|
467
|
-
if (cached)
|
|
527
|
+
if (cached)
|
|
528
|
+
return { ok: true, token: cached, source: 'cache', reason: null };
|
|
468
529
|
} else {
|
|
469
530
|
deleteObservabilityIngestToken(tokenKey);
|
|
470
531
|
const rotated = await requestIngestToken(config, true);
|
|
@@ -473,11 +534,17 @@ async function resolveIngestToken(
|
|
|
473
534
|
ok: false,
|
|
474
535
|
token: null,
|
|
475
536
|
source: null,
|
|
476
|
-
reason: formatGrantError(
|
|
537
|
+
reason: formatGrantError(
|
|
538
|
+
'failed to rotate observability ingest token',
|
|
539
|
+
rotated,
|
|
540
|
+
),
|
|
477
541
|
};
|
|
478
542
|
}
|
|
479
543
|
if (!rotated.token) {
|
|
480
|
-
const message =
|
|
544
|
+
const message =
|
|
545
|
+
rotated.message ||
|
|
546
|
+
rotated.errorText ||
|
|
547
|
+
'token rotate endpoint returned no token';
|
|
481
548
|
return {
|
|
482
549
|
ok: false,
|
|
483
550
|
token: null,
|
|
@@ -500,7 +567,10 @@ async function resolveIngestToken(
|
|
|
500
567
|
ok: false,
|
|
501
568
|
token: null,
|
|
502
569
|
source: null,
|
|
503
|
-
reason: formatGrantError(
|
|
570
|
+
reason: formatGrantError(
|
|
571
|
+
'failed to ensure observability ingest token',
|
|
572
|
+
granted,
|
|
573
|
+
),
|
|
504
574
|
};
|
|
505
575
|
}
|
|
506
576
|
if (!granted.token) {
|
|
@@ -510,11 +580,19 @@ async function resolveIngestToken(
|
|
|
510
580
|
ok: false,
|
|
511
581
|
token: null,
|
|
512
582
|
source: null,
|
|
513
|
-
reason: formatGrantError(
|
|
583
|
+
reason: formatGrantError(
|
|
584
|
+
'failed to rotate observability ingest token',
|
|
585
|
+
rotated,
|
|
586
|
+
),
|
|
514
587
|
};
|
|
515
588
|
}
|
|
516
589
|
if (!rotated.token) {
|
|
517
|
-
const message =
|
|
590
|
+
const message =
|
|
591
|
+
rotated.message ||
|
|
592
|
+
rotated.errorText ||
|
|
593
|
+
granted.message ||
|
|
594
|
+
granted.errorText ||
|
|
595
|
+
'token rotate endpoint returned no token';
|
|
518
596
|
return {
|
|
519
597
|
ok: false,
|
|
520
598
|
token: null,
|
|
@@ -540,7 +618,11 @@ async function resolveIngestToken(
|
|
|
540
618
|
};
|
|
541
619
|
}
|
|
542
620
|
|
|
543
|
-
async function postBatch(
|
|
621
|
+
async function postBatch(
|
|
622
|
+
config: ResolvedIngestConfig,
|
|
623
|
+
token: string,
|
|
624
|
+
payloadText: string,
|
|
625
|
+
): Promise<IngestResult> {
|
|
544
626
|
let response: Response;
|
|
545
627
|
try {
|
|
546
628
|
response = await fetch(config.ingestUrl, {
|
|
@@ -595,7 +677,12 @@ async function postBatch(config: ResolvedIngestConfig, token: string, payloadTex
|
|
|
595
677
|
}
|
|
596
678
|
|
|
597
679
|
function isPauseStatus(statusCode: number): boolean {
|
|
598
|
-
return
|
|
680
|
+
return (
|
|
681
|
+
statusCode === 400 ||
|
|
682
|
+
statusCode === 401 ||
|
|
683
|
+
statusCode === 403 ||
|
|
684
|
+
statusCode === 413
|
|
685
|
+
);
|
|
599
686
|
}
|
|
600
687
|
|
|
601
688
|
async function flushObservability(reason: string): Promise<void> {
|
|
@@ -618,7 +705,8 @@ async function flushObservability(reason: string): Promise<void> {
|
|
|
618
705
|
|
|
619
706
|
const initialToken = await resolveIngestToken(config);
|
|
620
707
|
if (!initialToken.ok || !initialToken.token) {
|
|
621
|
-
ingestState.reason =
|
|
708
|
+
ingestState.reason =
|
|
709
|
+
initialToken.reason || 'failed to resolve observability ingest token';
|
|
622
710
|
ingestState.lastFailureAt = new Date().toISOString();
|
|
623
711
|
ingestState.lastError = ingestState.reason;
|
|
624
712
|
logger.warn(
|
|
@@ -636,7 +724,11 @@ async function flushObservability(reason: string): Promise<void> {
|
|
|
636
724
|
ingestState.streamKey = streamKey;
|
|
637
725
|
let cursor = getObservabilityOffset(streamKey);
|
|
638
726
|
ingestState.lastCursor = cursor;
|
|
639
|
-
const fetchLimit = clampInteger(
|
|
727
|
+
const fetchLimit = clampInteger(
|
|
728
|
+
config.batchMaxEvents * FETCH_LIMIT_FACTOR,
|
|
729
|
+
config.batchMaxEvents,
|
|
730
|
+
5_000,
|
|
731
|
+
);
|
|
640
732
|
|
|
641
733
|
while (true) {
|
|
642
734
|
const rows = getStructuredAuditAfterId(cursor, fetchLimit);
|
|
@@ -663,7 +755,10 @@ async function flushObservability(reason: string): Promise<void> {
|
|
|
663
755
|
}
|
|
664
756
|
|
|
665
757
|
let result = await postBatch(config, activeToken, batch.payloadText);
|
|
666
|
-
if (
|
|
758
|
+
if (
|
|
759
|
+
!result.ok &&
|
|
760
|
+
(result.statusCode === 401 || result.statusCode === 403)
|
|
761
|
+
) {
|
|
667
762
|
const refreshed = await resolveIngestToken(config, true);
|
|
668
763
|
if (refreshed.ok && refreshed.token) {
|
|
669
764
|
activeToken = refreshed.token;
|
|
@@ -677,7 +772,8 @@ async function flushObservability(reason: string): Promise<void> {
|
|
|
677
772
|
);
|
|
678
773
|
result = await postBatch(config, activeToken, batch.payloadText);
|
|
679
774
|
} else {
|
|
680
|
-
const refreshReason =
|
|
775
|
+
const refreshReason =
|
|
776
|
+
refreshed.reason || 'unknown token refresh failure';
|
|
681
777
|
result = {
|
|
682
778
|
...result,
|
|
683
779
|
errorText: `${result.errorText} | token refresh failed: ${refreshReason}`,
|
|
@@ -754,7 +850,10 @@ export function startObservabilityIngest(): void {
|
|
|
754
850
|
if (!validation.ok) {
|
|
755
851
|
ingestState.reason = validation.reason;
|
|
756
852
|
if (config.enabled) {
|
|
757
|
-
logger.warn(
|
|
853
|
+
logger.warn(
|
|
854
|
+
{ reason: validation.reason },
|
|
855
|
+
'Observability ingest not started',
|
|
856
|
+
);
|
|
758
857
|
}
|
|
759
858
|
return;
|
|
760
859
|
}
|