@jini-ai/daemon 0.6.12 → 0.6.15-rc.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent-executor/codex/home-config.d.ts +50 -0
  3. package/dist/agent-executor/codex/home-config.js +209 -0
  4. package/dist/agent-executor/codex/home-staging.d.ts +60 -0
  5. package/dist/agent-executor/codex/home-staging.js +195 -0
  6. package/dist/agent-executor/codex/isolation.d.ts +27 -0
  7. package/dist/agent-executor/codex/isolation.js +67 -0
  8. package/dist/agent-executor/contracts.d.ts +4 -1
  9. package/dist/agent-executor/event-translation.d.ts +7 -7
  10. package/dist/agent-executor/event-translation.js +17 -17
  11. package/dist/agent-executor/events.js +5 -2
  12. package/dist/agent-executor/index.d.ts +3 -1
  13. package/dist/agent-executor/index.js +3 -1
  14. package/dist/agent-executor/launch.d.ts +3 -2
  15. package/dist/agent-executor/launch.js +77 -21
  16. package/dist/agent-executor/live-session-state.js +4 -1
  17. package/dist/agent-executor/mcp-bridge-probe.d.ts +2 -1
  18. package/dist/agent-executor/mcp-bridge-probe.js +1 -1
  19. package/dist/agent-executor/mcp-bridge.d.ts +9 -86
  20. package/dist/agent-executor/mcp-bridge.js +12 -360
  21. package/dist/ai-trace-lifecycle.d.ts +48 -0
  22. package/dist/ai-trace-lifecycle.js +116 -0
  23. package/dist/core/read-only-tools.js +3 -1
  24. package/dist/delegated-tool-bridge.js +19 -1
  25. package/dist/http/attachment-lookup-routes.d.ts +44 -0
  26. package/dist/http/attachment-lookup-routes.js +71 -0
  27. package/dist/http/attachments.d.ts +112 -7
  28. package/dist/http/attachments.js +58 -17
  29. package/dist/http/connectors.js +23 -18
  30. package/dist/http/index.d.ts +6 -0
  31. package/dist/http/index.js +3 -0
  32. package/dist/http/message-relay.js +4 -1
  33. package/dist/http/tool-execution-routes.d.ts +69 -0
  34. package/dist/http/tool-execution-routes.js +181 -0
  35. package/dist/http/xai.js +19 -9
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +3 -0
  38. package/dist/remote-attachment-lookup.d.ts +29 -0
  39. package/dist/remote-attachment-lookup.js +71 -0
  40. package/dist/remote-tool-executor.d.ts +85 -0
  41. package/dist/remote-tool-executor.js +250 -0
  42. package/dist/routing-tool-executor.d.ts +29 -0
  43. package/dist/routing-tool-executor.js +16 -0
  44. package/dist/run-lifecycle.d.ts +4 -1
  45. package/dist/run-lifecycle.js +35 -5
  46. package/dist/store/event-log/sqlite-kernel.d.ts +18 -0
  47. package/dist/store/event-log/sqlite-kernel.js +135 -0
  48. package/dist/store/event-log/sqlite-policy.d.ts +93 -0
  49. package/dist/store/event-log/sqlite-policy.js +74 -0
  50. package/dist/store/event-log/sqlite.d.ts +7 -31
  51. package/dist/store/event-log/sqlite.js +32 -95
  52. package/dist/tool-recovery/analytics.d.ts +27 -0
  53. package/dist/tool-recovery/analytics.js +38 -0
  54. package/dist/tool-recovery/index.d.ts +2 -0
  55. package/dist/tool-recovery/index.js +2 -0
  56. package/dist/tool-recovery/observability.d.ts +82 -0
  57. package/dist/tool-recovery/observability.js +88 -0
  58. package/package.json +15 -14
@@ -0,0 +1,69 @@
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 { type ErrorRequestHandler, type RequestHandler } from 'express';
26
+ import type { ToolDescriptor } from '@jini-ai/core';
27
+ import type { ToolExecutor } from '../tool-executor.js';
28
+ import { TOOL_EXECUTIONS_ROUTE_PATH } from '../remote-tool-executor.js';
29
+ export { TOOL_EXECUTIONS_ROUTE_PATH };
30
+ export declare const TOOL_CATALOG_ROUTE_PATH = "/internal/tool-catalog";
31
+ export declare 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 declare const DEFAULT_TOOL_EXEC_MAX_BODY_BYTES: number;
34
+ /** Anything routes can be mounted on: an Express app or a `Router`. */
35
+ export interface ToolExecutionRouteTarget {
36
+ post(path: string, ...handlers: Array<RequestHandler | ErrorRequestHandler>): unknown;
37
+ get(path: string, ...handlers: RequestHandler[]): unknown;
38
+ }
39
+ /** Runs one call inside host context (e.g. an AsyncLocalStorage scope) built from the wire `context`. */
40
+ export type RunWithToolContext = <T>(required: {
41
+ readonly context: unknown;
42
+ readonly fn: () => Promise<T>;
43
+ }, optional?: Record<string, never>) => Promise<T>;
44
+ export interface ToolExecutionRoutesRequired {
45
+ readonly app: ToolExecutionRouteTarget;
46
+ readonly executor: ToolExecutor;
47
+ /** Built fresh on every catalog request; the route never caches it. */
48
+ readonly listToolsFor: (required: {
49
+ readonly principalId: string;
50
+ }, optional?: Record<string, never>) => readonly ToolDescriptor[] | Promise<readonly ToolDescriptor[]>;
51
+ }
52
+ export interface ToolExecutionRoutesOptional {
53
+ /** Env var holding the shared secret. Defaults to {@link DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR}. */
54
+ readonly tokenEnvVar?: string;
55
+ /** Defaults to `process.env`; injectable so tests never mutate the real env. */
56
+ readonly env?: NodeJS.ProcessEnv;
57
+ /** Defaults to {@link DEFAULT_TOOL_EXEC_MAX_BODY_BYTES}. */
58
+ readonly maxBodyBytes?: number;
59
+ readonly runWithContext?: RunWithToolContext;
60
+ }
61
+ /** The token + loopback gate; mounted before any body parser. Exported for hosts that need it on another path. */
62
+ export declare function requireToolExecutionCaller(_required: Record<string, never>, { tokenEnvVar, env }?: Pick<ToolExecutionRoutesOptional, 'tokenEnvVar' | 'env'>): RequestHandler;
63
+ /**
64
+ * Mounts both routes on `app` (an Express app or a `Router`). Mount it BEFORE any app-level
65
+ * `express.json()` with a smaller limit: this route parses its own body after its gate, and an
66
+ * earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
67
+ */
68
+ export declare function registerToolExecutionRoutes({ app, executor, listToolsFor }: ToolExecutionRoutesRequired, { tokenEnvVar, env, maxBodyBytes, runWithContext }?: ToolExecutionRoutesOptional): void;
69
+ //# sourceMappingURL=tool-execution-routes.d.ts.map
@@ -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
- /** Stops (best-effort) and clears the in-flight loopback listener, if any. Never throws — the listener self-closes on completion/timeout anyway, matching OD's origin `stopActiveListener`. */
171
- async function stopListener(listenerRef) {
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.listenerRef);
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.listenerRef);
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 only stops the in-flight loopback listener; it must never wipe a stored token — a
414
- // user clicking Cancel mid-reconnect would otherwise lose their existing grant. Disconnect
415
- // (below) is the destructive path. Matches OD's origin `oauth/cancel` handler exactly.
416
- await stopListener(resolved.listenerRef);
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.listenerRef);
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