@jini-ai/daemon 0.6.7 → 0.6.8

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.8 — 2026-10-16
2
+
3
+ - An unknown tool on the read-only gateway points to discovery, never a mutation 409 (75d37c32).
4
+
1
5
  ## 0.6.7 — 2026-10-15
2
6
 
3
7
  - 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.
@@ -25,8 +25,8 @@ export interface ReadOnlyConstrainedPrincipal extends Principal {
25
25
  readonly toolAccess: 'read-only';
26
26
  }
27
27
  /**
28
- * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
29
- * one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
28
+ * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
29
+ * refuse unsafe dispatch; unknown ids direct discovery because an unrestricted retry cannot run them.
30
30
  * Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
31
31
  */
32
32
  export declare const defaultDaemonMessages: {
@@ -35,6 +35,10 @@ export declare const defaultDaemonMessages: {
35
35
  toolRefusalMessage: ({ toolId }: {
36
36
  toolId: string;
37
37
  }) => string;
38
+ unknownToolMessage: ({ toolId, candidates }: {
39
+ toolId: string;
40
+ candidates: readonly string[];
41
+ }) => string;
38
42
  };
39
43
  };
40
44
  /** Required host wording; absent metadata is always a refusal. */
@@ -43,6 +47,10 @@ export interface ReadOnlyToolMessages {
43
47
  toolRefusalMessage(required: {
44
48
  toolId: string;
45
49
  }): string;
50
+ unknownToolMessage(required: {
51
+ toolId: string;
52
+ candidates: readonly string[];
53
+ }): string;
46
54
  }
47
55
  export interface ReadOnlyToolCheckRequired {
48
56
  readonly toolId: string;
@@ -63,7 +71,7 @@ export declare function constrainPrincipalToReadOnlyTools({ principal }: {
63
71
  export declare function principalIsReadOnlyConstrained({ principal }: {
64
72
  principal: Principal;
65
73
  }, _optional?: Record<string, never>): boolean;
66
- /** @returns Required host wording for an unknown or non-read-only tool. */
74
+ /** @returns Required host wording for a registered non-read-only tool. */
67
75
  export declare function readOnlyToolRefusalMessage({ toolId, messages }: {
68
76
  toolId: string;
69
77
  messages: ReadOnlyToolMessages;
@@ -1,13 +1,15 @@
1
1
  import { isReadOnlyTool } from '@jini-ai/core';
2
+ import { toolCandidates } from './tool-candidates.js';
2
3
  /**
3
- * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes share
4
- * one refusal: neither corroborates a safe read, and catalog discovery already reveals existence.
4
+ * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
5
+ * refuse unsafe dispatch; unknown ids direct discovery because an unrestricted retry cannot run them.
5
6
  * Missing verification metadata always refuses, naming the missing wiring so the host can fix it.
6
7
  */
7
8
  export const defaultDaemonMessages = {
8
9
  readOnly: {
9
10
  unverifiableMessage: 'this host cannot verify read-only tools — POST /api/delegated-tool-calls was mounted without DelegatedToolsHttpDeps.toolRegistry, so a requireReadOnly call cannot be checked and is refused',
10
11
  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
+ unknownToolMessage: ({ toolId, candidates }) => `no tool "${toolId}" is registered; use search_tools${candidates.length > 0 ? ` — did you mean ${candidates.join(', ')}?` : ''}`,
11
13
  },
12
14
  };
13
15
  /** @returns A copied, attenuated principal; its roles are unchanged. */
@@ -20,7 +22,7 @@ export function constrainPrincipalToReadOnlyTools({ principal }, _optional = {})
20
22
  export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
21
23
  return 'toolAccess' in principal && principal.toolAccess === 'read-only';
22
24
  }
23
- /** @returns Required host wording for an unknown or non-read-only tool. */
25
+ /** @returns Required host wording for a registered non-read-only tool. */
24
26
  export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
25
27
  return messages.toolRefusalMessage({ toolId });
26
28
  }
@@ -34,7 +36,10 @@ export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {})
34
36
  export function checkReadOnlyTool({ toolId, registry, messages }, _optional = {}) {
35
37
  if (registry === undefined)
36
38
  return messages.unverifiableMessage;
37
- const descriptor = registry.list({}).find((candidate) => candidate.id === toolId);
39
+ const tools = registry.list({});
40
+ const descriptor = tools.find((candidate) => candidate.id === toolId);
41
+ if (!descriptor)
42
+ return messages.unknownToolMessage({ toolId, candidates: toolCandidates({ toolId, tools }) });
38
43
  if (isReadOnlyTool({ descriptor }))
39
44
  return null;
40
45
  return readOnlyToolRefusalMessage({ toolId, messages });
@@ -0,0 +1,8 @@
1
+ import type { ToolDescriptor } from '@jini-ai/core';
2
+ /** Registered prefix matches for unknown-tool diagnostics, in catalog order, capped at eight.
3
+ * @complexity O(n) time and space over n registered tools. */
4
+ export declare function toolCandidates({ toolId, tools }: {
5
+ toolId: string;
6
+ tools: readonly Pick<ToolDescriptor, 'id'>[];
7
+ }, _optional?: Record<string, never>): string[];
8
+ //# sourceMappingURL=tool-candidates.d.ts.map
@@ -0,0 +1,6 @@
1
+ /** Registered prefix matches for unknown-tool diagnostics, in catalog order, capped at eight.
2
+ * @complexity O(n) time and space over n registered tools. */
3
+ export function toolCandidates({ toolId, tools }, _optional = {}) {
4
+ return tools.map((tool) => tool.id).filter((id) => id.startsWith(toolId)).slice(0, 8);
5
+ }
6
+ //# sourceMappingURL=tool-candidates.js.map
@@ -48,6 +48,7 @@
48
48
  import { randomUUID } from 'node:crypto';
49
49
  import { ToolInputError, } from '@jini-ai/core';
50
50
  import { authorizeToolInvocation } from '@jini-ai/core/composition';
51
+ import { toolCandidates } from './core/tool-candidates.js';
51
52
  /**
52
53
  * Enforces `maxOutputBytes` in BYTES, which is what the option is named for.
53
54
  *
@@ -182,7 +183,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
182
183
  // existence, never returns a handler, so this doesn't reopen the leak authorizeToolInvocation
183
184
  // closed — that gate still runs, still unconditionally, inside the call below.
184
185
  if (!registry.has({ toolId })) {
185
- const candidates = registry.list({}).map((tool) => tool.id).filter((id) => id.startsWith(toolId)).slice(0, 8);
186
+ const candidates = toolCandidates({ toolId, tools: registry.list({}) });
186
187
  const hint = candidates.length > 0 ? `; did you mean ${candidates.join(', ')}?` : '';
187
188
  throw new ToolInputError({ message: `ToolExecutor: unknown tool "${toolId}"${hint}` });
188
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
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,17 +139,17 @@
139
139
  "@types/pg": "8.23.1",
140
140
  "express": "^4.21.0",
141
141
  "@types/express": "^4.17.21",
142
- "@jini-ai/agent-runtime": "^0.4.11",
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",
142
+ "@jini-ai/db": "0.3.5",
147
143
  "@jini-ai/http-kit": "0.4.2",
148
- "@jini-ai/db": "0.3.4"
144
+ "@jini-ai/agent-runtime": "^0.4.11",
145
+ "@jini-ai/chat": "0.6.6",
146
+ "@jini-ai/platform": "^0.5.9",
147
+ "@jini-ai/core": "^0.4.7",
148
+ "@jini-ai/protocol": "^0.4.4"
149
149
  },
150
150
  "dependencies": {
151
- "@jini-ai/protocol": "^0.4.4",
152
- "@jini-ai/core": "^0.4.6"
151
+ "@jini-ai/core": "^0.4.7",
152
+ "@jini-ai/protocol": "^0.4.4"
153
153
  },
154
154
  "peerDependencies": {
155
155
  "node-pty": "^1.1.0",