@jini-ai/daemon 0.6.22 → 0.6.23
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 +4 -0
- package/dist/agent-executor/codex/home-config.d.ts +5 -3
- package/dist/agent-executor/codex/home-config.js +8 -7
- package/dist/agent-executor/codex/home-staging.js +5 -5
- package/dist/agent-executor/contracts.d.ts +4 -0
- package/dist/http/frontend-control.d.ts +4 -0
- package/dist/http/frontend-control.js +2 -1
- package/dist/http/frontend-request-route.d.ts +27 -0
- package/dist/http/frontend-request-route.js +69 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +1 -0
- package/dist/http/tool-execution-routes.d.ts +14 -2
- package/dist/http/tool-execution-routes.js +9 -2
- package/dist/session-coordination.d.ts +9 -2
- package/dist/session-coordination.js +8 -3
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpHttpEndpoint, McpJsonServerEntry } from '../contracts.js';
|
|
1
|
+
import type { McpHttpEndpoint, McpJsonServerEntry, McpJsonInjectionOptions } from '../contracts.js';
|
|
2
2
|
/** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
|
|
3
3
|
export declare function tomlKey(key: string): string;
|
|
4
4
|
/**
|
|
@@ -16,8 +16,9 @@ export declare function tomlKey(key: string): string;
|
|
|
16
16
|
* @complexity O(n) in the number of argv/env entries.
|
|
17
17
|
* @overallScore 100/100
|
|
18
18
|
*/
|
|
19
|
-
export declare function buildCodexMcpServerToml({ entry }: {
|
|
19
|
+
export declare function buildCodexMcpServerToml({ entry, toolApprovalOwner }: {
|
|
20
20
|
readonly entry: McpJsonServerEntry | Pick<McpHttpEndpoint, 'url' | 'token'>;
|
|
21
|
+
readonly toolApprovalOwner?: McpJsonInjectionOptions['toolApprovalOwner'];
|
|
21
22
|
}, _options?: {}): string;
|
|
22
23
|
/**
|
|
23
24
|
* Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
|
|
@@ -42,10 +43,11 @@ export declare function buildCodexMcpServerToml({ entry }: {
|
|
|
42
43
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
43
44
|
* @complexity O(n) in the existing config's length.
|
|
44
45
|
*/
|
|
45
|
-
export declare function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings }: {
|
|
46
|
+
export declare function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings, toolApprovalOwner }: {
|
|
46
47
|
readonly existingRaw: string | undefined;
|
|
47
48
|
readonly entry: McpJsonServerEntry | Pick<McpHttpEndpoint, 'url' | 'token'>;
|
|
48
49
|
readonly inheritUserMcp?: boolean;
|
|
49
50
|
readonly inheritUserSettings?: boolean;
|
|
51
|
+
readonly toolApprovalOwner?: McpJsonInjectionOptions['toolApprovalOwner'];
|
|
50
52
|
}, _optional?: Record<string, never>): string;
|
|
51
53
|
//# sourceMappingURL=home-config.d.ts.map
|
|
@@ -56,12 +56,13 @@ const CODEX_TOOL_TIMEOUT_SEC = 6 * 60 + 40;
|
|
|
56
56
|
* @complexity O(n) in the number of argv/env entries.
|
|
57
57
|
* @overallScore 100/100
|
|
58
58
|
*/
|
|
59
|
-
export function buildCodexMcpServerToml({ entry }, _options = {}) {
|
|
59
|
+
export function buildCodexMcpServerToml({ entry, toolApprovalOwner }, _options = {}) {
|
|
60
|
+
const approval = toolApprovalOwner === 'host' ? 'default_tools_approval_mode = "approve"\n' : '';
|
|
60
61
|
if ('url' in entry)
|
|
61
|
-
return buildCodexHttpMcpServerToml({ endpoint: entry });
|
|
62
|
+
return buildCodexHttpMcpServerToml({ endpoint: entry }, { approval });
|
|
62
63
|
const argsLiteral = entry.args.map(tomlString).join(', ');
|
|
63
64
|
const serverTable = `[mcp_servers.${JINI_MCP_SERVER_KEY}]\ncommand = ${tomlString(entry.command)}\nargs = [${argsLiteral}]\n` +
|
|
64
|
-
`tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n`;
|
|
65
|
+
`tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n${approval}`;
|
|
65
66
|
const envLines = Object.entries(entry.env)
|
|
66
67
|
.filter((pair) => typeof pair[1] === 'string')
|
|
67
68
|
.map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`);
|
|
@@ -73,9 +74,9 @@ export function buildCodexMcpServerToml({ entry }, _options = {}) {
|
|
|
73
74
|
* @returns One HTTP table using the existing timeout and TOML escaping owner.
|
|
74
75
|
* @complexity O(endpoint bytes), without I/O.
|
|
75
76
|
*/
|
|
76
|
-
function buildCodexHttpMcpServerToml({ endpoint },
|
|
77
|
+
function buildCodexHttpMcpServerToml({ endpoint }, { approval = '' } = {}) {
|
|
77
78
|
return `[mcp_servers.${JINI_MCP_SERVER_KEY}]\nurl = ${tomlString(endpoint.url)}\n` +
|
|
78
|
-
`tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n\n[mcp_servers.${JINI_MCP_SERVER_KEY}.http_headers]\n` +
|
|
79
|
+
`tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n${approval}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.http_headers]\n` +
|
|
79
80
|
`Authorization = ${tomlString(`Bearer ${endpoint.token}`)}\n`;
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
@@ -213,10 +214,10 @@ function stripExistingJiniMcpServerTable(existingRaw, optional = {}) {
|
|
|
213
214
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
214
215
|
* @complexity O(n) in the existing config's length.
|
|
215
216
|
*/
|
|
216
|
-
export function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp = true, inheritUserSettings = true }, _optional = {}) {
|
|
217
|
+
export function buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp = true, inheritUserSettings = true, toolApprovalOwner }, _optional = {}) {
|
|
217
218
|
const base = stripExistingJiniMcpServerTable(existingRaw ?? '', { allServers: !inheritUserMcp, mcpOnly: !inheritUserSettings });
|
|
218
219
|
const blockBreak = base.endsWith('\n') ? '\n' : '\n\n';
|
|
219
220
|
const separator = base.length === 0 ? '' : blockBreak;
|
|
220
|
-
return `${base}${separator}${buildCodexMcpServerToml({ entry
|
|
221
|
+
return `${base}${separator}${buildCodexMcpServerToml({ entry, toolApprovalOwner })}`;
|
|
221
222
|
}
|
|
222
223
|
//# sourceMappingURL=home-config.js.map
|
|
@@ -75,8 +75,8 @@ function resolveCodexHomeSeams(options) {
|
|
|
75
75
|
* {@link prepareCodexHomeIfNeeded} stays the backstop for every blocked case.
|
|
76
76
|
* @throws {@link CodexMcpIsolationError} when the run can't be isolated.
|
|
77
77
|
*/
|
|
78
|
-
function buildStagedCodexConfig({ existingRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome }) {
|
|
79
|
-
const build = () => buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings: inheritUserConfig });
|
|
78
|
+
function buildStagedCodexConfig({ existingRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome, toolApprovalOwner }) {
|
|
79
|
+
const build = () => buildCodexHomeConfigToml({ existingRaw, entry, inheritUserMcp, inheritUserSettings: inheritUserConfig, toolApprovalOwner });
|
|
80
80
|
if (inheritUserMcp)
|
|
81
81
|
return build();
|
|
82
82
|
const path = join(sourceHome, 'config.toml');
|
|
@@ -115,7 +115,7 @@ function buildStagedCodexConfig({ existingRaw, entry, inheritUserConfig, inherit
|
|
|
115
115
|
* @complexity O(h) filesystem operations for h top-level user-home entries; one staged config write.
|
|
116
116
|
* @overallScore 100/100
|
|
117
117
|
*/
|
|
118
|
-
async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams, inheritUserConfig = true, inheritUserMcp = true) {
|
|
118
|
+
async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams, inheritUserConfig = true, inheritUserMcp = true, toolApprovalOwner) {
|
|
119
119
|
// Stricter than `@jini-ai/agent-runtime`'s `prepareAgentLogFile`/`preparePromptFileForAgent`
|
|
120
120
|
// labels (which keep dots): this prefix stages a directory that ends up holding a copied Codex
|
|
121
121
|
// login credential, so it gets `mcpJsonPathForRun`'s tighter discipline instead — dots stripped
|
|
@@ -133,7 +133,7 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams, i
|
|
|
133
133
|
// matching writeMcpJsonForRun's identical "missing file" handling.
|
|
134
134
|
existingConfigRaw = undefined;
|
|
135
135
|
}
|
|
136
|
-
const content = buildStagedCodexConfig({ existingRaw: existingConfigRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome: sourceCodexHomeDir });
|
|
136
|
+
const content = buildStagedCodexConfig({ existingRaw: existingConfigRaw, entry, inheritUserConfig, inheritUserMcp, sourceHome: sourceCodexHomeDir, toolApprovalOwner });
|
|
137
137
|
await seams.writeFile({ path: join(dir, 'config.toml'), content });
|
|
138
138
|
await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, { inheritUserConfig });
|
|
139
139
|
}
|
|
@@ -183,7 +183,7 @@ export async function prepareCodexHomeIfNeeded({ input, deps }) {
|
|
|
183
183
|
}
|
|
184
184
|
const inherit = codexInheritance(input);
|
|
185
185
|
try {
|
|
186
|
-
return await prepareCodexHomeForRun(input.runId, entry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), inherit.settings, inherit.mcp);
|
|
186
|
+
return await prepareCodexHomeForRun(input.runId, entry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection), inherit.settings, inherit.mcp, deps.mcpJsonInjection.toolApprovalOwner);
|
|
187
187
|
}
|
|
188
188
|
catch (err) {
|
|
189
189
|
await deps.releaseStagedResources();
|
|
@@ -351,6 +351,10 @@ export interface McpHttpEndpoint {
|
|
|
351
351
|
close(required: Record<string, never>, optional?: Record<string, never>): Promise<void>;
|
|
352
352
|
}
|
|
353
353
|
export interface McpJsonInjectionOptions {
|
|
354
|
+
/** The trusted host owns authorization and human confirmation for bridge calls. Codex may
|
|
355
|
+
* approve the jini MCP transport without its own prompt; this grants no target-tool permission.
|
|
356
|
+
* Omission preserves the CLI's default approval behavior. Applies only to the jini bridge. */
|
|
357
|
+
readonly toolApprovalOwner?: 'host';
|
|
354
358
|
/** Optional transport port. Reject before CLI launch when HTTP cannot be prepared.
|
|
355
359
|
* The host keeps callback credentials in memory; only this endpoint capability reaches the CLI. */
|
|
356
360
|
readonly prepareHttp?: (required: {
|
|
@@ -52,6 +52,10 @@ export interface CreateFrontendControlOptions {
|
|
|
52
52
|
readonly resolveBindToken: (requiredArgs: {
|
|
53
53
|
readonly request: RunCreateRequest;
|
|
54
54
|
}) => string | undefined;
|
|
55
|
+
/** Authenticated owner of a run's originating surface when session ownership is enabled. */
|
|
56
|
+
readonly resolveBindOwner?: (required: {
|
|
57
|
+
readonly request: RunCreateRequest;
|
|
58
|
+
}) => FrontendSessionOwner | undefined;
|
|
55
59
|
/** @default `@jini-ai/daemon`'s `denyAllFrontendCapabilityPolicy` — a host grants access explicitly. */
|
|
56
60
|
readonly policy?: ToolPolicy;
|
|
57
61
|
/** @default `@jini-ai/daemon`'s `DEFAULT_FRONTEND_CAPABILITY_TIMEOUT_MS`. */
|
|
@@ -100,7 +100,8 @@ export function createFrontendControl(requiredArgs, optionalArgs = {}) {
|
|
|
100
100
|
return;
|
|
101
101
|
let release;
|
|
102
102
|
try {
|
|
103
|
-
|
|
103
|
+
const owner = options.resolveBindOwner?.({ request });
|
|
104
|
+
release = registry.bindRunByToken({ runId: run.id, bindToken, ...(owner ? { owner } : {}) });
|
|
104
105
|
}
|
|
105
106
|
catch (error) {
|
|
106
107
|
reportBindError({ runId: run.id, error });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import type { FrontendControl } from './frontend-control.js';
|
|
3
|
+
import type { FrontendSessionOwner } from '../frontend-session-registry.js';
|
|
4
|
+
import type { ToolExecutor } from '../tool-executor.js';
|
|
5
|
+
export interface FrontendRequestRouteRequired {
|
|
6
|
+
readonly app: Express;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly control: Pick<FrontendControl, 'bindRequest'>;
|
|
9
|
+
readonly executor: Pick<ToolExecutor, 'execute'>;
|
|
10
|
+
/** Host must authenticate session, origin and the tab before returning an owner. */
|
|
11
|
+
readonly resolveOwner: (required: {
|
|
12
|
+
req: Request;
|
|
13
|
+
res: Response;
|
|
14
|
+
}) => Promise<FrontendSessionOwner | null>;
|
|
15
|
+
readonly capabilityIds: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** Bind one authenticated request to its originating surface, then use the host's existing gates.
|
|
18
|
+
* @param required Host-authenticated owner, frontend control, gated executor and allowed ids.
|
|
19
|
+
* @param optional Injectable fresh request id. A caller never chooses a run id or principal.
|
|
20
|
+
* @returns Nothing; mounts one JSON-body route behind the host's authentication/body-limit middleware.
|
|
21
|
+
* @complexity O(allowed ids) setup; one binding and one execution per request, no storage or polling.
|
|
22
|
+
* @overallScore 100
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerFrontendRequestRoute({ app, path, control, executor, resolveOwner, capabilityIds }: FrontendRequestRouteRequired, { newRequestId }?: {
|
|
25
|
+
newRequestId?: () => string;
|
|
26
|
+
}): void;
|
|
27
|
+
//# sourceMappingURL=frontend-request-route.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { streamToolExecution } from './tool-execution-routes.js';
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** Exact browser envelope: an agent cannot borrow a run or supply execution authority.
|
|
7
|
+
* @complexity O(input bytes) time and space for the frozen request; envelope has three keys.
|
|
8
|
+
* @overallScore 100
|
|
9
|
+
*/
|
|
10
|
+
function readFrontendRequest(body, allowed) {
|
|
11
|
+
if (!isRecord(body))
|
|
12
|
+
return null;
|
|
13
|
+
const value = body;
|
|
14
|
+
if (Object.keys(value).length !== 3)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof value.capabilityId !== 'string' || !allowed.has(value.capabilityId))
|
|
17
|
+
return null;
|
|
18
|
+
if (typeof value.bindToken !== 'string' || value.bindToken.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
if (!isRecord(value.input))
|
|
21
|
+
return null;
|
|
22
|
+
return { capabilityId: value.capabilityId, bindToken: value.bindToken, input: structuredClone(value.input) };
|
|
23
|
+
}
|
|
24
|
+
/** Bind one authenticated request to its originating surface, then use the host's existing gates.
|
|
25
|
+
* @param required Host-authenticated owner, frontend control, gated executor and allowed ids.
|
|
26
|
+
* @param optional Injectable fresh request id. A caller never chooses a run id or principal.
|
|
27
|
+
* @returns Nothing; mounts one JSON-body route behind the host's authentication/body-limit middleware.
|
|
28
|
+
* @complexity O(allowed ids) setup; one binding and one execution per request, no storage or polling.
|
|
29
|
+
* @overallScore 100
|
|
30
|
+
*/
|
|
31
|
+
export function registerFrontendRequestRoute({ app, path, control, executor, resolveOwner, capabilityIds }, { newRequestId = randomUUID } = {}) {
|
|
32
|
+
const allowed = new Set(capabilityIds);
|
|
33
|
+
app.post(path, async (req, res) => {
|
|
34
|
+
const request = readFrontendRequest(req.body, allowed);
|
|
35
|
+
if (!request) {
|
|
36
|
+
res.status(400).json({ error: 'Expected an allowed capabilityId, object input and frontend bindToken.' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let owner;
|
|
40
|
+
try {
|
|
41
|
+
owner = await resolveOwner({ req, res });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
res.status(403).json({ error: 'The browser session could not be verified for this page action.' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!owner) {
|
|
48
|
+
res.status(403).json({ error: 'This page request has no authenticated browser owner.' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const runId = newRequestId();
|
|
52
|
+
const boundExecutor = {
|
|
53
|
+
execute: async (call, options = {}) => {
|
|
54
|
+
const signal = options.signal;
|
|
55
|
+
const release = control.bindRequest({ runId, bindToken: request.bindToken, owner, signal });
|
|
56
|
+
try {
|
|
57
|
+
return await executor.execute(call, options);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
release();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
await streamToolExecution({ req, res, executor: boundExecutor, call: {
|
|
65
|
+
executionId: runId, run: { id: runId }, principal: { id: owner.principalId }, toolId: request.capabilityId, input: request.input,
|
|
66
|
+
} }, { keepalive: true });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=frontend-request-route.js.map
|
package/dist/http/index.d.ts
CHANGED
|
@@ -59,4 +59,6 @@ export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
|
59
59
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
60
60
|
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
61
61
|
export { tryRestoreSurface } from './surface-restoration.js';
|
|
62
|
+
export { registerFrontendRequestRoute } from './frontend-request-route.js';
|
|
63
|
+
export type { FrontendRequestRouteRequired } from './frontend-request-route.js';
|
|
62
64
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -32,4 +32,5 @@ export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
|
32
32
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
33
33
|
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
34
34
|
export { tryRestoreSurface } from './surface-restoration.js';
|
|
35
|
+
export { registerFrontendRequestRoute } from './frontend-request-route.js';
|
|
35
36
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
* body that never completed never reaches the executor. Listeners are detached once the call
|
|
23
23
|
* settles, so a long-lived keep-alive socket never accumulates them.
|
|
24
24
|
*/
|
|
25
|
-
import { type ErrorRequestHandler, type RequestHandler } from 'express';
|
|
25
|
+
import { type ErrorRequestHandler, type Request, type RequestHandler, type Response } from 'express';
|
|
26
26
|
import type { ToolDescriptor } from '@jini-ai/core';
|
|
27
27
|
import type { ToolExecutor } from '../tool-executor.js';
|
|
28
|
-
import { TOOL_EXECUTIONS_ROUTE_PATH } from '../remote-tool-executor.js';
|
|
28
|
+
import { TOOL_EXECUTIONS_ROUTE_PATH, type ToolExecutionWireRequest } from '../remote-tool-executor.js';
|
|
29
29
|
export { TOOL_EXECUTIONS_ROUTE_PATH };
|
|
30
30
|
export declare const TOOL_CATALOG_ROUTE_PATH = "/internal/tool-catalog";
|
|
31
31
|
export declare const DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR = "JINI_TOOL_EXEC_TOKEN";
|
|
@@ -62,6 +62,18 @@ export interface ToolExecutionRoutesOptional {
|
|
|
62
62
|
}
|
|
63
63
|
/** The token + loopback gate; mounted before any body parser. Exported for hosts that need it on another path. */
|
|
64
64
|
export declare function requireToolExecutionCaller(_required: Record<string, never>, { tokenEnvVar, env }?: Pick<ToolExecutionRoutesOptional, 'tokenEnvVar' | 'env'>): RequestHandler;
|
|
65
|
+
/**
|
|
66
|
+
* Streams one execution as NDJSON. Headers go out first; surfaces are written as they are emitted
|
|
67
|
+
* and are dropped once the call settled; exactly one terminal line follows.
|
|
68
|
+
*/
|
|
69
|
+
export declare function streamToolExecution({ req, res, call, executor }: {
|
|
70
|
+
readonly req: Request;
|
|
71
|
+
readonly res: Response;
|
|
72
|
+
readonly call: ToolExecutionWireRequest;
|
|
73
|
+
readonly executor: Pick<ToolExecutor, 'execute'>;
|
|
74
|
+
}, { runWithContext, keepalive }: Pick<ToolExecutionRoutesOptional, 'runWithContext'> & {
|
|
75
|
+
keepalive?: boolean;
|
|
76
|
+
}): Promise<void>;
|
|
65
77
|
/**
|
|
66
78
|
* Mounts both routes on `app` (an Express app or a `Router`). Mount it BEFORE any app-level
|
|
67
79
|
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
@@ -99,7 +99,9 @@ function wireError(error) {
|
|
|
99
99
|
* Streams one execution as NDJSON. Headers go out first; surfaces are written as they are emitted
|
|
100
100
|
* and are dropped once the call settled; exactly one terminal line follows.
|
|
101
101
|
*/
|
|
102
|
-
async function
|
|
102
|
+
export async function streamToolExecution({ req, res, call, executor }, { runWithContext, keepalive = false }) {
|
|
103
|
+
if (res.destroyed)
|
|
104
|
+
return;
|
|
103
105
|
const controller = new AbortController();
|
|
104
106
|
// `res` 'close' (not `req` 'close', which fires as soon as a completed body was read) means the
|
|
105
107
|
// caller went away; after a normal end `writableFinished` is already true and nothing aborts.
|
|
@@ -116,6 +118,9 @@ async function streamExecution({ req, res, call, executor }, { runWithContext })
|
|
|
116
118
|
res.write(`${JSON.stringify(line)}\n`); };
|
|
117
119
|
const emitSurface = async (emission) => { if (!settled)
|
|
118
120
|
writeLine({ type: 'surface', emission }); };
|
|
121
|
+
// Human confirmation may outlive the proxy's idle-body timeout; never poll for an answer.
|
|
122
|
+
const heartbeat = keepalive ? setInterval(() => writeLine({ type: 'keepalive' }), 15_000) : undefined;
|
|
123
|
+
heartbeat?.unref();
|
|
119
124
|
const run = () => executor.execute({ principal: call.principal, run: call.run, toolId: call.toolId, input: call.input }, { signal: controller.signal, emitSurface });
|
|
120
125
|
try {
|
|
121
126
|
if (!req.complete || controller.signal.aborted)
|
|
@@ -128,6 +133,8 @@ async function streamExecution({ req, res, call, executor }, { runWithContext })
|
|
|
128
133
|
}
|
|
129
134
|
finally {
|
|
130
135
|
settled = true;
|
|
136
|
+
if (heartbeat)
|
|
137
|
+
clearInterval(heartbeat);
|
|
131
138
|
res.off('close', onClose);
|
|
132
139
|
req.off('error', onRequestError);
|
|
133
140
|
res.end();
|
|
@@ -141,7 +148,7 @@ function executionHandler({ executor }, optional) {
|
|
|
141
148
|
sendApiError({ res, status: 400, error });
|
|
142
149
|
return;
|
|
143
150
|
}
|
|
144
|
-
void
|
|
151
|
+
void streamToolExecution({ req, res, call, executor }, optional);
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
@@ -77,11 +77,18 @@ export interface FailingRunLifecycle {
|
|
|
77
77
|
}, optional?: Record<string, never>): Promise<unknown>;
|
|
78
78
|
}
|
|
79
79
|
/** Emit the refusal reason on the run stream so the chat can show and save it. Event delivery
|
|
80
|
-
* is best-effort; finishing remains mandatory even when the stream cannot accept the event.
|
|
81
|
-
|
|
80
|
+
* is best-effort; finishing remains mandatory even when the stream cannot accept the event.
|
|
81
|
+
* Startup exceptions carry redacted stage/cause text through that same saved error event.
|
|
82
|
+
* @example failRunBeforeStart({ lifecycle, runId, message: 'Could not start.', startup: { stage: 'session binding', error } }, {});
|
|
83
|
+
*/
|
|
84
|
+
export declare function failRunBeforeStart({ lifecycle, runId, message, startup }: {
|
|
82
85
|
lifecycle: FailingRunLifecycle;
|
|
83
86
|
runId: string;
|
|
84
87
|
message: string;
|
|
88
|
+
startup?: {
|
|
89
|
+
stage: string;
|
|
90
|
+
error: unknown;
|
|
91
|
+
};
|
|
85
92
|
}, _optional?: Record<string, never>): Promise<void>;
|
|
86
93
|
export interface AgentCapabilities {
|
|
87
94
|
readonly resumesSessionViaCli?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { startupFailureMessage } from '@jini-ai/agent-runtime';
|
|
1
2
|
/** Fresh private maps keep daemon instances independent. */
|
|
2
3
|
export function createLiveRunTracker(_args, _optional = {}) {
|
|
3
4
|
const runs = new Map();
|
|
@@ -82,10 +83,14 @@ export async function waitForStoppingRuns({ tracker, lifecycle, scheduler, conve
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
/** Emit the refusal reason on the run stream so the chat can show and save it. Event delivery
|
|
85
|
-
* is best-effort; finishing remains mandatory even when the stream cannot accept the event.
|
|
86
|
-
|
|
86
|
+
* is best-effort; finishing remains mandatory even when the stream cannot accept the event.
|
|
87
|
+
* Startup exceptions carry redacted stage/cause text through that same saved error event.
|
|
88
|
+
* @example failRunBeforeStart({ lifecycle, runId, message: 'Could not start.', startup: { stage: 'session binding', error } }, {});
|
|
89
|
+
*/
|
|
90
|
+
export async function failRunBeforeStart({ lifecycle, runId, message, startup }, _optional = {}) {
|
|
91
|
+
const reason = startup ? startupFailureMessage(startup, {}) : message;
|
|
87
92
|
try {
|
|
88
|
-
await lifecycle.emit({ runId: runId, input: { event: 'error', data: { message } } });
|
|
93
|
+
await lifecycle.emit({ runId: runId, input: { event: 'error', data: { message: reason } } });
|
|
89
94
|
}
|
|
90
95
|
catch { /* Finishing remains mandatory. */ }
|
|
91
96
|
await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
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": {
|
|
@@ -153,22 +153,22 @@
|
|
|
153
153
|
"express": "^4.21.0",
|
|
154
154
|
"@types/express": "^4.17.21",
|
|
155
155
|
"@jini-ai/db": "0.6.19",
|
|
156
|
-
"@jini-ai/
|
|
157
|
-
"@jini-ai/
|
|
158
|
-
"@jini-ai/
|
|
156
|
+
"@jini-ai/chat": "0.6.23",
|
|
157
|
+
"@jini-ai/core": "^0.6.23",
|
|
158
|
+
"@jini-ai/platform": "^0.6.23",
|
|
159
159
|
"@jini-ai/protocol": "^0.6.19",
|
|
160
|
-
"@jini-ai/
|
|
160
|
+
"@jini-ai/agent-runtime": "^0.6.23",
|
|
161
161
|
"@jini-ai/http-kit": "0.6.22"
|
|
162
162
|
},
|
|
163
163
|
"dependencies": {
|
|
164
164
|
"smol-toml": "1.9.1",
|
|
165
165
|
"kysely": "^0.29.6",
|
|
166
|
-
"@jini-ai/core": "^0.6.
|
|
167
|
-
"@jini-ai/protocol": "^0.6.19",
|
|
166
|
+
"@jini-ai/core": "^0.6.23",
|
|
168
167
|
"@jini-ai/db": "^0.6.19",
|
|
169
|
-
"@jini-ai/
|
|
170
|
-
"@jini-ai/platform": "^0.6.
|
|
171
|
-
"@jini-ai/http-kit": "^0.6.22"
|
|
168
|
+
"@jini-ai/protocol": "^0.6.19",
|
|
169
|
+
"@jini-ai/platform": "^0.6.23",
|
|
170
|
+
"@jini-ai/http-kit": "^0.6.22",
|
|
171
|
+
"@jini-ai/agent-runtime": "^0.6.23"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
174
|
"node-pty": "^1.1.0",
|