@jini-ai/daemon 0.2.1 → 0.3.0
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/README.md +92 -0
- package/dist/agent-executor.d.ts +714 -42
- package/dist/agent-executor.d.ts.map +1 -1
- package/dist/agent-executor.js +1416 -351
- package/dist/agent-executor.js.map +1 -1
- package/dist/continuation/index.d.ts +1 -0
- package/dist/continuation/index.d.ts.map +1 -1
- package/dist/continuation/index.js +1 -0
- package/dist/continuation/index.js.map +1 -1
- package/dist/continuation/run-scoped-context-store.d.ts +79 -0
- package/dist/continuation/run-scoped-context-store.d.ts.map +1 -0
- package/dist/continuation/run-scoped-context-store.js +56 -0
- package/dist/continuation/run-scoped-context-store.js.map +1 -0
- package/dist/continuation/run-start-handler.d.ts +39 -9
- package/dist/continuation/run-start-handler.d.ts.map +1 -1
- package/dist/continuation/run-start-handler.js +12 -2
- package/dist/continuation/run-start-handler.js.map +1 -1
- package/dist/delegated-tool-bridge.d.ts +8 -0
- package/dist/delegated-tool-bridge.d.ts.map +1 -1
- package/dist/delegated-tool-bridge.js +117 -1
- package/dist/delegated-tool-bridge.js.map +1 -1
- package/dist/event-log.d.ts +18 -105
- package/dist/event-log.d.ts.map +1 -1
- package/dist/event-log.js +0 -17
- package/dist/event-log.js.map +1 -1
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +1 -1
- package/dist/frontend-session-registry.d.ts.map +1 -1
- package/dist/frontend-session-registry.js +26 -10
- package/dist/frontend-session-registry.js.map +1 -1
- package/dist/image-prompt-delivery.d.ts +56 -0
- package/dist/image-prompt-delivery.d.ts.map +1 -0
- package/dist/image-prompt-delivery.js +104 -0
- package/dist/image-prompt-delivery.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/remote-tool-bridge.d.ts +45 -0
- package/dist/remote-tool-bridge.d.ts.map +1 -0
- package/dist/remote-tool-bridge.js +23 -0
- package/dist/remote-tool-bridge.js.map +1 -0
- package/dist/routines/routine-store.js +1 -1
- package/dist/routines/schedule.d.ts +0 -8
- package/dist/routines/schedule.d.ts.map +1 -1
- package/dist/routines/schedule.js +71 -44
- package/dist/routines/schedule.js.map +1 -1
- package/dist/routines/scheduler.d.ts +43 -0
- package/dist/routines/scheduler.d.ts.map +1 -1
- package/dist/routines/scheduler.js +160 -120
- package/dist/routines/scheduler.js.map +1 -1
- package/dist/run/core/retry.d.ts.map +1 -1
- package/dist/run/core/retry.js +47 -35
- package/dist/run/core/retry.js.map +1 -1
- package/dist/run/diagnostics/diagnostics.d.ts.map +1 -1
- package/dist/run/diagnostics/diagnostics.js +149 -91
- package/dist/run/diagnostics/diagnostics.js.map +1 -1
- package/dist/run-lifecycle.d.ts +84 -4
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +378 -116
- package/dist/run-lifecycle.js.map +1 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.d.ts.map +1 -1
- package/dist/terminal-session.js +1 -1
- package/dist/tool-executor.d.ts +26 -6
- package/dist/tool-executor.d.ts.map +1 -1
- package/dist/tool-executor.js +220 -55
- package/dist/tool-executor.js.map +1 -1
- package/dist/tool-result-media.d.ts +79 -0
- package/dist/tool-result-media.d.ts.map +1 -0
- package/dist/tool-result-media.js +80 -0
- package/dist/tool-result-media.js.map +1 -0
- package/dist/tool-result-surfaces.d.ts +78 -0
- package/dist/tool-result-surfaces.d.ts.map +1 -0
- package/dist/tool-result-surfaces.js +92 -0
- package/dist/tool-result-surfaces.js.map +1 -0
- package/package.json +18 -9
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts UI-renderable media blocks from a tool handler's raw MCP-style content envelope
|
|
3
|
+
* (`{ content: [...] }`), for `delegated-tool-bridge.ts` and `agent-executor.ts` to attach to the
|
|
4
|
+
* `tool_result` wire event's new `media` field (`@jini-ai/protocol`'s `events.ts`).
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is not `tool-result-surfaces.ts`
|
|
7
|
+
*
|
|
8
|
+
* That module's whitelist governs a SECURITY property: which blocks may reach the model versus
|
|
9
|
+
* which are withheld for a human-only UI resource (MCP-UI, still deferred). This module answers a
|
|
10
|
+
* different question — "does this block carry displayable media a chat pane can render inline" —
|
|
11
|
+
* and is deliberately narrower: it recognizes exactly one block type today (`image`), and has no
|
|
12
|
+
* withhold-from-model concept of its own.
|
|
13
|
+
*
|
|
14
|
+
* The two DO interact, though not by pipelining: `delegated-tool-bridge.ts`'s `execute()` calls both
|
|
15
|
+
* of them against the SAME raw `executed.output`, not this module's `remainder` into
|
|
16
|
+
* `splitToolResultSurfaces`. That used to be pipelined — this module removed recognized image
|
|
17
|
+
* blocks from `content` before the surfaces pass ran, because `splitToolResultSurfaces`'s whitelist
|
|
18
|
+
* used to recognize only `text`, and an image block left in place would have been swept into
|
|
19
|
+
* `surfaces` and withheld from the model. Now that `image` is itself one of
|
|
20
|
+
* `tool-result-surfaces.ts`'s `MODEL_VISIBLE_BLOCK_TYPES` (a deliberate addition — see that
|
|
21
|
+
* constant's own doc), the surfaces pass classifies an image block correctly on its own, so there is
|
|
22
|
+
* no misclassification left to prevent by removing it first. This module's `remainder` field is
|
|
23
|
+
* therefore no longer read by that caller at all (kept for this module's own tests and any future
|
|
24
|
+
* caller that wants a de-imaged copy); only `media` is used, to populate the `tool_result` event's
|
|
25
|
+
* sibling `media` field for a UI that wants to render inline without re-parsing `content`.
|
|
26
|
+
*
|
|
27
|
+
* ## Why only `image`, and why blocks are silently skipped rather than reported
|
|
28
|
+
*
|
|
29
|
+
* `text` blocks already reach the model (and the UI, via `content`) through the existing flattened
|
|
30
|
+
* string — duplicating them into `media` would be redundant, not additive, so this module leaves
|
|
31
|
+
* them in `remainder` and never collects them. A block claiming `type: 'image'` without a string
|
|
32
|
+
* `mimeType`/`data` is dropped rather than surfaced as a malformed-block error: a handler's return
|
|
33
|
+
* value is untrusted input to this module (it may come from an external MCP server), and the
|
|
34
|
+
* contract is "render what is well-formed, ignore what isn't" — the same fail-quiet posture
|
|
35
|
+
* `serializeDelegatedToolOutput` already takes on a non-serializable output.
|
|
36
|
+
*/
|
|
37
|
+
/** One MCP-style text content block. Part of the recognized envelope shape but never collected —
|
|
38
|
+
* see this module's doc for why `text` blocks stay out of `media`. Exported so a caller narrowing
|
|
39
|
+
* `ToolResultMediaBlock.type` has both arms of the union in scope. */
|
|
40
|
+
export interface ToolResultTextBlock {
|
|
41
|
+
readonly type: 'text';
|
|
42
|
+
readonly text: string;
|
|
43
|
+
}
|
|
44
|
+
/** One MCP-style image content block — the one block type this slice proves end to end.
|
|
45
|
+
* `mimeType`/`data` mirror MCP's own `ImageContent` field names deliberately, so a block copied
|
|
46
|
+
* verbatim out of an MCP tool result already matches this shape with no field renaming. */
|
|
47
|
+
export interface ToolResultImageBlock {
|
|
48
|
+
readonly type: 'image';
|
|
49
|
+
/** IANA media type, e.g. `"image/png"`. */
|
|
50
|
+
readonly mimeType: string;
|
|
51
|
+
/** Base64-encoded image bytes. */
|
|
52
|
+
readonly data: string;
|
|
53
|
+
}
|
|
54
|
+
/** A tool result content block `media` may carry. Modeled on MCP's own content-block vocabulary —
|
|
55
|
+
* see this module's doc for why that vocabulary is reused rather than a fourth one invented. */
|
|
56
|
+
export type ToolResultMediaBlock = ToolResultTextBlock | ToolResultImageBlock;
|
|
57
|
+
export interface ExtractResultMedia {
|
|
58
|
+
/**
|
|
59
|
+
* `output` with every recognized image block removed from its `content` array. Identical
|
|
60
|
+
* reference to the input when nothing was extracted (not an envelope, no `content` array, or no
|
|
61
|
+
* well-formed `image` blocks) — mirrors `tool-result-surfaces.ts`'s `SplitToolResultSurfaces`
|
|
62
|
+
* same-reference contract for the common no-op case.
|
|
63
|
+
*/
|
|
64
|
+
readonly remainder: unknown;
|
|
65
|
+
/** Recognized image blocks, in arrival order. Empty when nothing was extracted. */
|
|
66
|
+
readonly media: readonly ToolResultMediaBlock[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits a tool handler's raw return value into its recognized media blocks and the remainder,
|
|
70
|
+
* ready to feed onward into `splitToolResultSurfaces` (see this module's doc for why that order
|
|
71
|
+
* matters).
|
|
72
|
+
*
|
|
73
|
+
* @param output - A tool handler's raw return value (`ToolExecutionResult.output` when
|
|
74
|
+
* `status === 'completed'`). Non-envelope values (the overwhelming majority of tools, which return
|
|
75
|
+
* plain JSON) pass through untouched with no media.
|
|
76
|
+
* @complexity O(n) in the number of content blocks.
|
|
77
|
+
*/
|
|
78
|
+
export declare function extractResultMedia(output: unknown): ExtractResultMedia;
|
|
79
|
+
//# sourceMappingURL=tool-result-media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-media.d.ts","sourceRoot":"","sources":["../src/tool-result-media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;;uEAEuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;4FAE4F;AAC5F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;iGACiG;AACjG,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,oBAAoB,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACjD;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,kBAAkB,CAetE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts UI-renderable media blocks from a tool handler's raw MCP-style content envelope
|
|
3
|
+
* (`{ content: [...] }`), for `delegated-tool-bridge.ts` and `agent-executor.ts` to attach to the
|
|
4
|
+
* `tool_result` wire event's new `media` field (`@jini-ai/protocol`'s `events.ts`).
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is not `tool-result-surfaces.ts`
|
|
7
|
+
*
|
|
8
|
+
* That module's whitelist governs a SECURITY property: which blocks may reach the model versus
|
|
9
|
+
* which are withheld for a human-only UI resource (MCP-UI, still deferred). This module answers a
|
|
10
|
+
* different question — "does this block carry displayable media a chat pane can render inline" —
|
|
11
|
+
* and is deliberately narrower: it recognizes exactly one block type today (`image`), and has no
|
|
12
|
+
* withhold-from-model concept of its own.
|
|
13
|
+
*
|
|
14
|
+
* The two DO interact, though not by pipelining: `delegated-tool-bridge.ts`'s `execute()` calls both
|
|
15
|
+
* of them against the SAME raw `executed.output`, not this module's `remainder` into
|
|
16
|
+
* `splitToolResultSurfaces`. That used to be pipelined — this module removed recognized image
|
|
17
|
+
* blocks from `content` before the surfaces pass ran, because `splitToolResultSurfaces`'s whitelist
|
|
18
|
+
* used to recognize only `text`, and an image block left in place would have been swept into
|
|
19
|
+
* `surfaces` and withheld from the model. Now that `image` is itself one of
|
|
20
|
+
* `tool-result-surfaces.ts`'s `MODEL_VISIBLE_BLOCK_TYPES` (a deliberate addition — see that
|
|
21
|
+
* constant's own doc), the surfaces pass classifies an image block correctly on its own, so there is
|
|
22
|
+
* no misclassification left to prevent by removing it first. This module's `remainder` field is
|
|
23
|
+
* therefore no longer read by that caller at all (kept for this module's own tests and any future
|
|
24
|
+
* caller that wants a de-imaged copy); only `media` is used, to populate the `tool_result` event's
|
|
25
|
+
* sibling `media` field for a UI that wants to render inline without re-parsing `content`.
|
|
26
|
+
*
|
|
27
|
+
* ## Why only `image`, and why blocks are silently skipped rather than reported
|
|
28
|
+
*
|
|
29
|
+
* `text` blocks already reach the model (and the UI, via `content`) through the existing flattened
|
|
30
|
+
* string — duplicating them into `media` would be redundant, not additive, so this module leaves
|
|
31
|
+
* them in `remainder` and never collects them. A block claiming `type: 'image'` without a string
|
|
32
|
+
* `mimeType`/`data` is dropped rather than surfaced as a malformed-block error: a handler's return
|
|
33
|
+
* value is untrusted input to this module (it may come from an external MCP server), and the
|
|
34
|
+
* contract is "render what is well-formed, ignore what isn't" — the same fail-quiet posture
|
|
35
|
+
* `serializeDelegatedToolOutput` already takes on a non-serializable output.
|
|
36
|
+
*/
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
/** Narrows one loosely-typed content-array entry to a well-formed {@link ToolResultImageBlock}, or
|
|
41
|
+
* `undefined` for anything else (wrong `type`, or a `type: 'image'` block missing/mistyping
|
|
42
|
+
* `mimeType`/`data`). */
|
|
43
|
+
function asImageBlock(block) {
|
|
44
|
+
if (!isRecord(block) || block['type'] !== 'image')
|
|
45
|
+
return undefined;
|
|
46
|
+
const { mimeType, data } = block;
|
|
47
|
+
if (typeof mimeType !== 'string' || typeof data !== 'string')
|
|
48
|
+
return undefined;
|
|
49
|
+
return { type: 'image', mimeType, data };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Splits a tool handler's raw return value into its recognized media blocks and the remainder,
|
|
53
|
+
* ready to feed onward into `splitToolResultSurfaces` (see this module's doc for why that order
|
|
54
|
+
* matters).
|
|
55
|
+
*
|
|
56
|
+
* @param output - A tool handler's raw return value (`ToolExecutionResult.output` when
|
|
57
|
+
* `status === 'completed'`). Non-envelope values (the overwhelming majority of tools, which return
|
|
58
|
+
* plain JSON) pass through untouched with no media.
|
|
59
|
+
* @complexity O(n) in the number of content blocks.
|
|
60
|
+
*/
|
|
61
|
+
export function extractResultMedia(output) {
|
|
62
|
+
if (!isRecord(output))
|
|
63
|
+
return { remainder: output, media: [] };
|
|
64
|
+
const content = output['content'];
|
|
65
|
+
if (!Array.isArray(content))
|
|
66
|
+
return { remainder: output, media: [] };
|
|
67
|
+
const media = [];
|
|
68
|
+
const kept = [];
|
|
69
|
+
for (const entry of content) {
|
|
70
|
+
const image = asImageBlock(entry);
|
|
71
|
+
if (image)
|
|
72
|
+
media.push(image);
|
|
73
|
+
else
|
|
74
|
+
kept.push(entry);
|
|
75
|
+
}
|
|
76
|
+
if (media.length === 0)
|
|
77
|
+
return { remainder: output, media: [] };
|
|
78
|
+
return { remainder: { ...output, content: kept }, media };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=tool-result-media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-media.js","sourceRoot":"","sources":["../src/tool-result-media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAqCH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;0BAE0B;AAC1B,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IACpE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC/E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAe;IAChD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAErE,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;;YACxB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAChE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits a tool handler's return value into the part the MODEL may see and the parts only a HUMAN
|
|
3
|
+
* may see.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* MCP's own model is that a tool result carries content blocks and the *host* decides which blocks
|
|
8
|
+
* reach the model and which are rendered for the person. Keeping them in one payload is the
|
|
9
|
+
* protocol; separating them is the host's job. Jini's daemon is the only actor standing in that
|
|
10
|
+
* position — the spawned coding-agent CLI is not an MCP-UI host, and `okResult()`
|
|
11
|
+
* (`@jini-ai/mcp`'s `tool-protocol.ts`) `JSON.stringify`s whatever it is handed into a single text
|
|
12
|
+
* block. So a UI resource left in a handler's return value becomes ordinary model-visible context.
|
|
13
|
+
*
|
|
14
|
+
* That is not hypothetical. A host's `content_post_delete` tool returned
|
|
15
|
+
* `{ content: [textBlock, uiResource] }` where the resource's inline `<script>` held a single-use
|
|
16
|
+
* confirmation token, and the tool's own description told the model the token "is never shown to
|
|
17
|
+
* you". The model could read it out of its own tool result and approve its own deletion. The split
|
|
18
|
+
* this module performs is what makes that structurally impossible.
|
|
19
|
+
*
|
|
20
|
+
* ## Whitelist, not blacklist — the security boundary
|
|
21
|
+
*
|
|
22
|
+
* {@link splitToolResultSurfaces} does NOT look for known UI shapes and strip them. It keeps only
|
|
23
|
+
* block types explicitly known to be model-safe (`text`) and routes **everything else** to the
|
|
24
|
+
* human channel, including block types that did not exist when this was written.
|
|
25
|
+
*
|
|
26
|
+
* The direction of that default is the whole point. A blacklist fails OPEN: the first time the
|
|
27
|
+
* MCP-UI spec adds a block type this file has never heard of, an unrecognised block sails through
|
|
28
|
+
* into model context — exactly the leak being fixed, reintroduced silently. A whitelist fails
|
|
29
|
+
* CLOSED: an unrecognised block is withheld from the model and rendered for the human, so the worst
|
|
30
|
+
* case is a surface that renders oddly rather than a secret that escapes.
|
|
31
|
+
*
|
|
32
|
+
* **Do not "improve" this by enumerating UI types to remove.** If a new block type is genuinely
|
|
33
|
+
* model-safe, add it to {@link MODEL_VISIBLE_BLOCK_TYPES} deliberately, with a reason.
|
|
34
|
+
*
|
|
35
|
+
* ## What is deliberately left alone
|
|
36
|
+
*
|
|
37
|
+
* A return value that is not an MCP content envelope (`{ content: [...] }`) passes through
|
|
38
|
+
* untouched. The overwhelming majority of registered tools return plain JSON — a list of posts, an
|
|
39
|
+
* id, a count — and are not making a model-vs-human distinction at all. Rewriting those would be a
|
|
40
|
+
* behavioural change to every tool in the registry in service of a problem they do not have.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Block `type` values a tool result may show the model.
|
|
44
|
+
*
|
|
45
|
+
* `text` and `image`, deliberately so — see this module's header before adding to it. `image` was
|
|
46
|
+
* added rather than left to the fail-closed default because it is genuinely model-safe in a way an
|
|
47
|
+
* MCP-UI resource is not: `ImageContent` is MCP's own vision-input mechanism, defined exactly so a
|
|
48
|
+
* tool result can put picture bytes in front of the model (`assistant_demo_image`'s whole reason to
|
|
49
|
+
* exist, in Tovu, is proving that round trip). That is a different property from "renders a UI a
|
|
50
|
+
* human clicks" — an image block carries no interactive surface and no host-side secret the way an
|
|
51
|
+
* embedded confirmation dialog can, so withholding it here would not close a leak, it would just
|
|
52
|
+
* break the feature. Anything else absent from this list is still treated as human-only.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MODEL_VISIBLE_BLOCK_TYPES: readonly string[];
|
|
55
|
+
/** One entry of an MCP tool result's `content` array. Structure beyond `type` is not this module's business. */
|
|
56
|
+
export type ToolResultBlock = Record<string, unknown>;
|
|
57
|
+
export interface SplitToolResultSurfaces {
|
|
58
|
+
/**
|
|
59
|
+
* What may be returned to the caller and therefore reach the model. Identical to the input
|
|
60
|
+
* (same reference) when the value is not an MCP content envelope.
|
|
61
|
+
*/
|
|
62
|
+
readonly modelOutput: unknown;
|
|
63
|
+
/**
|
|
64
|
+
* Blocks withheld from the model, in arrival order, for the host to render to the human. Empty
|
|
65
|
+
* when nothing was withheld — the common case.
|
|
66
|
+
*/
|
|
67
|
+
readonly surfaces: readonly ToolResultBlock[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Partitions an MCP content envelope into model-visible and human-only halves.
|
|
71
|
+
*
|
|
72
|
+
* @param output - A tool handler's raw return value.
|
|
73
|
+
* @returns `modelOutput` (safe to return to the agent) and `surfaces` (human-only blocks). A
|
|
74
|
+
* non-envelope value is passed through by reference with no surfaces.
|
|
75
|
+
* @complexity O(n) in the number of content blocks.
|
|
76
|
+
*/
|
|
77
|
+
export declare function splitToolResultSurfaces(output: unknown): SplitToolResultSurfaces;
|
|
78
|
+
//# sourceMappingURL=tool-result-surfaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-surfaces.d.ts","sourceRoot":"","sources":["../src/tool-result-surfaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAE9E,gHAAgH;AAChH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAMD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,CA2BhF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits a tool handler's return value into the part the MODEL may see and the parts only a HUMAN
|
|
3
|
+
* may see.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* MCP's own model is that a tool result carries content blocks and the *host* decides which blocks
|
|
8
|
+
* reach the model and which are rendered for the person. Keeping them in one payload is the
|
|
9
|
+
* protocol; separating them is the host's job. Jini's daemon is the only actor standing in that
|
|
10
|
+
* position — the spawned coding-agent CLI is not an MCP-UI host, and `okResult()`
|
|
11
|
+
* (`@jini-ai/mcp`'s `tool-protocol.ts`) `JSON.stringify`s whatever it is handed into a single text
|
|
12
|
+
* block. So a UI resource left in a handler's return value becomes ordinary model-visible context.
|
|
13
|
+
*
|
|
14
|
+
* That is not hypothetical. A host's `content_post_delete` tool returned
|
|
15
|
+
* `{ content: [textBlock, uiResource] }` where the resource's inline `<script>` held a single-use
|
|
16
|
+
* confirmation token, and the tool's own description told the model the token "is never shown to
|
|
17
|
+
* you". The model could read it out of its own tool result and approve its own deletion. The split
|
|
18
|
+
* this module performs is what makes that structurally impossible.
|
|
19
|
+
*
|
|
20
|
+
* ## Whitelist, not blacklist — the security boundary
|
|
21
|
+
*
|
|
22
|
+
* {@link splitToolResultSurfaces} does NOT look for known UI shapes and strip them. It keeps only
|
|
23
|
+
* block types explicitly known to be model-safe (`text`) and routes **everything else** to the
|
|
24
|
+
* human channel, including block types that did not exist when this was written.
|
|
25
|
+
*
|
|
26
|
+
* The direction of that default is the whole point. A blacklist fails OPEN: the first time the
|
|
27
|
+
* MCP-UI spec adds a block type this file has never heard of, an unrecognised block sails through
|
|
28
|
+
* into model context — exactly the leak being fixed, reintroduced silently. A whitelist fails
|
|
29
|
+
* CLOSED: an unrecognised block is withheld from the model and rendered for the human, so the worst
|
|
30
|
+
* case is a surface that renders oddly rather than a secret that escapes.
|
|
31
|
+
*
|
|
32
|
+
* **Do not "improve" this by enumerating UI types to remove.** If a new block type is genuinely
|
|
33
|
+
* model-safe, add it to {@link MODEL_VISIBLE_BLOCK_TYPES} deliberately, with a reason.
|
|
34
|
+
*
|
|
35
|
+
* ## What is deliberately left alone
|
|
36
|
+
*
|
|
37
|
+
* A return value that is not an MCP content envelope (`{ content: [...] }`) passes through
|
|
38
|
+
* untouched. The overwhelming majority of registered tools return plain JSON — a list of posts, an
|
|
39
|
+
* id, a count — and are not making a model-vs-human distinction at all. Rewriting those would be a
|
|
40
|
+
* behavioural change to every tool in the registry in service of a problem they do not have.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Block `type` values a tool result may show the model.
|
|
44
|
+
*
|
|
45
|
+
* `text` and `image`, deliberately so — see this module's header before adding to it. `image` was
|
|
46
|
+
* added rather than left to the fail-closed default because it is genuinely model-safe in a way an
|
|
47
|
+
* MCP-UI resource is not: `ImageContent` is MCP's own vision-input mechanism, defined exactly so a
|
|
48
|
+
* tool result can put picture bytes in front of the model (`assistant_demo_image`'s whole reason to
|
|
49
|
+
* exist, in Tovu, is proving that round trip). That is a different property from "renders a UI a
|
|
50
|
+
* human clicks" — an image block carries no interactive surface and no host-side secret the way an
|
|
51
|
+
* embedded confirmation dialog can, so withholding it here would not close a leak, it would just
|
|
52
|
+
* break the feature. Anything else absent from this list is still treated as human-only.
|
|
53
|
+
*/
|
|
54
|
+
export const MODEL_VISIBLE_BLOCK_TYPES = ['text', 'image'];
|
|
55
|
+
function isRecord(value) {
|
|
56
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Partitions an MCP content envelope into model-visible and human-only halves.
|
|
60
|
+
*
|
|
61
|
+
* @param output - A tool handler's raw return value.
|
|
62
|
+
* @returns `modelOutput` (safe to return to the agent) and `surfaces` (human-only blocks). A
|
|
63
|
+
* non-envelope value is passed through by reference with no surfaces.
|
|
64
|
+
* @complexity O(n) in the number of content blocks.
|
|
65
|
+
*/
|
|
66
|
+
export function splitToolResultSurfaces(output) {
|
|
67
|
+
if (!isRecord(output))
|
|
68
|
+
return { modelOutput: output, surfaces: [] };
|
|
69
|
+
const content = output['content'];
|
|
70
|
+
if (!Array.isArray(content))
|
|
71
|
+
return { modelOutput: output, surfaces: [] };
|
|
72
|
+
const modelBlocks = [];
|
|
73
|
+
const surfaces = [];
|
|
74
|
+
for (const block of content) {
|
|
75
|
+
// A non-record block carries no `type` to check, so it cannot be shown to be model-safe.
|
|
76
|
+
// Withheld rather than passed through, per this module's fail-closed rule.
|
|
77
|
+
if (!isRecord(block)) {
|
|
78
|
+
surfaces.push({ type: 'unknown', value: block });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const type = block['type'];
|
|
82
|
+
if (typeof type === 'string' && MODEL_VISIBLE_BLOCK_TYPES.includes(type)) {
|
|
83
|
+
modelBlocks.push(block);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
surfaces.push(block);
|
|
87
|
+
}
|
|
88
|
+
if (surfaces.length === 0)
|
|
89
|
+
return { modelOutput: output, surfaces: [] };
|
|
90
|
+
return { modelOutput: { ...output, content: modelBlocks }, surfaces };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=tool-result-surfaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-surfaces.js","sourceRoot":"","sources":["../src/tool-result-surfaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAkB9E,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAe;IACrD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAEpE,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAE1E,MAAM,WAAW,GAAc,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAsB,EAAE,CAAC;IAEvC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,yFAAyF;QACzF,2EAA2E;QAC3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACjD,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,yBAAyB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACzE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAExE,OAAO,EAAE,WAAW,EAAE,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,CAAC;AACxE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
|
|
4
5
|
"license": "Apache-2.0",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
@@ -10,10 +11,10 @@
|
|
|
10
11
|
"jini": {
|
|
11
12
|
"domain": "server",
|
|
12
13
|
"kind": "runtime",
|
|
13
|
-
"runtime": "node"
|
|
14
|
-
"admission": "locked"
|
|
14
|
+
"runtime": "node"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
17
18
|
"main": "./dist/index.js",
|
|
18
19
|
"types": "./dist/index.d.ts",
|
|
19
20
|
"exports": {
|
|
@@ -36,14 +37,22 @@
|
|
|
36
37
|
"registry": "https://registry.npmjs.org"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"
|
|
40
|
-
"@jini-ai/
|
|
41
|
-
"@jini-ai/
|
|
42
|
-
"@jini-ai/
|
|
43
|
-
"@jini-ai/protocol": "0.1.2"
|
|
40
|
+
"@jini-ai/core": "0.3.0",
|
|
41
|
+
"@jini-ai/platform": "0.3.0",
|
|
42
|
+
"@jini-ai/agent-runtime": "0.3.0",
|
|
43
|
+
"@jini-ai/protocol": "0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@vitest/coverage-v8": "^2.1.9"
|
|
46
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
47
|
+
"node-pty": "^1.1.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"node-pty": "^1.1.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"node-pty": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
47
56
|
},
|
|
48
57
|
"scripts": {
|
|
49
58
|
"build": "tsc -p tsconfig.json",
|