@jini-ai/daemon 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.6.7 — 2026-10-15
2
+
3
+ - An unknown tool id is a `400 BAD_REQUEST`, not a redacted `500 INTERNAL_ERROR` (b4a3abad): `ToolExecutor` throws `ToolInputError` naming up to eight registered ids the unknown id prefixes (e.g. `content_read` → `content_read.menu`), and the delegated-tools route returns that message.
4
+
1
5
  ## 0.6.6 — 2026-10-14
2
6
 
3
7
  - A failed jini MCP bridge now says why (8bc0f07d): the run still stops at once, but a disposable credential-free copy of the bridge is spawned, sent MCP `initialize`, and its exit code + stderr tail (e.g. `ERR_MODULE_NOT_FOUND`) go into the `MCP_BRIDGE_UNAVAILABLE` message and `error.details`, emitted before the run's end event. `probeMcpBridge` is exported so hosts can run the same check at boot; `mcpJsonInjection.probeOnFailure: false` opts out. The baseline agent env moves to `agent-env.ts` so probe and launch share one allowlist.
@@ -33,8 +33,8 @@ export interface DelegatedToolInvocation {
33
33
  export interface DelegatedToolBridge {
34
34
  /**
35
35
  * Emits a canonical `tool_use`, executes through `ToolExecutor`, then emits
36
- * exactly one matching `tool_result`. Unknown tool ids remain programmer or
37
- * routing errors from `ToolExecutor` and are rethrown after their error
36
+ * exactly one matching `tool_result`. Unknown tool ids are input errors
37
+ * from `ToolExecutor` and are rethrown after their error
38
38
  * result is recorded.
39
39
  */
40
40
  execute(requiredArgs: Pick<DelegatedToolInvocation, "runId" | "toolUseId" | "toolId" | "principal" | "input">, optionalArgs?: Pick<DelegatedToolInvocation, "signal">): Promise<ToolExecutionResult>;
@@ -1,5 +1,5 @@
1
1
  import type { Express } from 'express';
2
- import type { Principal, ToolRegistry } from '@jini-ai/core';
2
+ import { type Principal, type ToolRegistry } from '@jini-ai/core';
3
3
  import { type RunLifecycle, type ToolExecutionResult, type ToolExecutor } from '../index.js';
4
4
  import { type AdapterContext } from '@jini-ai/http-kit';
5
5
  export interface DelegatedToolExecuteRequest {
@@ -81,7 +81,7 @@ export interface DelegatedToolsHttpDeps {
81
81
  /**
82
82
  * Host opt-in: the model-safe text to send in place of `'an internal error occurred'` for every
83
83
  * failure this route would otherwise answer with the SEC-005-redacted `500 INTERNAL_ERROR` — a
84
- * thrown executor (e.g. an unknown tool id), a throwing `resolvePrincipal`, `timed-out`,
84
+ * thrown executor infrastructure failure, a throwing `resolvePrincipal`, `timed-out`,
85
85
  * `cancelled`, and a `failed` result `isModelSafeToolFailure` did not vouch for. Without it, those
86
86
  * reach the model as a bare "INTERNAL_ERROR" it can neither act on nor report.
87
87
  *
@@ -98,8 +98,8 @@ export interface DelegatedToolsHttpDeps {
98
98
  * `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
99
99
  * run. Maps `ToolExecutionResult.status` to the HTTP response via `toolExecutionResultToApiResult`
100
100
  * above — mirroring `db-ops.ts`'s precedent for the identical status union rather than treating
101
- * every business outcome as a `200`. A genuinely unexpected throw (an unregistered `toolId` — a
102
- * routing/programming error per `ToolExecutor.execute`'s own contract — or a race where the run
101
+ * every business outcome as a `200`. An unregistered `toolId` throws `ToolInputError` and answers
102
+ * `400 BAD_REQUEST`. A genuinely unexpected throw (such as a race where the run
103
103
  * became terminal between this route's existence check and the bridge's first emitted event)
104
104
  * reaches the same SEC-005 redaction path.
105
105
  */
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { randomUUID } from 'node:crypto';
29
29
  import { createApiError } from '@jini-ai/protocol';
30
+ import { ToolInputError } from '@jini-ai/core';
30
31
  import { createDelegatedToolBridge } from '../index.js';
31
32
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
32
33
  import { validationError } from '@jini-ai/http-kit';
@@ -159,8 +160,8 @@ function toolExecutionResultToApiResult(deps, runId, toolId, result) {
159
160
  * `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
160
161
  * run. Maps `ToolExecutionResult.status` to the HTTP response via `toolExecutionResultToApiResult`
161
162
  * above — mirroring `db-ops.ts`'s precedent for the identical status union rather than treating
162
- * every business outcome as a `200`. A genuinely unexpected throw (an unregistered `toolId` — a
163
- * routing/programming error per `ToolExecutor.execute`'s own contract — or a race where the run
163
+ * every business outcome as a `200`. An unregistered `toolId` throws `ToolInputError` and answers
164
+ * `400 BAD_REQUEST`. A genuinely unexpected throw (such as a race where the run
164
165
  * became terminal between this route's existence check and the bridge's first emitted event)
165
166
  * reaches the same SEC-005 redaction path.
166
167
  */
@@ -219,6 +220,9 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
219
220
  return toolExecutionResultToApiResult(deps, input.runId, input.toolId, result);
220
221
  }
221
222
  catch (error) {
223
+ if (error instanceof ToolInputError) {
224
+ return err({ error: createApiError({ code: 'BAD_REQUEST', message: error.message }) });
225
+ }
222
226
  return err({ error: reportInternalError(deps, 'delegated-tool-execute', error, input.runId, input.toolId) });
223
227
  }
224
228
  }
@@ -83,7 +83,7 @@ export interface ToolExecutor {
83
83
  * @param emitSurface - Transport-supplied seam letting the handler push a human-only surface into
84
84
  * the run's event stream *before* this call settles. Omit for a caller with no run event stream;
85
85
  * see `@jini-ai/core`'s `SurfaceEmitter` for why a handler that waits on a human needs it.
86
- * @throws If `toolId` isn't registered — a routing/programming error, distinct from the denial/confirmation-denial states `ToolExecutionResult.status` covers.
86
+ * @throws {ToolInputError} If `toolId` isn't registered, with up to eight registered prefix matches.
87
87
  */
88
88
  execute(args: {
89
89
  readonly principal: Principal;
@@ -125,7 +125,7 @@ export interface CreateToolExecutorOptions {
125
125
  * — the one and only caller of that internal export.
126
126
  * @param options.delegate - Transport-supplied authorize/confirm UI seam;
127
127
  * omit for a headless caller whose tools never need interactive gating.
128
- * @complexity `execute` is O(1) plus the handler's own cost; `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
128
+ * @complexity Known-id `execute` is O(1) plus the handler's own cost; unknown ids scan O(n) descriptors for hints. `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
129
129
  * @overallScore 100/100
130
130
  */
131
131
  export declare function createToolExecutor(requiredArgs: Pick<CreateToolExecutorOptions, "registry">, optionalArgs?: Pick<CreateToolExecutorOptions, "delegate" | "now">): ToolExecutor;
@@ -81,7 +81,7 @@ function truncateOutput(output, maxOutputBytes) {
81
81
  * — the one and only caller of that internal export.
82
82
  * @param options.delegate - Transport-supplied authorize/confirm UI seam;
83
83
  * omit for a headless caller whose tools never need interactive gating.
84
- * @complexity `execute` is O(1) plus the handler's own cost; `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
84
+ * @complexity Known-id `execute` is O(1) plus the handler's own cost; unknown ids scan O(n) descriptors for hints. `resumeConfirmation`/`cancel`/`getAuditRecord` are O(1) map lookups.
85
85
  * @overallScore 100/100
86
86
  */
87
87
  export function createToolExecutor(requiredArgs, optionalArgs = {}) {
@@ -182,7 +182,9 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
182
182
  // existence, never returns a handler, so this doesn't reopen the leak authorizeToolInvocation
183
183
  // closed — that gate still runs, still unconditionally, inside the call below.
184
184
  if (!registry.has({ toolId })) {
185
- throw new Error(`ToolExecutor: unknown tool "${toolId}"`);
185
+ const candidates = registry.list({}).map((tool) => tool.id).filter((id) => id.startsWith(toolId)).slice(0, 8);
186
+ const hint = candidates.length > 0 ? `; did you mean ${candidates.join(', ')}?` : '';
187
+ throw new ToolInputError({ message: `ToolExecutor: unknown tool "${toolId}"${hint}` });
186
188
  }
187
189
  const executionId = openAudit(principal, run, toolId);
188
190
  // Observes `signal` for this execution's ENTIRE remaining lifetime, not just the handler-run
@@ -216,7 +218,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
216
218
  // Recorded terminal and returned as `failed` with a fixed message. `failed` is the right state:
217
219
  // this execution ended, and it ended for an operational reason rather than a decision. The real
218
220
  // exception stays server-side. Deliberately narrower than the `unknown tool` throw above, which
219
- // stays a throw because it is a routing/programming error, not an execution outcome.
221
+ // stays a typed input-error throw because no tool execution exists to settle.
220
222
  let resolved;
221
223
  try {
222
224
  resolved = await authorize(principal, run, toolId, input);
@@ -1,8 +1,8 @@
1
1
  // Local federation forks moved to @jini-ai/mcp/federation (+ /stdio, /approvals); see development/DELETED-CODE.md.
2
2
  /**
3
3
  * `ToolExecutor.execute`'s own contract for an unregistered id (`@jini-ai/daemon`'s
4
- * `tool-executor.ts`): a thrown `Error` reading `unknown tool "<id>"`, with no code or subclass to
5
- * match on instead. Duplicated from `tool-executor-audit.ts`'s identical `isUnknownToolError` rather
4
+ * `tool-executor.ts`): a thrown `ToolInputError` (from `@jini-ai/core`) reading `unknown tool "<id>"`.
5
+ * This still matches on the message text, not the class. Duplicated from `tool-executor-audit.ts`'s identical `isUnknownToolError` rather
6
6
  * than imported: that file is a sibling one layer up with no dependency on `mcp-federation`, and
7
7
  * importing across that line for an 18-character regex is not worth the coupling. Both copies carry
8
8
  * the same MEDIUM message-text-coupling note that file's own doc records — if `@jini-ai/daemon`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -139,13 +139,13 @@
139
139
  "@types/pg": "8.23.1",
140
140
  "express": "^4.21.0",
141
141
  "@types/express": "^4.17.21",
142
- "@jini-ai/db": "0.3.4",
143
- "@jini-ai/http-kit": "0.4.2",
144
- "@jini-ai/platform": "^0.5.7",
145
- "@jini-ai/core": "^0.4.6",
146
142
  "@jini-ai/agent-runtime": "^0.4.11",
147
- "@jini-ai/chat": "0.6.3",
148
- "@jini-ai/protocol": "^0.4.4"
143
+ "@jini-ai/chat": "0.6.5",
144
+ "@jini-ai/protocol": "^0.4.4",
145
+ "@jini-ai/core": "^0.4.6",
146
+ "@jini-ai/platform": "^0.5.8",
147
+ "@jini-ai/http-kit": "0.4.2",
148
+ "@jini-ai/db": "0.3.4"
149
149
  },
150
150
  "dependencies": {
151
151
  "@jini-ai/protocol": "^0.4.4",