@jini-ai/daemon 0.6.15-rc.1 → 0.6.15
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/agent-executor/codex/home-config.js +3 -2
- package/dist/agent-executor/codex/home-staging.js +2 -2
- package/dist/agent-executor/codex/isolation.js +6 -4
- package/dist/agent-executor/event-translation.js +2 -2
- package/dist/agent-executor/events.js +68 -49
- package/dist/agent-executor/launch.js +39 -50
- package/dist/agent-executor/live-session-state.js +6 -3
- package/dist/agent-executor/mcp-bridge.js +13 -5
- package/dist/agent-executor/message-relay.js +5 -6
- package/dist/agent-executor/run-output.js +7 -2
- package/dist/close-status.js +6 -2
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/read-only-tools.js +5 -1
- package/dist/core/tool-gaps.js +1 -1
- package/dist/delegated-result-correlation.js +2 -2
- package/dist/http/agents.d.ts +4 -0
- package/dist/http/attachments.js +13 -12
- package/dist/http/component-catalog.js +1 -1
- package/dist/http/connectors.js +7 -5
- package/dist/http/db-ops.js +5 -0
- package/dist/http/delegated-tools.js +5 -0
- package/dist/http/frontend-sessions.js +7 -5
- package/dist/http/host-tools.js +7 -5
- package/dist/http/memory.js +3 -1
- package/dist/http/message-relay.js +4 -1
- package/dist/http/model-proxy.js +13 -8
- package/dist/http/research-tavily.adapter.js +2 -1
- package/dist/http/routines.js +7 -5
- package/dist/http/terminals.js +5 -0
- package/dist/http/tool-catalog.js +1 -1
- package/dist/http/xai.js +10 -7
- package/dist/legacy-data-migration.d.ts +0 -12
- package/dist/legacy-data-migration.js +21 -13
- package/dist/remote-tool-executor.js +2 -1
- package/dist/routines/schedule.js +2 -1
- package/dist/routines/scheduler.js +6 -7
- package/dist/run-lifecycle.js +2 -1
- package/dist/store/agent-sessions/sql.js +7 -3
- package/dist/store/event-log/sqlite-kernel.js +5 -1
- package/dist/store/event-log/sqlite.js +6 -2
- package/dist/surface-exchanges.js +3 -3
- package/dist/terminal-session.js +4 -5
- package/dist/tool-executor.js +47 -3
- package/dist/tool-recovery/recovery.js +5 -3
- package/dist/tool-recovery/redaction.js +1 -2
- package/dist/tool-recovery/stack.js +5 -1
- package/dist/tool-result-media.js +7 -1
- package/dist/tool-result-surfaces.js +10 -9
- package/package.json +20 -15
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { redactSecrets } from '@jini-ai/core';
|
|
2
2
|
// Only a terminal-prefixed failure overrides real output; ordinary assistant explanations
|
|
3
3
|
// can mention empty output or headless permissions without being failed runs themselves.
|
|
4
|
-
|
|
4
|
+
// One pattern per diagnostic; a line is a diagnostic when any of them matches.
|
|
5
|
+
const DIAGNOSTIC_LINES = [
|
|
6
|
+
/^(?:jetski|agy|error|fatal):\s*no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)/i,
|
|
7
|
+
/^(?:jetski|agy|error|fatal):\s*(?:error:\s*)?permission denied\b.*headless/i,
|
|
8
|
+
/^(?:jetski|agy|error|fatal):\s*headless\b.*auto-denied/i,
|
|
9
|
+
];
|
|
5
10
|
/** Records whether the event is assistant text, tool activity or a run error. */
|
|
6
11
|
function noteActivity(seen, event) {
|
|
7
12
|
if (event.kind === 'error')
|
|
@@ -23,7 +28,7 @@ function noteDiagnostic({ seen, tails, event, plainStdout }) {
|
|
|
23
28
|
return;
|
|
24
29
|
const channel = event.kind === 'stderr' ? 'stderr' : 'stdout';
|
|
25
30
|
tails[channel] = (tails[channel] + event.payload.chunk).slice(-8192);
|
|
26
|
-
const line = tails[channel].split(/\r?\n/).find(value =>
|
|
31
|
+
const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINES.some(pattern => pattern.test(value.trim())));
|
|
27
32
|
if (line)
|
|
28
33
|
seen.diagnostic = line;
|
|
29
34
|
}
|
package/dist/close-status.js
CHANGED
|
@@ -30,6 +30,11 @@ export function classifyRunCloseStatus(requiredArgs, optionalArgs = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
return 'failed';
|
|
32
32
|
}
|
|
33
|
+
/** A set environment value parsed as a positive, finite millisecond count, else undefined. */
|
|
34
|
+
function positiveTimeout(raw) {
|
|
35
|
+
const value = raw !== undefined ? Number(raw) : undefined;
|
|
36
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Resolves a timeout in milliseconds from an env-var override, an
|
|
35
40
|
* agent-supplied default, and a kernel default, in that precedence order,
|
|
@@ -48,8 +53,7 @@ export function resolveTimeoutMs(requiredArgs, optionalArgs = {}) {
|
|
|
48
53
|
const input = { ...requiredArgs, ...optionalArgs };
|
|
49
54
|
const env = input.env ?? (typeof process !== 'undefined' ? process.env : {});
|
|
50
55
|
const rawFromEnv = input.envVar ? env[input.envVar] : undefined;
|
|
51
|
-
const
|
|
52
|
-
const candidate = fromEnv !== undefined && Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (input.agentDefaultMs ?? input.defaultMs);
|
|
56
|
+
const candidate = positiveTimeout(rawFromEnv) ?? input.agentDefaultMs ?? input.defaultMs;
|
|
53
57
|
return Math.min(candidate, input.maxMs);
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
|
|
3
|
+
* without binding a throwaway name for every dropped key.
|
|
4
|
+
*/
|
|
5
|
+
/** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
|
|
6
|
+
export declare function omitKeys<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Omit<T, K>;
|
|
7
|
+
//# sourceMappingURL=omit-keys.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
|
|
3
|
+
* without binding a throwaway name for every dropped key.
|
|
4
|
+
*/
|
|
5
|
+
/** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
|
|
6
|
+
export function omitKeys(source, keys) {
|
|
7
|
+
const copy = { ...source };
|
|
8
|
+
for (const key of keys)
|
|
9
|
+
delete copy[key];
|
|
10
|
+
return copy;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=omit-keys.js.map
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { isReadOnlyTool, toolDiscoveryMetadata } from '@jini-ai/core';
|
|
2
2
|
import { toolCandidates } from './tool-candidates.js';
|
|
3
|
+
/** ` — did you mean a, b?` for any close tool ids, else nothing. */
|
|
4
|
+
function didYouMean(candidates) {
|
|
5
|
+
return candidates.length > 0 ? ` — did you mean ${candidates.join(', ')}?` : '';
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
|
|
5
9
|
* refuse unsafe dispatch; unknown ids direct discovery because an unrestricted retry cannot run them.
|
|
@@ -11,7 +15,7 @@ export const defaultDaemonMessages = {
|
|
|
11
15
|
toolRefusalMessage: ({ toolId }) => `tool "${toolId}" is not registered as read-only — this gateway executes only tools whose registration declares readOnly; call it through execute_delegated_tool instead`,
|
|
12
16
|
unknownToolMessage: ({ toolId, candidates }) => toolId === 'check_messages'
|
|
13
17
|
? 'check_messages is an MCP-local tool; call check_messages directly, never as toolId in a delegated gateway'
|
|
14
|
-
: `no tool "${toolId}" is registered; use search_tools${candidates
|
|
18
|
+
: `no tool "${toolId}" is registered; use search_tools${didYouMean(candidates)}`,
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
/** @returns A copied, attenuated principal; its roles are unchanged. */
|
package/dist/core/tool-gaps.js
CHANGED
|
@@ -189,7 +189,7 @@ export function createToolGapCollector(required, optional = {}) {
|
|
|
189
189
|
const payload = event.payload;
|
|
190
190
|
if (payload.type === 'text_delta')
|
|
191
191
|
readDelta(payload.delta);
|
|
192
|
-
|
|
192
|
+
if (payload.type === 'tool_use')
|
|
193
193
|
recordToolUse(payload, event.ts);
|
|
194
194
|
},
|
|
195
195
|
/** Await outstanding audit writes during graceful shutdown/tests. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { omitKeys } from './core/omit-keys.js';
|
|
1
2
|
/** Accept only the complete base64 source shape; extra fields must remain part of the digest. */
|
|
2
3
|
function base64ImageSource({ source }, _options = {}) {
|
|
3
4
|
if (typeof source !== 'object' || source === null)
|
|
@@ -14,8 +15,7 @@ function normalizeSourceImage({ record }, _options = {}) {
|
|
|
14
15
|
const image = base64ImageSource({ source: record.source });
|
|
15
16
|
if (!image)
|
|
16
17
|
return record;
|
|
17
|
-
|
|
18
|
-
return { ...rest, data: image.data, mimeType: image.media_type };
|
|
18
|
+
return { ...omitKeys(record, ['source']), data: image.data, mimeType: image.media_type };
|
|
19
19
|
}
|
|
20
20
|
/** Normalize unambiguous CLI images and JSON object key order.
|
|
21
21
|
* @complexity O(n + k log k) for n bytes and k keys per object; inputs are wire JSON. */
|
package/dist/http/agents.d.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface AgentSummary {
|
|
|
51
51
|
* as `true` (no known reason to warn) rather than assuming the worst.
|
|
52
52
|
*/
|
|
53
53
|
readonly supportsTools?: boolean;
|
|
54
|
+
/** Project the runtime definition so picker preflight agrees with launch enforcement. */
|
|
55
|
+
readonly supportsNativeToolAccess?: boolean;
|
|
56
|
+
readonly fullCliAccessOnly?: boolean;
|
|
57
|
+
readonly fullCliAccessNote?: string;
|
|
54
58
|
}
|
|
55
59
|
export interface AgentsHttpDeps {
|
|
56
60
|
/** Returns the host's cached or freshly resolved client-safe agent inventory. */
|
package/dist/http/attachments.js
CHANGED
|
@@ -284,13 +284,12 @@ const SIGNATURE_BYTES = FTYP_COMPATIBLE_BRANDS_OFFSET + MAX_SCANNED_COMPATIBLE_B
|
|
|
284
284
|
* production is a check nobody knows works — a fake identity states each case directly.
|
|
285
285
|
*/
|
|
286
286
|
export function isUnchangedAttachment({ recorded, observed }, _optional = {}) {
|
|
287
|
-
|
|
288
|
-
&& observed.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
&& observed.size === recorded.size
|
|
292
|
-
&& (recorded.mtimeMs === undefined || observed.mtimeMs === recorded.mtimeMs)
|
|
287
|
+
const sameFile = observed.isRegularFile && observed.canonicalPath === recorded.filePath
|
|
288
|
+
&& observed.dev === recorded.dev && observed.ino === recorded.ino;
|
|
289
|
+
// A recorded timestamp must still hold; one never recorded cannot disagree.
|
|
290
|
+
const sameTimes = (recorded.mtimeMs === undefined || observed.mtimeMs === recorded.mtimeMs)
|
|
293
291
|
&& (recorded.ctimeMs === undefined || observed.ctimeMs === recorded.ctimeMs);
|
|
292
|
+
return sameFile && observed.size === recorded.size && sameTimes;
|
|
294
293
|
}
|
|
295
294
|
/**
|
|
296
295
|
* Phase 1 of `claim()`: synchronously reserves `attachments` for `runId`, or throws (releasing
|
|
@@ -384,13 +383,15 @@ export function sanitizeAttachmentName({ requestedName }, _optional = {}) {
|
|
|
384
383
|
return 'attachment';
|
|
385
384
|
return basename(requestedName).replaceAll(/[^a-zA-Z0-9._ -]/gu, '_') || 'attachment';
|
|
386
385
|
}
|
|
386
|
+
/** An attachment no run has claimed, or one claimed by `runId` (never by an absent run id). */
|
|
387
|
+
function isUnclaimedOrClaimedBy(claimedRunId, runId) {
|
|
388
|
+
return claimedRunId === undefined || (runId !== undefined && claimedRunId === runId);
|
|
389
|
+
}
|
|
390
|
+
/** The complete PNG signature includes the CR/LF and control-byte corruption markers. */
|
|
391
|
+
const PNG_SIGNATURE_PREFIX = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
387
392
|
/** `true` when `body`'s first 8 bytes are the PNG signature. */
|
|
388
393
|
export function hasPngSignature({ body }, _optional = {}) {
|
|
389
|
-
return body.length >= 8
|
|
390
|
-
&& body[0] === 0x89
|
|
391
|
-
&& body[1] === 0x50
|
|
392
|
-
&& body[2] === 0x4e
|
|
393
|
-
&& body[3] === 0x47;
|
|
394
|
+
return body.length >= 8 && PNG_SIGNATURE_PREFIX.every((byte, index) => body[index] === byte);
|
|
394
395
|
}
|
|
395
396
|
/** `true` when `body`'s first 3 bytes are the JPEG start-of-image marker. */
|
|
396
397
|
export function hasJpegSignature({ body }, _optional = {}) {
|
|
@@ -934,7 +935,7 @@ export async function createDiskAttachmentStore(requiredArgs, optionalArgs = {})
|
|
|
934
935
|
// what makes an ownerless record excluded rather than accidentally matched by a falsy-ish
|
|
935
936
|
// `ownerId` argument — see this method's own doc on why that must never be a wildcard.
|
|
936
937
|
.filter((record) => record.ownerId !== undefined && record.ownerId === ownerId
|
|
937
|
-
&& (record.claimedRunId
|
|
938
|
+
&& isUnclaimedOrClaimedBy(record.claimedRunId, runId))
|
|
938
939
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
939
940
|
.map((record) => ({ ref: record.id, name: record.name, kind: record.kind, size: record.size, createdAt: record.createdAt }));
|
|
940
941
|
},
|
|
@@ -12,7 +12,7 @@ function parseSearchInput(input) {
|
|
|
12
12
|
const rawLimit = input.query.limit;
|
|
13
13
|
if (rawLimit === undefined)
|
|
14
14
|
return ok({ value: { query: rawQuery, limit: DEFAULT_SEARCH_LIMIT } });
|
|
15
|
-
if (typeof rawLimit !== 'string' ||
|
|
15
|
+
if (typeof rawLimit !== 'string' || !/^\d+$/.test(rawLimit)) {
|
|
16
16
|
return err({ error: validationError({ message: 'limit must be a single positive-integer query-string value when provided' }) });
|
|
17
17
|
}
|
|
18
18
|
const parsed = Number(rawLimit);
|
package/dist/http/connectors.js
CHANGED
|
@@ -49,6 +49,8 @@ import { createApiError } from '@jini-ai/protocol';
|
|
|
49
49
|
import { ClientFacingError, defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
50
50
|
import { validationError } from '@jini-ai/http-kit';
|
|
51
51
|
import { err, ok } from '@jini-ai/http-kit';
|
|
52
|
+
/** Validation message for a request body that is not a JSON object. */
|
|
53
|
+
const BODY_NOT_OBJECT_MESSAGE = 'body must be a JSON object';
|
|
52
54
|
function defaultInternalErrorSink(context) {
|
|
53
55
|
// eslint-disable-next-line no-console
|
|
54
56
|
console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, context.error);
|
|
@@ -86,7 +88,7 @@ function firstQueryValue(raw) {
|
|
|
86
88
|
// ---------------------------------------------------------------------------
|
|
87
89
|
function parseAuthCredentials(input) {
|
|
88
90
|
if (!isRecord(input.body))
|
|
89
|
-
return err({ error: validationError({ message:
|
|
91
|
+
return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
|
|
90
92
|
const email = nonEmptyString(input.body.email);
|
|
91
93
|
if (!email)
|
|
92
94
|
return err({ error: validationError({ message: 'email must be a non-empty string' }, { issues: [{ path: 'email', message: 'required non-empty string' }] }) });
|
|
@@ -97,7 +99,7 @@ function parseAuthCredentials(input) {
|
|
|
97
99
|
}
|
|
98
100
|
function parseAuthToken(input) {
|
|
99
101
|
if (!isRecord(input.body))
|
|
100
|
-
return err({ error: validationError({ message:
|
|
102
|
+
return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
|
|
101
103
|
const token = nonEmptyString(input.body.token);
|
|
102
104
|
if (!token)
|
|
103
105
|
return err({ error: validationError({ message: 'token must be a non-empty string' }, { issues: [{ path: 'token', message: 'required non-empty string' }] }) });
|
|
@@ -164,7 +166,7 @@ function parseStoragePut(input) {
|
|
|
164
166
|
if (!keyResult.ok)
|
|
165
167
|
return keyResult;
|
|
166
168
|
if (!isRecord(input.body))
|
|
167
|
-
return err({ error: validationError({ message:
|
|
169
|
+
return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
|
|
168
170
|
const dataBase64 = input.body.dataBase64;
|
|
169
171
|
if (typeof dataBase64 !== 'string' || !isWellFormedBase64(dataBase64)) {
|
|
170
172
|
return err({ error: validationError({ message: 'dataBase64 must be a non-empty base64-encoded string' }, { issues: [{ path: 'dataBase64', message: 'required base64 string' }] }) });
|
|
@@ -231,7 +233,7 @@ function parseDbInsert(input) {
|
|
|
231
233
|
if (!collectionResult.ok)
|
|
232
234
|
return collectionResult;
|
|
233
235
|
if (!isRecord(input.body))
|
|
234
|
-
return err({ error: validationError({ message:
|
|
236
|
+
return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
|
|
235
237
|
const id = input.body.id;
|
|
236
238
|
if (typeof id !== 'string' || id.length === 0) {
|
|
237
239
|
return err({ error: validationError({ message: 'id must be a non-empty string field' }, { issues: [{ path: 'id', message: 'required non-empty string' }] }) });
|
|
@@ -252,7 +254,7 @@ function parseDbUpdate(input) {
|
|
|
252
254
|
if (!parsed.ok)
|
|
253
255
|
return parsed;
|
|
254
256
|
if (!isRecord(input.body))
|
|
255
|
-
return err({ error: validationError({ message:
|
|
257
|
+
return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
|
|
256
258
|
return ok({ value: { ...parsed.value, patch: input.body } });
|
|
257
259
|
}
|
|
258
260
|
function parseDbQueryRequest(input) {
|
package/dist/http/db-ops.js
CHANGED
|
@@ -138,6 +138,11 @@ function toolResultToApiResult(deps, source, result) {
|
|
|
138
138
|
sink({ source, correlationId, error: result.status === 'failed' ? result.error : result.status });
|
|
139
139
|
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
140
140
|
}
|
|
141
|
+
default: {
|
|
142
|
+
// Unreachable: every tool result status has a case above, and a new one fails typecheck here.
|
|
143
|
+
const unhandled = result.status;
|
|
144
|
+
throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
|
|
145
|
+
}
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
/** `GET /api/daemon/db` — the database inventory (schema version, tables, row counts, size). */
|
|
@@ -144,6 +144,11 @@ function toolExecutionResultToApiResult(deps, { runId, toolId, result }) {
|
|
|
144
144
|
return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error: result.status, runId, toolId, status: result.status }) });
|
|
145
145
|
case 'failed':
|
|
146
146
|
return err({ error: failedResultToApiError(deps, { runId, toolId, result }) });
|
|
147
|
+
default: {
|
|
148
|
+
// Unreachable: every tool result status has a case above, and a new one fails typecheck here.
|
|
149
|
+
const unhandled = result.status;
|
|
150
|
+
throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
|
|
151
|
+
}
|
|
147
152
|
}
|
|
148
153
|
}
|
|
149
154
|
/** The `failed` arm of {@link toolExecutionResultToApiResult} — see its doc for the three-way split. */
|
|
@@ -59,7 +59,8 @@ function reportInternalError(deps, source, error) {
|
|
|
59
59
|
* from a bracketed query string) is rejected rather than coerced.
|
|
60
60
|
*/
|
|
61
61
|
export function parseCapabilityQuery({ raw }, _optional = {}) {
|
|
62
|
-
const
|
|
62
|
+
const listed = Array.isArray(raw) ? raw : [raw];
|
|
63
|
+
const values = raw === undefined ? [] : listed;
|
|
63
64
|
const capabilities = [];
|
|
64
65
|
for (const value of values) {
|
|
65
66
|
if (typeof value !== 'string' || value.trim().length === 0)
|
|
@@ -167,11 +168,12 @@ export function registerFrontendSessionRoutes({ app, deps, adapter }, _optional
|
|
|
167
168
|
}
|
|
168
169
|
catch (error) {
|
|
169
170
|
reportInternalError(deps, 'stream', error);
|
|
170
|
-
if (
|
|
171
|
-
res.
|
|
171
|
+
if (res.headersSent) {
|
|
172
|
+
if (!res.writableEnded)
|
|
173
|
+
res.end();
|
|
172
174
|
}
|
|
173
|
-
else
|
|
174
|
-
res.
|
|
175
|
+
else {
|
|
176
|
+
res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
});
|
package/dist/http/host-tools.js
CHANGED
|
@@ -71,6 +71,12 @@ export function applicableForPlatform({ entry, platform }, _optional = {}) {
|
|
|
71
71
|
export function defaultProbeEnv(_required, _optional = {}) {
|
|
72
72
|
return { access: ({ path, mode }) => fsAccess(path, mode), env: process.env, platform: currentPlatform({}) };
|
|
73
73
|
}
|
|
74
|
+
/** The login-shell bin directories a GUI-launched daemon's `PATH` tends to lack, per platform. */
|
|
75
|
+
function extraProbeDirs(platform, home) {
|
|
76
|
+
if (platform === 'darwin')
|
|
77
|
+
return ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`];
|
|
78
|
+
return platform === 'linux' ? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`] : [];
|
|
79
|
+
}
|
|
74
80
|
/**
|
|
75
81
|
* `$PATH` directories to probe, plus the common macOS/Linux locations a login shell would have
|
|
76
82
|
* that a GUI-launched daemon's thin inherited `PATH` often lacks (no `/usr/local/bin`, no
|
|
@@ -81,11 +87,7 @@ export function pathDirs(probeEnv) {
|
|
|
81
87
|
const raw = probeEnv.env.PATH ?? '';
|
|
82
88
|
const sep = probeEnv.platform === 'win32' ? ';' : ':';
|
|
83
89
|
const home = probeEnv.env.HOME ?? '';
|
|
84
|
-
const extras = probeEnv.platform
|
|
85
|
-
? ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`]
|
|
86
|
-
: probeEnv.platform === 'linux'
|
|
87
|
-
? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`]
|
|
88
|
-
: [];
|
|
90
|
+
const extras = extraProbeDirs(probeEnv.platform, home);
|
|
89
91
|
return [...raw.split(sep), ...extras].filter((dir) => dir.length > 0);
|
|
90
92
|
}
|
|
91
93
|
export async function probeCommandOnPath({ command, probeEnv }, _optional = {}) {
|
package/dist/http/memory.js
CHANGED
|
@@ -290,7 +290,9 @@ export function registerMemoryEventStream({ app, deps, adapter }, _optional = {}
|
|
|
290
290
|
let seq = 0;
|
|
291
291
|
const channel = createSseChannel({ res });
|
|
292
292
|
const emit = (kind, data) => {
|
|
293
|
-
|
|
293
|
+
const cursor = seq;
|
|
294
|
+
seq += 1;
|
|
295
|
+
channel.enqueue({ event: { opaqueCursor: String(cursor), kind, data } });
|
|
294
296
|
};
|
|
295
297
|
const onChange = (event) => emit('change', event);
|
|
296
298
|
const onExtraction = (event) => emit('extraction', event);
|
|
@@ -8,7 +8,7 @@ export function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers
|
|
|
8
8
|
return (req, res, next) => {
|
|
9
9
|
try {
|
|
10
10
|
const token = bearerTokenFromHeader({ header: req.get('authorization') });
|
|
11
|
-
const caller =
|
|
11
|
+
const caller = resolveRelayCaller({ token, runScopedCallers });
|
|
12
12
|
if (runScopedCallers && !caller) {
|
|
13
13
|
res.status(401).json({ error: { code: 'UNAUTHENTICATED', message: 'a live run credential is required' } });
|
|
14
14
|
return;
|
|
@@ -30,6 +30,9 @@ export function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
function resolveRelayCaller({ token, runScopedCallers }) {
|
|
34
|
+
return token ? runScopedCallers?.resolveCaller({ token }) : undefined;
|
|
35
|
+
}
|
|
33
36
|
/** Strict receipt-channel validation; no caller-selected run or principal is accepted. */
|
|
34
37
|
function validClaim(body) {
|
|
35
38
|
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
package/dist/http/model-proxy.js
CHANGED
|
@@ -218,12 +218,12 @@ function toStreamEvent(seq, event) {
|
|
|
218
218
|
}
|
|
219
219
|
/** Answers a failure that happened before any SSE stream existed: a redacted JSON 500 if nothing was sent yet, otherwise just end the response (matching `mountJsonRoute`'s own SEC-005 catch). */
|
|
220
220
|
function respondInternalError(res, correlationId) {
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
res.end();
|
|
221
|
+
if (res.headersSent) {
|
|
222
|
+
if (!res.writableEnded)
|
|
223
|
+
res.end();
|
|
224
|
+
return;
|
|
226
225
|
}
|
|
226
|
+
sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
229
|
* The actual request handler body, shared by every fixed-path route and the `:provider`
|
|
@@ -238,6 +238,11 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
238
238
|
// reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
|
|
239
239
|
// before either exists yet.
|
|
240
240
|
let seq = 0;
|
|
241
|
+
const nextSeq = () => {
|
|
242
|
+
const current = seq;
|
|
243
|
+
seq += 1;
|
|
244
|
+
return current;
|
|
245
|
+
};
|
|
241
246
|
let channel;
|
|
242
247
|
try {
|
|
243
248
|
const origin = guardSameOrigin({ req, origin: adapter });
|
|
@@ -253,7 +258,7 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
253
258
|
const openChannel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
|
|
254
259
|
channel = openChannel;
|
|
255
260
|
openChannel.open();
|
|
256
|
-
await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(
|
|
261
|
+
await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(nextSeq(), event) }));
|
|
257
262
|
// No `channel.end()` call here: every reachable exit path inside every turn-runner emits
|
|
258
263
|
// exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
|
|
259
264
|
// see each provider module's own `turn-end-guard.ts` usage, and each function's own
|
|
@@ -283,8 +288,8 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
|
|
|
283
288
|
// here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
|
|
284
289
|
// no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
|
|
285
290
|
// event's own `isEndEvent` match closes the channel via the same path as a real one.
|
|
286
|
-
channel.enqueue({ event: toStreamEvent(
|
|
287
|
-
channel.enqueue({ event: toStreamEvent(
|
|
291
|
+
channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'error', message: 'an internal error occurred', code: correlationId }) });
|
|
292
|
+
channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'end', reason: 'error' }) });
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
295
|
/** Erases one provider's concrete parse/run types into the registry's shared entry shape — the single cast every entry needs. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Tavily default adapter. Request limits, timeout and secret-redaction policy remain here. */
|
|
2
2
|
import { validateBaseUrl } from '@jini-ai/agent-runtime';
|
|
3
3
|
import { redactSecrets } from '@jini-ai/core';
|
|
4
|
+
import { trimCharsEnd } from '@jini-ai/core/text';
|
|
4
5
|
const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
|
|
5
6
|
/** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
|
|
6
7
|
const TAVILY_MAX_RESULTS_LIMIT = 20;
|
|
@@ -20,7 +21,7 @@ function resolveTavilyBase(credentials) {
|
|
|
20
21
|
if (baseUrlCheck.error) {
|
|
21
22
|
throw new Error(baseUrlCheck.error);
|
|
22
23
|
}
|
|
23
|
-
return
|
|
24
|
+
return trimCharsEnd({ text: baseUrl, chars: '/' });
|
|
24
25
|
}
|
|
25
26
|
function trimmedOrUndefined(value) {
|
|
26
27
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
package/dist/http/routines.js
CHANGED
|
@@ -3,6 +3,8 @@ import { createApiError } from '@jini-ai/protocol';
|
|
|
3
3
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
4
4
|
import { validationError } from '@jini-ai/http-kit';
|
|
5
5
|
import { err, ok } from '@jini-ai/http-kit';
|
|
6
|
+
/** The 404 message for an unknown routine id. */
|
|
7
|
+
const ROUTINE_NOT_FOUND_MESSAGE = 'routine not found';
|
|
6
8
|
function isRecord(value) {
|
|
7
9
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
10
|
}
|
|
@@ -192,7 +194,7 @@ export const routineGetRoute = defineJsonRoute({
|
|
|
192
194
|
method: 'get', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
193
195
|
const routine = await deps.store.get({ id });
|
|
194
196
|
return routine === null
|
|
195
|
-
? err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
197
|
+
? err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) })
|
|
196
198
|
: ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
|
|
197
199
|
}
|
|
198
200
|
});
|
|
@@ -205,7 +207,7 @@ export const routineUpdateRoute = defineJsonRoute({
|
|
|
205
207
|
}
|
|
206
208
|
const routine = await deps.store.update({ id, patch });
|
|
207
209
|
if (routine === null)
|
|
208
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
210
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
209
211
|
deps.scheduler.rescheduleOne({ routineId: id });
|
|
210
212
|
return ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
|
|
211
213
|
}
|
|
@@ -214,14 +216,14 @@ export const routineDeleteRoute = defineJsonRoute({
|
|
|
214
216
|
method: 'delete', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
215
217
|
deps.scheduler.unschedule({ routineId: id });
|
|
216
218
|
const removed = await deps.store.delete({ id });
|
|
217
|
-
return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
219
|
+
return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
218
220
|
}
|
|
219
221
|
}, { requireSameOrigin: true });
|
|
220
222
|
export const routineRunNowRoute = defineJsonRoute({
|
|
221
223
|
method: 'post', path: '/api/routines/:id/run', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
|
|
222
224
|
const existing = await deps.store.get({ id });
|
|
223
225
|
if (existing === null)
|
|
224
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
226
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
225
227
|
const started = await deps.scheduler.runNow({ routineId: id });
|
|
226
228
|
const [routine, run] = await Promise.all([deps.store.get({ id }), deps.store.getLatestRun({ routineId: id })]);
|
|
227
229
|
return ok({
|
|
@@ -239,7 +241,7 @@ export const routineRunsListRoute = defineJsonRoute({
|
|
|
239
241
|
method: 'get', path: '/api/routines/:id/runs', parse: parseRoutineRunsQuery, handle: async ({ input: { id, limit }, deps }) => {
|
|
240
242
|
const existing = await deps.store.get({ id });
|
|
241
243
|
if (existing === null)
|
|
242
|
-
return err({ error: createApiError({ code: 'NOT_FOUND', message:
|
|
244
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
|
|
243
245
|
const runs = await deps.store.listRuns({ routineId: id, limit });
|
|
244
246
|
return ok({ value: { runs } });
|
|
245
247
|
}
|
package/dist/http/terminals.js
CHANGED
|
@@ -60,6 +60,11 @@ function toolResultToApiResult(deps, result) {
|
|
|
60
60
|
sink({ source: 'terminal-create', correlationId, error: result.status === 'failed' ? result.error : result.status });
|
|
61
61
|
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
62
62
|
}
|
|
63
|
+
default: {
|
|
64
|
+
// Unreachable: every tool result status has a case above, and a new one fails typecheck here.
|
|
65
|
+
const unhandled = result.status;
|
|
66
|
+
throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
function parseTerminalCreate(input) {
|
|
@@ -12,7 +12,7 @@ function parseSearchInput(input) {
|
|
|
12
12
|
const rawLimit = input.query.limit;
|
|
13
13
|
if (rawLimit === undefined)
|
|
14
14
|
return ok({ value: { query: rawQuery, limit: DEFAULT_SEARCH_LIMIT } });
|
|
15
|
-
if (typeof rawLimit !== 'string' ||
|
|
15
|
+
if (typeof rawLimit !== 'string' || !/^\d+$/.test(rawLimit)) {
|
|
16
16
|
return err({ error: validationError({ message: 'limit must be a single positive-integer query-string value when provided' }) });
|
|
17
17
|
}
|
|
18
18
|
const parsed = Number(rawLimit);
|
package/dist/http/xai.js
CHANGED
|
@@ -123,6 +123,9 @@ import { createApiError } from '@jini-ai/protocol';
|
|
|
123
123
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
124
124
|
import { validationError } from '@jini-ai/http-kit';
|
|
125
125
|
import { err, ok } from '@jini-ai/http-kit';
|
|
126
|
+
import { trimCharsEnd } from '@jini-ai/core/text';
|
|
127
|
+
/** The redacted message every internal failure answers with; details go to the error sink. */
|
|
128
|
+
const INTERNAL_ERROR_MESSAGE = 'an internal error occurred';
|
|
126
129
|
const DEFAULT_TOKEN_FILE_NAME = 'xai-oauth-token.json';
|
|
127
130
|
/** Matches the loopback listener's own 30-minute self-close timeout (OD's real comment: keeps the
|
|
128
131
|
* PKCE state, the open socket, and any paste-back UI expiring together). */
|
|
@@ -361,7 +364,7 @@ export const xaiOauthStartRoute = defineJsonRoute({
|
|
|
361
364
|
await stopListener(resolved);
|
|
362
365
|
const correlationId = randomUUID();
|
|
363
366
|
resolved.onInternalError({ source: 'oauth-start', correlationId, error });
|
|
364
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
367
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
365
368
|
}
|
|
366
369
|
});
|
|
367
370
|
}
|
|
@@ -410,7 +413,7 @@ export const xaiOauthCompleteRoute = defineJsonRoute({
|
|
|
410
413
|
}
|
|
411
414
|
const correlationId = randomUUID();
|
|
412
415
|
resolved.onInternalError({ source: 'oauth-complete', correlationId, error: redactError(error) });
|
|
413
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
416
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
}, { requireSameOrigin: true });
|
|
@@ -448,7 +451,7 @@ export const xaiAuthStatusRoute = defineJsonRoute({
|
|
|
448
451
|
catch (error) {
|
|
449
452
|
const correlationId = randomUUID();
|
|
450
453
|
resolved.onInternalError({ source: 'auth-status', correlationId, error });
|
|
451
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
454
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
452
455
|
}
|
|
453
456
|
}
|
|
454
457
|
}, { requireSameOrigin: true });
|
|
@@ -466,7 +469,7 @@ export const xaiOauthDisconnectRoute = defineJsonRoute({
|
|
|
466
469
|
catch (error) {
|
|
467
470
|
const correlationId = randomUUID();
|
|
468
471
|
resolved.onInternalError({ source: 'oauth-disconnect', correlationId, error });
|
|
469
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
472
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
470
473
|
}
|
|
471
474
|
}
|
|
472
475
|
}, { requireSameOrigin: true });
|
|
@@ -594,7 +597,7 @@ async function callXaiSearch(resolved, accessToken, request) {
|
|
|
594
597
|
const baseUrlCheck = validateBaseUrl({ baseUrl: resolved.searchBaseUrl });
|
|
595
598
|
if (baseUrlCheck.error)
|
|
596
599
|
throw new Error(baseUrlCheck.error);
|
|
597
|
-
const base = resolved.searchBaseUrl
|
|
600
|
+
const base = trimCharsEnd({ text: resolved.searchBaseUrl, chars: '/' });
|
|
598
601
|
const model = request.model ?? resolved.searchDefaultModel;
|
|
599
602
|
const requestBody = {
|
|
600
603
|
model,
|
|
@@ -633,7 +636,7 @@ export const xaiSearchRoute = defineJsonRoute({
|
|
|
633
636
|
catch (error) {
|
|
634
637
|
const correlationId = randomUUID();
|
|
635
638
|
resolved.onInternalError({ source: 'search', correlationId, error });
|
|
636
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
639
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
637
640
|
}
|
|
638
641
|
if (!credential) {
|
|
639
642
|
// No entitlement gate here — see module doc's "Dropped: OD's SuperGrok subscription gate"
|
|
@@ -649,7 +652,7 @@ export const xaiSearchRoute = defineJsonRoute({
|
|
|
649
652
|
catch (error) {
|
|
650
653
|
const correlationId = randomUUID();
|
|
651
654
|
resolved.onInternalError({ source: 'search', correlationId, error });
|
|
652
|
-
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message:
|
|
655
|
+
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
|
|
653
656
|
}
|
|
654
657
|
}
|
|
655
658
|
}, { requireSameOrigin: true });
|
|
@@ -122,18 +122,6 @@ export declare function dataDirHasExistingPayload({ dataDir, config }: {
|
|
|
122
122
|
}, { filesystem: fs }?: {
|
|
123
123
|
filesystem?: LegacyFilesystemPort;
|
|
124
124
|
}): string[];
|
|
125
|
-
/**
|
|
126
|
-
* Move staged payload entries into the final `dataDir`. Each entry is moved
|
|
127
|
-
* with `renameSync` (atomic on the same filesystem); a cross-device rename
|
|
128
|
-
* falls back to copy + remove. Rollback-safe in two layers: the copy
|
|
129
|
-
* fallback removes a partial destination on its own failure, and the outer
|
|
130
|
-
* loop removes every entry it already promoted before rethrowing.
|
|
131
|
-
*
|
|
132
|
-
* @param stagingDir - The sibling staging directory holding the copied payload.
|
|
133
|
-
* @param dataDir - The destination data root.
|
|
134
|
-
* @param entries - The payload entries to promote.
|
|
135
|
-
* @returns The entries that were placed into `dataDir` before any failure.
|
|
136
|
-
*/
|
|
137
125
|
export declare function promoteStaged({ stagingDir, dataDir, entries }: {
|
|
138
126
|
readonly stagingDir: string;
|
|
139
127
|
readonly dataDir: string;
|