@jini-ai/daemon 0.6.11 → 0.6.15-rc.1
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/CHANGELOG.md +41 -0
- package/dist/agent-executor/codex/home-config.d.ts +50 -0
- package/dist/agent-executor/codex/home-config.js +209 -0
- package/dist/agent-executor/codex/home-staging.d.ts +60 -0
- package/dist/agent-executor/codex/home-staging.js +195 -0
- package/dist/agent-executor/codex/isolation.d.ts +27 -0
- package/dist/agent-executor/codex/isolation.js +67 -0
- package/dist/agent-executor/contracts.d.ts +4 -1
- package/dist/agent-executor/event-translation.d.ts +7 -7
- package/dist/agent-executor/event-translation.js +17 -17
- package/dist/agent-executor/events.js +1 -1
- package/dist/agent-executor/index.d.ts +3 -1
- package/dist/agent-executor/index.js +3 -1
- package/dist/agent-executor/launch.d.ts +3 -2
- package/dist/agent-executor/launch.js +68 -17
- package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
- package/dist/agent-executor/mcp-bridge-probe.js +1 -1
- package/dist/agent-executor/mcp-bridge.d.ts +9 -86
- package/dist/agent-executor/mcp-bridge.js +4 -355
- package/dist/ai-trace-lifecycle.d.ts +48 -0
- package/dist/ai-trace-lifecycle.js +116 -0
- package/dist/core/read-only-tools.js +3 -1
- package/dist/delegated-tool-bridge.js +19 -1
- package/dist/http/attachment-lookup-routes.d.ts +44 -0
- package/dist/http/attachment-lookup-routes.js +71 -0
- package/dist/http/attachments.d.ts +112 -7
- package/dist/http/attachments.js +58 -17
- package/dist/http/connectors.js +23 -18
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +3 -0
- package/dist/http/tool-execution-routes.d.ts +69 -0
- package/dist/http/tool-execution-routes.js +181 -0
- package/dist/http/xai.js +19 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/remote-attachment-lookup.d.ts +29 -0
- package/dist/remote-attachment-lookup.js +71 -0
- package/dist/remote-tool-executor.d.ts +85 -0
- package/dist/remote-tool-executor.js +250 -0
- package/dist/routing-tool-executor.d.ts +29 -0
- package/dist/routing-tool-executor.js +16 -0
- package/dist/run-lifecycle.d.ts +4 -1
- package/dist/run-lifecycle.js +35 -5
- package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
- package/dist/store/event-log/sqlite-kernel.js +135 -0
- package/dist/store/event-log/sqlite-policy.d.ts +93 -0
- package/dist/store/event-log/sqlite-policy.js +74 -0
- package/dist/store/event-log/sqlite.d.ts +7 -31
- package/dist/store/event-log/sqlite.js +32 -95
- package/dist/tool-recovery/analytics.d.ts +27 -0
- package/dist/tool-recovery/analytics.js +38 -0
- package/dist/tool-recovery/index.d.ts +2 -0
- package/dist/tool-recovery/index.js +2 -0
- package/dist/tool-recovery/observability.d.ts +82 -0
- package/dist/tool-recovery/observability.js +88 -0
- package/package.json +15 -14
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tool-execution-routes
|
|
3
|
+
*
|
|
4
|
+
* `POST /internal/tool-executions` and `GET /internal/tool-catalog` — the server end of
|
|
5
|
+
* `createRemoteToolExecutor`. A host whose agent runtime runs in another local process mounts these
|
|
6
|
+
* over its own gated `ToolExecutor`, so that process can run tools without loading them. The wire
|
|
7
|
+
* protocol and the at-most-once rules are documented on `remote-tool-executor.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Trust boundary, checked before any body byte is parsed:
|
|
10
|
+
* - a dedicated per-boot secret read from `tokenEnvVar`, compared in constant time; unset → 503 (the
|
|
11
|
+
* route fails closed, there is no "open" mode), wrong or missing → 401;
|
|
12
|
+
* - the TCP peer (`req.socket.remoteAddress`, never a forwarded header) must be loopback → else 403.
|
|
13
|
+
* Only then does the route's own `express.json({ limit: maxBodyBytes })` run, so an unauthenticated
|
|
14
|
+
* caller cannot make the host buffer a large body, and an app-level parser with a smaller limit
|
|
15
|
+
* mounted after this route never sees these requests. Over the limit → 413.
|
|
16
|
+
*
|
|
17
|
+
* The executor below is the host's full gate (authorize, confirm, audit); this route adds no
|
|
18
|
+
* authority of its own and the serialized principal is passed through untouched for it to judge.
|
|
19
|
+
*
|
|
20
|
+
* Cancellation: the 200 headers are flushed before the executor runs, and the executor's signal
|
|
21
|
+
* aborts when the response closes before it finished (the caller destroyed its request). A request
|
|
22
|
+
* body that never completed never reaches the executor. Listeners are detached once the call
|
|
23
|
+
* settles, so a long-lived keep-alive socket never accumulates them.
|
|
24
|
+
*/
|
|
25
|
+
import express, {} from 'express';
|
|
26
|
+
import { createApiError } from '@jini-ai/protocol';
|
|
27
|
+
import { bearerTokenFromHeader, isLoopbackPeerAddress, sendApiError, statusForError, timingSafeTokenMatch } from '@jini-ai/http-kit';
|
|
28
|
+
import { TOOL_EXECUTIONS_ROUTE_PATH } from '../remote-tool-executor.js';
|
|
29
|
+
export { TOOL_EXECUTIONS_ROUTE_PATH };
|
|
30
|
+
export const TOOL_CATALOG_ROUTE_PATH = '/internal/tool-catalog';
|
|
31
|
+
export const DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR = 'JINI_TOOL_EXEC_TOKEN';
|
|
32
|
+
/** Tool inputs carry whole documents; 16 MiB matches the largest the agent side sends. */
|
|
33
|
+
export const DEFAULT_TOOL_EXEC_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
34
|
+
function refusalFor({ req, expected, tokenEnvVar }) {
|
|
35
|
+
if (!expected)
|
|
36
|
+
return createApiError({ code: 'NOT_CONFIGURED', message: `${tokenEnvVar} is not set — remote tool execution is disabled` });
|
|
37
|
+
if (!isLoopbackPeerAddress({ address: req.socket?.remoteAddress })) {
|
|
38
|
+
return createApiError({ code: 'FORBIDDEN', message: 'remote tool execution accepts loopback callers only' });
|
|
39
|
+
}
|
|
40
|
+
const presented = bearerTokenFromHeader({ header: req.get('authorization') });
|
|
41
|
+
if (presented === null || !timingSafeTokenMatch({ presented, expected })) {
|
|
42
|
+
return createApiError({ code: 'UNAUTHORIZED', message: `Authorization: Bearer <${tokenEnvVar}> required` });
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** The token + loopback gate; mounted before any body parser. Exported for hosts that need it on another path. */
|
|
47
|
+
export function requireToolExecutionCaller(_required, { tokenEnvVar = DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR, env = process.env } = {}) {
|
|
48
|
+
return (req, res, next) => {
|
|
49
|
+
const refusal = refusalFor({ req, expected: env[tokenEnvVar], tokenEnvVar });
|
|
50
|
+
if (refusal === null) {
|
|
51
|
+
next();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
sendApiError({ res, status: statusForError({ error: refusal }), error: refusal });
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Maps a body-parser failure (too large, malformed, aborted mid-body) to a refusal; the executor never runs. */
|
|
58
|
+
const bodyParserFailure = (error, _req, res, next) => {
|
|
59
|
+
if (res.headersSent) {
|
|
60
|
+
next(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const tooLarge = error.status === 413;
|
|
64
|
+
const apiError = createApiError({ code: tooLarge ? 'PAYLOAD_TOO_LARGE' : 'BAD_REQUEST', message: tooLarge ? 'request body too large' : 'request body must be valid JSON' });
|
|
65
|
+
sendApiError({ res, status: tooLarge ? 413 : 400, error: apiError });
|
|
66
|
+
};
|
|
67
|
+
function isRecord(value) {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
function hasStringId(value) {
|
|
71
|
+
return isRecord(value) && typeof value.id === 'string' && value.id.length > 0;
|
|
72
|
+
}
|
|
73
|
+
function nonEmptyString(value) {
|
|
74
|
+
return typeof value === 'string' && value.length > 0;
|
|
75
|
+
}
|
|
76
|
+
/** Validates the wire body; `null` → 400. The principal object is kept whole for the host's gate. */
|
|
77
|
+
function parseExecutionRequest(body) {
|
|
78
|
+
if (!isRecord(body) || !nonEmptyString(body.executionId) || !nonEmptyString(body.toolId))
|
|
79
|
+
return null;
|
|
80
|
+
if (!hasStringId(body.principal) || !hasStringId(body.run))
|
|
81
|
+
return null;
|
|
82
|
+
const run = body.run;
|
|
83
|
+
return {
|
|
84
|
+
executionId: body.executionId,
|
|
85
|
+
principal: body.principal,
|
|
86
|
+
run: { id: run.id },
|
|
87
|
+
toolId: body.toolId,
|
|
88
|
+
input: body.input,
|
|
89
|
+
...(body.context === undefined ? {} : { context: body.context }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function wireError(error) {
|
|
93
|
+
if (!(error instanceof Error))
|
|
94
|
+
return { name: 'Error', message: String(error) };
|
|
95
|
+
const code = error.code;
|
|
96
|
+
return { name: error.name, message: error.message, ...(typeof code === 'string' ? { code } : {}) };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Streams one execution as NDJSON. Headers go out first; surfaces are written as they are emitted
|
|
100
|
+
* and are dropped once the call settled; exactly one terminal line follows.
|
|
101
|
+
*/
|
|
102
|
+
async function streamExecution({ req, res, call, executor }, { runWithContext }) {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
// `res` 'close' (not `req` 'close', which fires as soon as a completed body was read) means the
|
|
105
|
+
// caller went away; after a normal end `writableFinished` is already true and nothing aborts.
|
|
106
|
+
const onClose = () => { if (!res.writableFinished)
|
|
107
|
+
controller.abort(); };
|
|
108
|
+
const onRequestError = () => controller.abort();
|
|
109
|
+
res.on('close', onClose);
|
|
110
|
+
req.on('error', onRequestError);
|
|
111
|
+
res.status(200).setHeader('content-type', 'application/x-ndjson');
|
|
112
|
+
res.setHeader('cache-control', 'no-store');
|
|
113
|
+
res.flushHeaders();
|
|
114
|
+
let settled = false;
|
|
115
|
+
const writeLine = (line) => { if (!res.writableEnded)
|
|
116
|
+
res.write(`${JSON.stringify(line)}\n`); };
|
|
117
|
+
const emitSurface = async (emission) => { if (!settled)
|
|
118
|
+
writeLine({ type: 'surface', emission }); };
|
|
119
|
+
const run = () => executor.execute({ principal: call.principal, run: call.run, toolId: call.toolId, input: call.input }, { signal: controller.signal, emitSurface });
|
|
120
|
+
try {
|
|
121
|
+
if (!req.complete || controller.signal.aborted)
|
|
122
|
+
return;
|
|
123
|
+
const result = runWithContext ? await runWithContext({ context: call.context, fn: run }) : await run();
|
|
124
|
+
writeLine({ type: 'result', result });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
writeLine({ type: 'throw', error: wireError(error) });
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
settled = true;
|
|
131
|
+
res.off('close', onClose);
|
|
132
|
+
req.off('error', onRequestError);
|
|
133
|
+
res.end();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function executionHandler({ executor }, optional) {
|
|
137
|
+
return (req, res) => {
|
|
138
|
+
const call = parseExecutionRequest(req.body);
|
|
139
|
+
if (call === null) {
|
|
140
|
+
const error = createApiError({ code: 'BAD_REQUEST', message: 'body must be { executionId, principal: { id }, run: { id }, toolId, input, context? }' });
|
|
141
|
+
sendApiError({ res, status: 400, error });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
void streamExecution({ req, res, call, executor }, optional);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Catalog for one principal. Written with `res.end` rather than `res.json` because Express adds a
|
|
149
|
+
* weak ETag to `json` GET responses, and this answer must never be served from a validator cache:
|
|
150
|
+
* plugin state and delete policy can change between two runs.
|
|
151
|
+
*/
|
|
152
|
+
function catalogHandler({ listToolsFor }) {
|
|
153
|
+
return async (req, res) => {
|
|
154
|
+
const principalId = req.query.principalId;
|
|
155
|
+
if (!nonEmptyString(principalId)) {
|
|
156
|
+
sendApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: 'principalId query parameter is required' }) });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const tools = await listToolsFor({ principalId });
|
|
161
|
+
res.status(200).setHeader('content-type', 'application/json; charset=utf-8');
|
|
162
|
+
res.setHeader('cache-control', 'no-store');
|
|
163
|
+
res.end(JSON.stringify({ tools }));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Mounts both routes on `app` (an Express app or a `Router`). Mount it BEFORE any app-level
|
|
172
|
+
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
173
|
+
* earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
|
|
174
|
+
*/
|
|
175
|
+
export function registerToolExecutionRoutes({ app, executor, listToolsFor }, { tokenEnvVar, env, maxBodyBytes = DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, runWithContext } = {}) {
|
|
176
|
+
const gate = requireToolExecutionCaller({}, { ...(tokenEnvVar === undefined ? {} : { tokenEnvVar }), ...(env === undefined ? {} : { env }) });
|
|
177
|
+
const parser = express.json({ limit: maxBodyBytes });
|
|
178
|
+
app.post(TOOL_EXECUTIONS_ROUTE_PATH, gate, parser, executionHandler({ executor }, runWithContext === undefined ? {} : { runWithContext }), bodyParserFailure);
|
|
179
|
+
app.get(TOOL_CATALOG_ROUTE_PATH, gate, catalogHandler({ listToolsFor }));
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=tool-execution-routes.js.map
|
package/dist/http/xai.js
CHANGED
|
@@ -167,8 +167,17 @@ function resolveStorageAndSearchDeps(deps) {
|
|
|
167
167
|
function xaiRedirectUri(resolved) {
|
|
168
168
|
return `http://${resolved.callbackHost}:${resolved.callbackPort}${resolved.callbackPath}`;
|
|
169
169
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
170
|
+
/** Tracks only the state's lookup key; `PendingAuthCache` remains the single-use authorization owner.
|
|
171
|
+
* Keying by the shared listener slot keeps cancellation scoped to one mounted route pack. */
|
|
172
|
+
const pendingOAuthStates = new WeakMap();
|
|
173
|
+
/** Revokes the pending state before stopping and clearing its listener. Listener shutdown is
|
|
174
|
+
* best-effort — the listener self-closes on completion/timeout anyway, matching OD's origin
|
|
175
|
+
* `stopActiveListener`. Revocation must precede the await so a late paste-back cannot reconnect. */
|
|
176
|
+
async function stopListener({ listenerRef, pending }, _optional = {}) {
|
|
177
|
+
const state = pendingOAuthStates.get(listenerRef);
|
|
178
|
+
pendingOAuthStates.delete(listenerRef);
|
|
179
|
+
if (state !== undefined)
|
|
180
|
+
pending.consume({ state });
|
|
172
181
|
const current = listenerRef.current;
|
|
173
182
|
listenerRef.current = null;
|
|
174
183
|
if (!current)
|
|
@@ -331,6 +340,7 @@ export const xaiOauthStartRoute = defineJsonRoute({
|
|
|
331
340
|
pending: resolved.pending,
|
|
332
341
|
redirectUri: xaiRedirectUri(resolved),
|
|
333
342
|
});
|
|
343
|
+
pendingOAuthStates.set(resolved.listenerRef, state);
|
|
334
344
|
const listener = await resolved.startCallbackListener({
|
|
335
345
|
host: resolved.callbackHost,
|
|
336
346
|
port: resolved.callbackPort,
|
|
@@ -348,7 +358,7 @@ export const xaiOauthStartRoute = defineJsonRoute({
|
|
|
348
358
|
});
|
|
349
359
|
}
|
|
350
360
|
catch (error) {
|
|
351
|
-
await stopListener(resolved
|
|
361
|
+
await stopListener(resolved);
|
|
352
362
|
const correlationId = randomUUID();
|
|
353
363
|
resolved.onInternalError({ source: 'oauth-start', correlationId, error });
|
|
354
364
|
return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
@@ -383,7 +393,7 @@ export const xaiOauthCompleteRoute = defineJsonRoute({
|
|
|
383
393
|
// We won the race against the loopback listener (or it was never going to resolve in the
|
|
384
394
|
// first place); shut it down so the next `oauth/start` has a clean slate — matching OD's
|
|
385
395
|
// origin behavior on a successful manual paste-back.
|
|
386
|
-
await stopListener(resolved
|
|
396
|
+
await stopListener(resolved);
|
|
387
397
|
return ok({ value: { ok: true } });
|
|
388
398
|
}
|
|
389
399
|
catch (error) {
|
|
@@ -410,10 +420,10 @@ export const xaiOauthCompleteRoute = defineJsonRoute({
|
|
|
410
420
|
export const xaiOauthCancelRoute = defineJsonRoute({
|
|
411
421
|
method: 'post', path: '/api/xai/oauth/cancel', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => {
|
|
412
422
|
const resolved = resolveXaiHttpDeps(deps);
|
|
413
|
-
// Cancel
|
|
414
|
-
// user clicking Cancel mid-reconnect would otherwise lose their existing grant. Disconnect
|
|
415
|
-
// (below) is the destructive path.
|
|
416
|
-
await stopListener(resolved
|
|
423
|
+
// Cancel revokes the pending state and stops the listener; it must never wipe a stored
|
|
424
|
+
// token — a user clicking Cancel mid-reconnect would otherwise lose their existing grant. Disconnect
|
|
425
|
+
// (below) is the destructive path. Preserves OD's origin `oauth/cancel` token-retention behavior.
|
|
426
|
+
await stopListener(resolved);
|
|
417
427
|
return ok({ value: { ok: true } });
|
|
418
428
|
}
|
|
419
429
|
}, { requireSameOrigin: true });
|
|
@@ -449,7 +459,7 @@ export const xaiOauthDisconnectRoute = defineJsonRoute({
|
|
|
449
459
|
method: 'post', path: '/api/xai/oauth/disconnect', parse: () => ok({ value: undefined }), handle: async ({ input: _input, deps }) => {
|
|
450
460
|
const resolved = resolveXaiHttpDeps(deps);
|
|
451
461
|
try {
|
|
452
|
-
await stopListener(resolved
|
|
462
|
+
await stopListener(resolved);
|
|
453
463
|
await clearStoredOAuthToken({ dataDir: resolved.dataDir, fileName: resolved.tokenFileName });
|
|
454
464
|
return ok({ value: { ok: true } });
|
|
455
465
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -80,9 +80,12 @@
|
|
|
80
80
|
export * from './event-log.js';
|
|
81
81
|
export * from './close-status.js';
|
|
82
82
|
export * from './run-lifecycle.js';
|
|
83
|
+
export * from './ai-trace-lifecycle.js';
|
|
83
84
|
export * from './tool-executor.js';
|
|
84
85
|
export * from './delegated-tool-bridge.js';
|
|
85
86
|
export * from './remote-tool-bridge.js';
|
|
87
|
+
export * from './remote-tool-executor.js';
|
|
88
|
+
export * from './routing-tool-executor.js';
|
|
86
89
|
export * from './frontend-session-registry.js';
|
|
87
90
|
export * from './frontend-capability-tools.js';
|
|
88
91
|
export * from './agent-executor.js';
|
package/dist/index.js
CHANGED
|
@@ -80,9 +80,12 @@
|
|
|
80
80
|
export * from './event-log.js';
|
|
81
81
|
export * from './close-status.js';
|
|
82
82
|
export * from './run-lifecycle.js';
|
|
83
|
+
export * from './ai-trace-lifecycle.js';
|
|
83
84
|
export * from './tool-executor.js';
|
|
84
85
|
export * from './delegated-tool-bridge.js';
|
|
85
86
|
export * from './remote-tool-bridge.js';
|
|
87
|
+
export * from './remote-tool-executor.js';
|
|
88
|
+
export * from './routing-tool-executor.js';
|
|
86
89
|
export * from './frontend-session-registry.js';
|
|
87
90
|
export * from './frontend-capability-tools.js';
|
|
88
91
|
export * from './agent-executor.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module remote-attachment-lookup
|
|
3
|
+
*
|
|
4
|
+
* Client for `registerAttachmentLookupRoutes`: the two read methods of a daemon-owned
|
|
5
|
+
* `AttachmentStore`, called from another local process over `node:http`. A store rejection comes
|
|
6
|
+
* back as the same `AttachmentRejectedError` class (same `reason` and message), so a caller's
|
|
7
|
+
* `instanceof` check behaves exactly as it would against the in-process store.
|
|
8
|
+
*
|
|
9
|
+
* Exported from `@jini-ai/daemon/http` rather than the root entry because it shares that class with
|
|
10
|
+
* the attachments route pack.
|
|
11
|
+
*/
|
|
12
|
+
import * as http from 'node:http';
|
|
13
|
+
import { type AttachmentLookup } from './http/attachment-lookup-routes.js';
|
|
14
|
+
export interface RemoteAttachmentLookupRequired {
|
|
15
|
+
/** Origin of the daemon, e.g. `http://127.0.0.1:4100`. */
|
|
16
|
+
readonly baseUrl: string;
|
|
17
|
+
/** The daemon's bearer token. */
|
|
18
|
+
readonly token: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RemoteAttachmentLookupOptional {
|
|
21
|
+
/** Injectable `node:http.request`. */
|
|
22
|
+
readonly request?: typeof http.request;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates the remote lookup.
|
|
26
|
+
* @complexity One loopback round trip per call; O(response bytes).
|
|
27
|
+
*/
|
|
28
|
+
export declare function createRemoteAttachmentLookup({ baseUrl, token }: RemoteAttachmentLookupRequired, { request }?: RemoteAttachmentLookupOptional): AttachmentLookup;
|
|
29
|
+
//# sourceMappingURL=remote-attachment-lookup.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module remote-attachment-lookup
|
|
3
|
+
*
|
|
4
|
+
* Client for `registerAttachmentLookupRoutes`: the two read methods of a daemon-owned
|
|
5
|
+
* `AttachmentStore`, called from another local process over `node:http`. A store rejection comes
|
|
6
|
+
* back as the same `AttachmentRejectedError` class (same `reason` and message), so a caller's
|
|
7
|
+
* `instanceof` check behaves exactly as it would against the in-process store.
|
|
8
|
+
*
|
|
9
|
+
* Exported from `@jini-ai/daemon/http` rather than the root entry because it shares that class with
|
|
10
|
+
* the attachments route pack.
|
|
11
|
+
*/
|
|
12
|
+
import * as http from 'node:http';
|
|
13
|
+
import { AttachmentRejectedError } from './http/attachments.js';
|
|
14
|
+
import { ATTACHMENT_LOOKUP_ROUTE_PATH } from './http/attachment-lookup-routes.js';
|
|
15
|
+
function postJson({ url, token, payload }, { request }) {
|
|
16
|
+
const body = JSON.stringify(payload);
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const req = request(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
agent: false,
|
|
21
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) },
|
|
22
|
+
}, (res) => {
|
|
23
|
+
let text = '';
|
|
24
|
+
res.setEncoding('utf8');
|
|
25
|
+
res.on('data', (chunk) => { text += chunk; });
|
|
26
|
+
res.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
resolve({ status: res.statusCode ?? 0, body: JSON.parse(text) });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
resolve({ status: res.statusCode ?? 0, body: null });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
res.on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
req.on('error', reject);
|
|
37
|
+
req.end(body);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function errorOf(body) {
|
|
41
|
+
const error = body?.error;
|
|
42
|
+
return typeof error === 'object' && error !== null ? error : {};
|
|
43
|
+
}
|
|
44
|
+
/** Returns `value` from a 200, rethrows a store rejection as `AttachmentRejectedError`, else a plain error naming the status. */
|
|
45
|
+
function unwrap({ status, body }) {
|
|
46
|
+
if (status === 200)
|
|
47
|
+
return body?.value ?? null;
|
|
48
|
+
const error = errorOf(body);
|
|
49
|
+
if (error.code === 'ATTACHMENT_REJECTED' && typeof error.details?.reason === 'string') {
|
|
50
|
+
throw new AttachmentRejectedError({ reason: error.details.reason, message: String(error.message ?? '') });
|
|
51
|
+
}
|
|
52
|
+
const code = typeof error.code === 'string' ? ` ${error.code}` : '';
|
|
53
|
+
throw new Error(`attachment lookup failed: HTTP ${status}${code}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates the remote lookup.
|
|
57
|
+
* @complexity One loopback round trip per call; O(response bytes).
|
|
58
|
+
*/
|
|
59
|
+
export function createRemoteAttachmentLookup({ baseUrl, token }, { request = http.request } = {}) {
|
|
60
|
+
const url = new URL(ATTACHMENT_LOOKUP_ROUTE_PATH, baseUrl);
|
|
61
|
+
const call = async (payload) => unwrap(await postJson({ url, token, payload }, { request }));
|
|
62
|
+
return {
|
|
63
|
+
resolveForRun: async ({ ref, runId }) => ((await call({ op: 'resolveForRun', ref, runId })) ?? undefined),
|
|
64
|
+
listPendingForOwner: async ({ ownerId }, optional = {}) => (await call({
|
|
65
|
+
op: 'listPendingForOwner',
|
|
66
|
+
ownerId,
|
|
67
|
+
...(optional.runId === undefined ? {} : { runId: optional.runId }),
|
|
68
|
+
})),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=remote-attachment-lookup.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import { type Principal, type RunRef, type SurfaceEmission } from '@jini-ai/core';
|
|
3
|
+
import type { ToolExecutionResult, ToolExecutor } from './tool-executor.js';
|
|
4
|
+
export type ToolRpcErrorCode = 'TOOL_NOT_DISPATCHED' | 'TOOL_OUTCOME_UNKNOWN';
|
|
5
|
+
/** A remote tool call that failed in transport. `code` says whether the call may have run; see the module doc. */
|
|
6
|
+
export declare class ToolRpcError extends Error {
|
|
7
|
+
readonly code: ToolRpcErrorCode;
|
|
8
|
+
constructor({ code, message }: {
|
|
9
|
+
readonly code: ToolRpcErrorCode;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
}, optional?: ErrorOptions);
|
|
12
|
+
}
|
|
13
|
+
/** The POST body `registerToolExecutionRoutes` accepts. */
|
|
14
|
+
export interface ToolExecutionWireRequest {
|
|
15
|
+
readonly executionId: string;
|
|
16
|
+
readonly principal: Principal;
|
|
17
|
+
readonly run: RunRef;
|
|
18
|
+
readonly toolId: string;
|
|
19
|
+
readonly input: unknown;
|
|
20
|
+
/** Opaque host JSON, handed to the far side's `runWithContext`. */
|
|
21
|
+
readonly context?: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** What a far-side throw carries over the wire. */
|
|
24
|
+
export interface ToolExecutionWireError {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly code?: string;
|
|
28
|
+
}
|
|
29
|
+
/** One NDJSON line of a tool-execution response. */
|
|
30
|
+
export type ToolExecutionWireLine = {
|
|
31
|
+
readonly type: 'surface';
|
|
32
|
+
readonly emission: SurfaceEmission;
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: 'result';
|
|
35
|
+
readonly result: ToolExecutionResult;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: 'throw';
|
|
38
|
+
readonly error: ToolExecutionWireError;
|
|
39
|
+
};
|
|
40
|
+
export declare const TOOL_EXECUTIONS_ROUTE_PATH = "/internal/tool-executions";
|
|
41
|
+
export interface RemoteToolExecutorRequired {
|
|
42
|
+
/** Origin of the far server, e.g. `http://127.0.0.1:3000`. */
|
|
43
|
+
readonly baseUrl: string;
|
|
44
|
+
/** Bearer secret the far route checks. */
|
|
45
|
+
readonly token: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RemoteToolExecutorOptional {
|
|
48
|
+
/**
|
|
49
|
+
* Injectable request function for the base URL's scheme. Omitted → `node:https.request` for
|
|
50
|
+
* `https:`, else `node:http.request`. A host whose local CA is not in the process trust store wraps
|
|
51
|
+
* `https.request` with its `ca`; tests use it to observe or fail the transport.
|
|
52
|
+
*/
|
|
53
|
+
readonly request?: typeof http.request;
|
|
54
|
+
/** Builds the opaque per-call `context` the far side receives. Omitted → no `context` field. */
|
|
55
|
+
readonly contextFor?: (required: {
|
|
56
|
+
readonly principal: Principal;
|
|
57
|
+
readonly run: RunRef;
|
|
58
|
+
readonly toolId: string;
|
|
59
|
+
}, optional?: Record<string, never>) => unknown;
|
|
60
|
+
}
|
|
61
|
+
/** Parses one NDJSON line; `null` means malformed, which the caller treats as outcome unknown. */
|
|
62
|
+
export declare function parseToolExecutionLine({ text }: {
|
|
63
|
+
readonly text: string;
|
|
64
|
+
}, _optional?: Record<string, never>): ToolExecutionWireLine | null;
|
|
65
|
+
/**
|
|
66
|
+
* Turns a far-side throw back into an error a local caller can branch on. `ToolInputError` keeps its
|
|
67
|
+
* class, because the in-process executor throws it for an unknown tool id and callers test it with
|
|
68
|
+
* `instanceof`. Anything else becomes a plain `Error` carrying the same name, message and code.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rehydrateToolExecutionError({ error }: {
|
|
71
|
+
readonly error: ToolExecutionWireError;
|
|
72
|
+
}, _optional?: Record<string, never>): Error;
|
|
73
|
+
/**
|
|
74
|
+
* Creates the client `ToolExecutor` for a far `registerToolExecutionRoutes` endpoint.
|
|
75
|
+
*
|
|
76
|
+
* - `execute` streams one call; a caller `signal` (or `cancel` with the id sent on the wire) destroys
|
|
77
|
+
* the request, which the far route turns into its own abort, and settles locally as `cancelled`
|
|
78
|
+
* — the same result an in-process executor gives a cancelled call.
|
|
79
|
+
* - `getAuditRecord` returns `null`: the far side owns the audit trail.
|
|
80
|
+
* - `resumeConfirmation` throws: confirmations park on, and are answered on, the far side.
|
|
81
|
+
*
|
|
82
|
+
* @complexity O(response bytes) per call; `cancel` is an O(1) map lookup.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createRemoteToolExecutor({ baseUrl, token }: RemoteToolExecutorRequired, { request: injectedRequest, contextFor }?: RemoteToolExecutorOptional): ToolExecutor;
|
|
85
|
+
//# sourceMappingURL=remote-tool-executor.d.ts.map
|