@jini-ai/daemon 0.3.8 → 0.5.1
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 +124 -0
- package/README.md +50 -8
- package/dist/agent-executor/contracts.d.ts +736 -0
- package/dist/agent-executor/contracts.js +28 -0
- package/dist/agent-executor/event-translation.d.ts +87 -0
- package/dist/agent-executor/event-translation.js +230 -0
- package/dist/agent-executor/events.d.ts +190 -0
- package/dist/agent-executor/events.js +848 -0
- package/dist/agent-executor/handoff.d.ts +18 -0
- package/dist/agent-executor/handoff.js +16 -0
- package/dist/agent-executor/index.d.ts +98 -0
- package/dist/agent-executor/index.js +98 -0
- package/dist/agent-executor/launch.d.ts +375 -0
- package/dist/agent-executor/launch.js +1001 -0
- package/dist/agent-executor/mcp-bridge.d.ts +338 -0
- package/dist/agent-executor/mcp-bridge.js +835 -0
- package/dist/agent-executor/values.d.ts +5 -0
- package/dist/agent-executor/values.js +13 -0
- package/dist/agent-executor.d.ts +375 -176
- package/dist/agent-executor.js +388 -335
- package/dist/close-status.d.ts +11 -8
- package/dist/close-status.js +31 -19
- package/dist/continuation/continuation-transport.d.ts +3 -1
- package/dist/continuation/continuation-transport.js +1 -1
- package/dist/continuation/journal.d.ts +12 -5
- package/dist/continuation/journal.js +4 -4
- package/dist/continuation/run-scoped-context-store.d.ts +13 -4
- package/dist/continuation/run-scoped-context-store.js +6 -6
- package/dist/continuation/run-start-handler.d.ts +1 -1
- package/dist/continuation/run-start-handler.js +3 -16
- package/dist/delegated-tool-bridge.d.ts +7 -3
- package/dist/delegated-tool-bridge.js +32 -31
- package/dist/event-log.d.ts +7 -5
- package/dist/event-log.js +12 -11
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +3 -2
- package/dist/frontend-session-registry.d.ts +32 -9
- package/dist/frontend-session-registry.js +12 -11
- package/dist/http/active-context.d.ts +105 -0
- package/dist/http/active-context.js +67 -0
- package/dist/http/agents.d.ts +68 -0
- package/dist/http/agents.js +12 -0
- package/dist/http/attachments.d.ts +618 -0
- package/dist/http/attachments.js +1034 -0
- package/dist/http/cancel-owned-runs.d.ts +49 -0
- package/dist/http/cancel-owned-runs.js +35 -0
- package/dist/http/component-catalog.d.ts +84 -0
- package/dist/http/component-catalog.js +64 -0
- package/dist/http/connectors.d.ts +209 -0
- package/dist/http/connectors.js +409 -0
- package/dist/http/daemon-status.d.ts +76 -0
- package/dist/http/daemon-status.js +55 -0
- package/dist/http/db-ops.d.ts +123 -0
- package/dist/http/db-ops.js +189 -0
- package/dist/http/delegated-tools.d.ts +113 -0
- package/dist/http/delegated-tools.js +230 -0
- package/dist/http/frontend-control.d.ts +136 -0
- package/dist/http/frontend-control.js +79 -0
- package/dist/http/frontend-sessions.d.ts +110 -0
- package/dist/http/frontend-sessions.js +180 -0
- package/dist/http/health.d.ts +95 -0
- package/dist/http/health.js +40 -0
- package/dist/http/host-tools.d.ts +214 -0
- package/dist/http/host-tools.js +329 -0
- package/dist/http/index.d.ts +52 -0
- package/dist/http/index.js +27 -0
- package/dist/http/media.d.ts +146 -0
- package/dist/http/media.js +346 -0
- package/dist/http/memory.d.ts +265 -0
- package/dist/http/memory.js +328 -0
- package/dist/http/model-proxy.d.ts +49 -0
- package/dist/http/model-proxy.js +642 -0
- package/dist/http/remote-run-events.d.ts +70 -0
- package/dist/http/remote-run-events.js +224 -0
- package/dist/http/research.d.ts +48 -0
- package/dist/http/research.js +315 -0
- package/dist/http/route-manifest.d.ts +35 -0
- package/dist/http/route-manifest.js +103 -0
- package/dist/http/routines.d.ts +120 -0
- package/dist/http/routines.js +298 -0
- package/dist/http/run-credentials.d.ts +73 -0
- package/dist/http/run-credentials.js +71 -0
- package/dist/http/runs.d.ts +91 -0
- package/dist/http/runs.js +221 -0
- package/dist/http/terminals.d.ts +138 -0
- package/dist/http/terminals.js +325 -0
- package/dist/http/tool-catalog.d.ts +78 -0
- package/dist/http/tool-catalog.js +66 -0
- package/dist/http/workspace-root.d.ts +77 -0
- package/dist/http/workspace-root.js +63 -0
- package/dist/http/xai.d.ts +90 -0
- package/dist/http/xai.js +655 -0
- package/dist/image-prompt-delivery.d.ts +13 -3
- package/dist/image-prompt-delivery.js +5 -5
- package/dist/index.d.ts +556 -4
- package/dist/index.js +556 -4
- package/dist/legacy-data-migration.d.ts +59 -8
- package/dist/legacy-data-migration.js +38 -33
- package/dist/read-only-tools.d.ts +130 -0
- package/dist/read-only-tools.js +115 -0
- package/dist/remote-tool-bridge.d.ts +11 -5
- package/dist/remote-tool-bridge.js +15 -15
- package/dist/routines/index.d.ts +1 -1
- package/dist/routines/index.js +1 -1
- package/dist/routines/routine-store.d.ts +34 -12
- package/dist/routines/routine-store.js +21 -20
- package/dist/routines/schedule.d.ts +48 -10
- package/dist/routines/schedule.js +21 -21
- package/dist/routines/scheduler.d.ts +32 -11
- package/dist/routines/scheduler.js +61 -57
- package/dist/routines/types.d.ts +10 -3
- package/dist/run/core/result.d.ts +6 -2
- package/dist/run/core/result.js +3 -3
- package/dist/run/core/retry.d.ts +17 -4
- package/dist/run/core/retry.js +7 -11
- package/dist/run/diagnostics/diagnostics.d.ts +14 -4
- package/dist/run/diagnostics/diagnostics.js +7 -7
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.js +1 -1
- package/dist/run-credentials.d.ts +138 -0
- package/dist/run-credentials.js +152 -0
- package/dist/run-lifecycle.d.ts +92 -33
- package/dist/run-lifecycle.js +81 -74
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +2 -0
- package/dist/session-coordination.d.ts +115 -0
- package/dist/session-coordination.js +111 -0
- package/dist/store/agent-sessions/errors.d.ts +8 -0
- package/dist/store/agent-sessions/errors.js +10 -0
- package/dist/store/agent-sessions/index.d.ts +12 -0
- package/dist/store/agent-sessions/index.js +19 -0
- package/dist/store/agent-sessions/pglite.d.ts +12 -0
- package/dist/store/agent-sessions/pglite.js +7 -0
- package/dist/store/agent-sessions/ports.d.ts +24 -0
- package/dist/store/agent-sessions/ports.js +2 -0
- package/dist/store/agent-sessions/postgres.d.ts +12 -0
- package/dist/store/agent-sessions/postgres.js +7 -0
- package/dist/store/agent-sessions/schema.d.ts +3 -0
- package/dist/store/agent-sessions/schema.js +23 -0
- package/dist/store/agent-sessions/sql.d.ts +35 -0
- package/dist/store/agent-sessions/sql.js +59 -0
- package/dist/store/agent-sessions/sqlite.d.ts +117 -0
- package/dist/store/agent-sessions/sqlite.js +128 -0
- package/dist/store/event-log/sqlite.d.ts +75 -0
- package/dist/store/event-log/sqlite.js +230 -0
- package/dist/surface-exchanges.d.ts +97 -0
- package/dist/surface-exchanges.js +153 -0
- package/dist/terminal-session.d.ts +43 -11
- package/dist/terminal-session.js +22 -20
- package/dist/tokens.js +4 -4
- package/dist/tool-audit.d.ts +114 -0
- package/dist/tool-audit.js +95 -0
- package/dist/tool-executor.d.ts +22 -7
- package/dist/tool-executor.js +16 -16
- package/dist/tool-result-media.d.ts +3 -1
- package/dist/tool-result-media.js +1 -1
- package/dist/tool-result-surfaces.d.ts +3 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +127 -9
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Mount before JSON parsing, then again on delegated calls after parsing with body validation.
|
|
2
|
+
* Only the allowed policy result may replace the principal header; trusted proxy headers remain intact.
|
|
3
|
+
* Policy exceptions go to Express's error boundary. No request body is accessed in the first gate.
|
|
4
|
+
* @complexity O(1) adapter work, excluding the injected authorization policy.
|
|
5
|
+
*/
|
|
6
|
+
export function createDaemonAuthMiddleware({ authorize, authorizationHeaderName }, { validateDelegatedRunId = false } = {}) {
|
|
7
|
+
return (req, res, next) => {
|
|
8
|
+
try {
|
|
9
|
+
const decision = authorize({ request: {
|
|
10
|
+
method: req.method, path: req.path,
|
|
11
|
+
headers: { [authorizationHeaderName]: req.get(authorizationHeaderName) },
|
|
12
|
+
...(validateDelegatedRunId ? { body: req.body } : {}),
|
|
13
|
+
} }, { validateDelegatedRunId });
|
|
14
|
+
if (!decision.allowed) {
|
|
15
|
+
res.status(decision.status).json(decision.body);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (decision.principalHeader)
|
|
19
|
+
req.headers[decision.principalHeader.name.toLowerCase()] = decision.principalHeader.value;
|
|
20
|
+
next();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
next(error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Mount after authentication on routes with a runId parameter. Denial envelopes pass through intact.
|
|
28
|
+
* @complexity O(1) adapter work plus the injected ownership lookup; no storage or identity defaults.
|
|
29
|
+
*/
|
|
30
|
+
export function createRunOwnershipMiddleware({ authorize, principalHeaderName, isEventStream }) {
|
|
31
|
+
return async (req, res, next) => {
|
|
32
|
+
try {
|
|
33
|
+
const decision = await authorize({ runId: req.params.runId || undefined, principalId: req.get(principalHeaderName) || undefined, eventStream: isEventStream({ path: req.path }) });
|
|
34
|
+
if (!decision.allowed) {
|
|
35
|
+
res.status(decision.status).json(decision.body);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
next(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** List only the authenticated principal's runs, after the host's context filter.
|
|
46
|
+
* Missing principals receive the existing 401 envelope before the list port is called.
|
|
47
|
+
* @complexity O(n) policy filtering for n listed runs; O(1) adapter space excluding results.
|
|
48
|
+
*/
|
|
49
|
+
export function createOwnedRunListHandler({ listRuns, filterOwnedRuns, principalHeaderName }) {
|
|
50
|
+
return async (req, res, next) => {
|
|
51
|
+
try {
|
|
52
|
+
const principalId = req.get(principalHeaderName) || undefined;
|
|
53
|
+
if (!principalId) {
|
|
54
|
+
res.status(401).json({ error: { code: 'UNAUTHENTICATED', message: `${principalHeaderName} is required on run-scoped requests` } });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const contextRef = typeof req.query.contextRef === 'string' ? req.query.contextRef : undefined;
|
|
58
|
+
const runs = await listRuns({}, contextRef === undefined ? {} : { contextRef });
|
|
59
|
+
const decision = filterOwnedRuns({ runs, principalId });
|
|
60
|
+
if (!decision.allowed) {
|
|
61
|
+
res.status(decision.status).json(decision.body);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res.json({ runs: decision.runs });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
next(error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=run-credentials.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import { type RunStatus } from '@jini-ai/protocol';
|
|
3
|
+
import type { RunLifecycle } from '../index.js';
|
|
4
|
+
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
5
|
+
import type { ServerResponse } from 'node:http';
|
|
6
|
+
/**
|
|
7
|
+
* Diagnostic detail for an internal-error response the public API deliberately does not
|
|
8
|
+
* disclose (SEC-005): spawn/storage/adapter failures can embed executable paths, working
|
|
9
|
+
* directories, hostnames, or third-party provider text. The correlation id is the only thing
|
|
10
|
+
* that crosses the boundary; the real `error` goes only to this host-owned sink so an operator
|
|
11
|
+
* can still find and act on it.
|
|
12
|
+
*/
|
|
13
|
+
export interface RunInternalErrorContext {
|
|
14
|
+
readonly source: 'run-start' | 'run-stream';
|
|
15
|
+
readonly runId: string;
|
|
16
|
+
readonly correlationId: string;
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface RunCreateRequest {
|
|
20
|
+
readonly contextRef: string;
|
|
21
|
+
readonly agentId?: string;
|
|
22
|
+
readonly idempotencyKey?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RunStartContext {
|
|
25
|
+
readonly request: RunCreateRequest;
|
|
26
|
+
readonly run: RunStatus;
|
|
27
|
+
/** The lifecycle this driver must use for emitted events, cancellation observation, and terminal completion. */
|
|
28
|
+
readonly lifecycle: RunLifecycle;
|
|
29
|
+
}
|
|
30
|
+
/** Host-owned execution hook. It attaches a driver only after a durable run has been created. */
|
|
31
|
+
export type RunStartHandler = (context: RunStartContext) => Promise<void> | void;
|
|
32
|
+
export interface RunHttpDeps {
|
|
33
|
+
readonly lifecycle: RunLifecycle;
|
|
34
|
+
readonly onStarted?: RunStartHandler;
|
|
35
|
+
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
36
|
+
readonly onInternalError?: (context: RunInternalErrorContext) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface RunStartResponse {
|
|
39
|
+
readonly run: RunStatus;
|
|
40
|
+
readonly started: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RunStatusResponse {
|
|
43
|
+
readonly run: RunStatus;
|
|
44
|
+
}
|
|
45
|
+
export interface RunCancelResponse {
|
|
46
|
+
readonly run: RunStatus;
|
|
47
|
+
}
|
|
48
|
+
export interface RunListResponse {
|
|
49
|
+
readonly runs: readonly RunStatus[];
|
|
50
|
+
}
|
|
51
|
+
export declare const runStartRoute: import("@jini-ai/http-kit").JsonRouteSpec<RunCreateRequest, RunStartResponse, RunHttpDeps>;
|
|
52
|
+
/** `GET /api/runs` — lists runs, optionally scoped to a `contextRef` query parameter. */
|
|
53
|
+
export declare const runListRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
54
|
+
contextRef?: string;
|
|
55
|
+
}, RunListResponse, RunHttpDeps>;
|
|
56
|
+
export declare const runStatusRoute: import("@jini-ai/http-kit").JsonRouteSpec<string, RunStatusResponse, RunHttpDeps>;
|
|
57
|
+
export declare const runCancelRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
58
|
+
runId: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}, RunCancelResponse, RunHttpDeps>;
|
|
61
|
+
/**
|
|
62
|
+
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
63
|
+
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
64
|
+
* against the raw `node:http` `ServerResponse` rather than Express's own `Response` type — Express's
|
|
65
|
+
* `Response` extends it directly, so this is a no-op widening, the same pattern `run-stream.ts`'s
|
|
66
|
+
* AG-UI handler already established.
|
|
67
|
+
*/
|
|
68
|
+
export declare function handleRunEventStreamRequest({ res, runId, afterCursor, deps }: {
|
|
69
|
+
readonly res: ServerResponse;
|
|
70
|
+
readonly runId: string;
|
|
71
|
+
readonly afterCursor: string | null;
|
|
72
|
+
readonly deps: RunHttpDeps;
|
|
73
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Path {@link registerRunEventStream} mounts. Exported as a constant so a caller that needs to know
|
|
76
|
+
* this route exists — a reverse proxy's forward list, `route-manifest.ts` — references it rather than
|
|
77
|
+
* restating the literal and later drifting from it.
|
|
78
|
+
*/
|
|
79
|
+
export declare const RUN_EVENTS_ROUTE_PATH = "/api/runs/:runId/events";
|
|
80
|
+
/** Express mounting glue for {@link handleRunEventStreamRequest} — resolves `runId`/`afterCursor` from an Express `Request` and hands the request straight through. */
|
|
81
|
+
export declare function registerRunEventStream({ app, deps }: {
|
|
82
|
+
readonly app: Express;
|
|
83
|
+
readonly deps: RunHttpDeps;
|
|
84
|
+
}, _optional?: Record<string, never>): void;
|
|
85
|
+
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
86
|
+
export declare function registerRunRoutes({ app, deps, adapter }: {
|
|
87
|
+
readonly app: Express;
|
|
88
|
+
readonly deps: RunHttpDeps;
|
|
89
|
+
readonly adapter: AdapterContext;
|
|
90
|
+
}, _optional?: Record<string, never>): void;
|
|
91
|
+
//# sourceMappingURL=runs.d.ts.map
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic HTTP/SSE projection of the kernel RunLifecycle. The routes own no
|
|
3
|
+
* agent, tool, or product vocabulary: a host optionally supplies `onStarted`
|
|
4
|
+
* to attach its chosen driver after the lifecycle has durably recorded start.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { createApiError } from '@jini-ai/protocol';
|
|
8
|
+
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
9
|
+
import { validationError } from '@jini-ai/http-kit';
|
|
10
|
+
import { sendApiError } from '@jini-ai/http-kit';
|
|
11
|
+
import { createSseChannel, requestedAfterCursor, sendRawApiError } from '@jini-ai/http-kit';
|
|
12
|
+
import { err, ok } from '@jini-ai/http-kit';
|
|
13
|
+
/** Default sink when a host does not supply `onInternalError`: still observable, never silent. */
|
|
14
|
+
function defaultInternalErrorSink(context) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Logs the real failure server-side and returns the generic, correlation-id-bearing public error
|
|
20
|
+
* (SEC-005: never the raw exception). `runId` is mandatory — both real call sites (`run-start`,
|
|
21
|
+
* after a durably-created run's id is already known; `run-stream`, against an already-parsed path
|
|
22
|
+
* parameter) always have one in hand, so an optional field with an unreachable "no runId" branch
|
|
23
|
+
* was speculative flexibility no caller ever exercised.
|
|
24
|
+
*/
|
|
25
|
+
function reportInternalError(deps, source, error, runId) {
|
|
26
|
+
const correlationId = randomUUID();
|
|
27
|
+
const sink = deps.onInternalError ?? defaultInternalErrorSink;
|
|
28
|
+
sink({ source, runId, correlationId, error });
|
|
29
|
+
return createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId });
|
|
30
|
+
}
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function optionalString(body, key) {
|
|
35
|
+
const value = body[key];
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
39
|
+
}
|
|
40
|
+
function parseRunCreate(input) {
|
|
41
|
+
if (!isRecord(input.body))
|
|
42
|
+
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
43
|
+
const contextRef = optionalString(input.body, 'contextRef');
|
|
44
|
+
if (contextRef === undefined || contextRef === null) {
|
|
45
|
+
return err({ error: validationError({ message: 'contextRef must be a non-empty string' }, { issues: [{ path: 'contextRef', message: 'required non-empty string' }] }) });
|
|
46
|
+
}
|
|
47
|
+
const agentId = optionalString(input.body, 'agentId');
|
|
48
|
+
const idempotencyKey = optionalString(input.body, 'idempotencyKey');
|
|
49
|
+
if (agentId === null || idempotencyKey === null) {
|
|
50
|
+
return err({ error: validationError({ message: 'optional string fields must be non-empty when provided' }) });
|
|
51
|
+
}
|
|
52
|
+
return ok({
|
|
53
|
+
value: {
|
|
54
|
+
contextRef,
|
|
55
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
56
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function parseRunList(input) {
|
|
61
|
+
const value = input.query.contextRef;
|
|
62
|
+
if (value === undefined)
|
|
63
|
+
return ok({ value: {} });
|
|
64
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
65
|
+
return err({ error: validationError({ message: 'contextRef must be a non-empty string when provided' }, { issues: [{ path: 'contextRef', message: 'non-empty string when provided' }] }) });
|
|
66
|
+
}
|
|
67
|
+
return ok({ value: { contextRef: value } });
|
|
68
|
+
}
|
|
69
|
+
function parseRunId(input) {
|
|
70
|
+
const runId = input.params.runId;
|
|
71
|
+
return typeof runId === 'string' && runId.length > 0
|
|
72
|
+
? ok({ value: runId })
|
|
73
|
+
: err({ error: validationError({ message: 'runId must be a non-empty path parameter' }) });
|
|
74
|
+
}
|
|
75
|
+
function parseRunCancel(input) {
|
|
76
|
+
const parsedRunId = parseRunId(input);
|
|
77
|
+
if (!parsedRunId.ok)
|
|
78
|
+
return parsedRunId;
|
|
79
|
+
if (input.body === undefined || input.body === null)
|
|
80
|
+
return ok({ value: { runId: parsedRunId.value } });
|
|
81
|
+
if (!isRecord(input.body))
|
|
82
|
+
return err({ error: validationError({ message: 'body must be a JSON object when provided' }) });
|
|
83
|
+
const reason = optionalString(input.body, 'reason');
|
|
84
|
+
if (reason === null)
|
|
85
|
+
return err({ error: validationError({ message: 'reason must be a non-empty string when provided' }) });
|
|
86
|
+
return ok({ value: { runId: parsedRunId.value, ...(reason === undefined ? {} : { reason }) } });
|
|
87
|
+
}
|
|
88
|
+
export const runStartRoute = defineJsonRoute({
|
|
89
|
+
method: 'post', path: '/api/runs', parse: parseRunCreate, handle: async ({ input, deps }) => {
|
|
90
|
+
const startInput = input;
|
|
91
|
+
const started = await deps.lifecycle.start({ contextRef: startInput.contextRef }, { ...startInput });
|
|
92
|
+
if (started.started && deps.onStarted) {
|
|
93
|
+
try {
|
|
94
|
+
await deps.onStarted({ request: input, run: started.run, lifecycle: deps.lifecycle });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
await deps.lifecycle.finish({ runId: started.run.id, status: 'failed', code: null, signal: null, resumable: false });
|
|
98
|
+
return err({ error: reportInternalError(deps, 'run-start', error, started.run.id) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// A host-owned driver may finish immediately (for example, a single-step
|
|
102
|
+
// run). Return the lifecycle's current view rather than the snapshot
|
|
103
|
+
// captured before invoking that driver.
|
|
104
|
+
const run = (await deps.lifecycle.get({ runId: started.run.id })) ?? started.run;
|
|
105
|
+
return ok({ value: { run, started: started.started } });
|
|
106
|
+
}
|
|
107
|
+
}, { requireSameOrigin: true, successStatus: 201 });
|
|
108
|
+
/** `GET /api/runs` — lists runs, optionally scoped to a `contextRef` query parameter. */
|
|
109
|
+
export const runListRoute = defineJsonRoute({ method: 'get', path: '/api/runs', parse: parseRunList, handle: async ({ input, deps }) => ok({ value: { runs: await deps.lifecycle.list({}, input.contextRef === undefined ? {} : { contextRef: input.contextRef }) } }) });
|
|
110
|
+
export const runStatusRoute = defineJsonRoute({
|
|
111
|
+
method: 'get', path: '/api/runs/:runId', parse: parseRunId, handle: async ({ input: runId, deps }) => {
|
|
112
|
+
const run = await deps.lifecycle.get({ runId });
|
|
113
|
+
return run === undefined ? err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${runId}" was not found` }) }) : ok({ value: { run } });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
export const runCancelRoute = defineJsonRoute({
|
|
117
|
+
method: 'post', path: '/api/runs/:runId/cancel', parse: parseRunCancel, handle: async ({ input, deps }) => {
|
|
118
|
+
const run = await deps.lifecycle.get({ runId: input.runId });
|
|
119
|
+
if (run === undefined)
|
|
120
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${input.runId}" was not found` }) });
|
|
121
|
+
return ok({ value: { run: await deps.lifecycle.cancel(input) } });
|
|
122
|
+
}
|
|
123
|
+
}, { requireSameOrigin: true });
|
|
124
|
+
function sendStreamFailure(res, kind) {
|
|
125
|
+
if (kind.kind === 'unknown-run') {
|
|
126
|
+
sendRawApiError({ res, status: 404, error: createApiError({ code: 'NOT_FOUND', message: 'run was not found' }) });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (kind.kind === 'invalid-cursor') {
|
|
130
|
+
sendRawApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: `invalid replay cursor "${kind.requestedCursor}"` }) });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
sendRawApiError({
|
|
134
|
+
res, status: 409, error: createApiError({ code: 'CONFLICT', message: `replay gap after cursor "${kind.requestedCursor}"` }, {
|
|
135
|
+
details: { oldestAvailableCursor: kind.oldestAvailableCursor },
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
141
|
+
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
142
|
+
* against the raw `node:http` `ServerResponse` rather than Express's own `Response` type — Express's
|
|
143
|
+
* `Response` extends it directly, so this is a no-op widening, the same pattern `run-stream.ts`'s
|
|
144
|
+
* AG-UI handler already established.
|
|
145
|
+
*/
|
|
146
|
+
export async function handleRunEventStreamRequest({ res, runId, afterCursor, deps }, _optional = {}) {
|
|
147
|
+
if (runId.length === 0) {
|
|
148
|
+
sendRawApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: 'runId must be a non-empty path parameter' }) });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// `createSseChannel` (`sse.ts`) owns the bounded queue, backpressure, and client-disconnect
|
|
152
|
+
// handling that used to be inlined here — see that module's doc for the generalization.
|
|
153
|
+
const channel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
|
|
154
|
+
let unsubscribeFn = null;
|
|
155
|
+
channel.onClose({
|
|
156
|
+
callback: () => {
|
|
157
|
+
const stop = unsubscribeFn;
|
|
158
|
+
unsubscribeFn = null;
|
|
159
|
+
stop?.();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
const subscribed = await deps.lifecycle.stream({ runId, onEvent: (event) => channel.enqueue({ event }) }, { afterCursor });
|
|
164
|
+
if (subscribed.kind !== 'ok') {
|
|
165
|
+
// Nothing was ever subscribed, so `abandon()` has nothing to unsubscribe — it only marks
|
|
166
|
+
// the channel closed. `res` itself is untouched, leaving `sendStreamFailure` free to send
|
|
167
|
+
// a normal JSON error response instead of an SSE stream.
|
|
168
|
+
channel.abandon();
|
|
169
|
+
sendStreamFailure(res, subscribed);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (channel.isClosed()) {
|
|
173
|
+
// The client already disconnected (or the bounded queue already gave up) while
|
|
174
|
+
// `stream()` was resolving — unsubscribe immediately instead of leaking it.
|
|
175
|
+
subscribed.unsubscribe();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
unsubscribeFn = subscribed.unsubscribe;
|
|
179
|
+
channel.open();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (!res.headersSent) {
|
|
183
|
+
// `channel.open()` never ran (or never got past `flushHeaders`) — abandon without
|
|
184
|
+
// touching `res`, so the JSON error response below is the only thing written.
|
|
185
|
+
// Using `channel.end()` here instead would end the response before this write, turning
|
|
186
|
+
// it into a write-after-end failure.
|
|
187
|
+
channel.abandon();
|
|
188
|
+
sendRawApiError({ res, status: 500, error: reportInternalError(deps, 'run-stream', error, runId) });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Headers were already sent (the stream had started, or `open()` partially ran before
|
|
192
|
+
// throwing) — end the stream itself rather than attempting a second, incompatible response.
|
|
193
|
+
channel.end();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Path {@link registerRunEventStream} mounts. Exported as a constant so a caller that needs to know
|
|
198
|
+
* this route exists — a reverse proxy's forward list, `route-manifest.ts` — references it rather than
|
|
199
|
+
* restating the literal and later drifting from it.
|
|
200
|
+
*/
|
|
201
|
+
export const RUN_EVENTS_ROUTE_PATH = '/api/runs/:runId/events';
|
|
202
|
+
/** Express mounting glue for {@link handleRunEventStreamRequest} — resolves `runId`/`afterCursor` from an Express `Request` and hands the request straight through. */
|
|
203
|
+
export function registerRunEventStream({ app, deps }, _optional = {}) {
|
|
204
|
+
app.get(RUN_EVENTS_ROUTE_PATH, async (req, res) => {
|
|
205
|
+
const runId = req.params.runId;
|
|
206
|
+
if (typeof runId !== 'string' || runId.length === 0) {
|
|
207
|
+
sendApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: 'runId must be a non-empty path parameter' }) });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await handleRunEventStreamRequest({ res, runId, afterCursor: requestedAfterCursor(req), deps });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
214
|
+
export function registerRunRoutes({ app, deps, adapter }, _optional = {}) {
|
|
215
|
+
mountJsonRoute({ app, spec: runStartRoute, deps, adapter });
|
|
216
|
+
mountJsonRoute({ app, spec: runListRoute, deps, adapter });
|
|
217
|
+
mountJsonRoute({ app, spec: runStatusRoute, deps, adapter });
|
|
218
|
+
mountJsonRoute({ app, spec: runCancelRoute, deps, adapter });
|
|
219
|
+
registerRunEventStream({ app, deps });
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=runs.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { Principal } from '@jini-ai/core';
|
|
3
|
+
import type { TerminalSessionActionResult, TerminalSessionInfo, TerminalSessionManager } from '../index.js';
|
|
4
|
+
import type { ToolExecutor } from '../index.js';
|
|
5
|
+
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
6
|
+
import { type WorkspaceRootResolver } from './workspace-root.js';
|
|
7
|
+
import { type Result } from '@jini-ai/http-kit';
|
|
8
|
+
/** Diagnostic detail for an internal-error response the public API deliberately does not disclose (SEC-005, matching `runs.ts`/`db-ops.ts`): a spawn failure (e.g. a missing/uncompiled native addon) can embed executable paths or host detail. */
|
|
9
|
+
export interface TerminalsInternalErrorContext {
|
|
10
|
+
readonly source: 'terminal-create';
|
|
11
|
+
readonly correlationId: string;
|
|
12
|
+
readonly error: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** Everything this route pack needs from the host. */
|
|
15
|
+
export interface TerminalsHttpDeps {
|
|
16
|
+
readonly manager: TerminalSessionManager;
|
|
17
|
+
readonly toolExecutor: ToolExecutor;
|
|
18
|
+
/** The identity these routes act as — same fixed, host-supplied-value shape as `db-ops.ts`'s `DaemonDbHttpDeps.principal` (this transport has no session/identity subsystem of its own). */
|
|
19
|
+
readonly principal: Principal;
|
|
20
|
+
/** Resolves a `resourceRef` to a spawn working directory. Defaults to {@link denyAllWorkspaceRoots} — a host that never wires a real resolver gets a 404 on every create call, never a guessed path (see `workspace-root.ts`). */
|
|
21
|
+
readonly resolveRoot?: WorkspaceRootResolver;
|
|
22
|
+
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
23
|
+
readonly onInternalError?: (context: TerminalsInternalErrorContext) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface TerminalCreateRequest {
|
|
26
|
+
readonly resourceRef: string;
|
|
27
|
+
readonly detail?: string;
|
|
28
|
+
readonly cols?: number;
|
|
29
|
+
readonly rows?: number;
|
|
30
|
+
readonly shell?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `POST /api/terminals` — the one gated call (see module doc). Resolves
|
|
34
|
+
* `resourceRef` to a working directory the same way `host-tools.ts`'s
|
|
35
|
+
* open-in route does, then authorizes+spawns the session through
|
|
36
|
+
* `ToolExecutor.execute(..., 'terminal.create', ...)`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const terminalCreateRoute: import("@jini-ai/http-kit").JsonRouteSpec<TerminalCreateRequest, TerminalSessionInfo, TerminalsHttpDeps>;
|
|
39
|
+
export interface TerminalListResponse {
|
|
40
|
+
readonly terminals: readonly TerminalSessionInfo[];
|
|
41
|
+
}
|
|
42
|
+
/** `GET /api/terminals` — sessions the calling principal owns, optionally narrowed by `resourceRef`. Never gated through `ToolExecutor` — a read scoped to the caller's own sessions, matching `runs.ts`'s `runListRoute` precedent. */
|
|
43
|
+
export declare const terminalListRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
44
|
+
resourceRef?: string;
|
|
45
|
+
}, TerminalListResponse, TerminalsHttpDeps>;
|
|
46
|
+
export interface TerminalActionResponse {
|
|
47
|
+
readonly ok: boolean;
|
|
48
|
+
readonly terminal?: TerminalSessionInfo;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Exported (not just internal) so the `result.session === null` branch — a real race
|
|
52
|
+
* (`write`/`resize`/`kill` finding metadata for an id whose underlying session was concurrently
|
|
53
|
+
* killed between the ownership check and the critical section, per `@jini-ai/daemon`'s
|
|
54
|
+
* `terminal-session.ts` `currentSnapshot`) — is directly unit-testable against a synthetic
|
|
55
|
+
* `TerminalSessionActionResult` rather than requiring a real, hard-to-deterministically-force race
|
|
56
|
+
* through the full `TerminalSessionManager`. Matches this file's own `createDeferredEndGate`
|
|
57
|
+
* precedent for the same "extract into a directly-testable pure function" shape.
|
|
58
|
+
*/
|
|
59
|
+
export declare function actionResultToApiResult({ result, notFoundMessage }: {
|
|
60
|
+
readonly result: TerminalSessionActionResult;
|
|
61
|
+
readonly notFoundMessage: string;
|
|
62
|
+
}, _optional?: Record<string, never>): Result<TerminalActionResponse>;
|
|
63
|
+
/** `POST /api/terminals/:id/stdin` — writes `data` to the session's pty. Ownership-checked by `deps.manager.write` directly, not re-gated through `ToolExecutor` (see module doc). */
|
|
64
|
+
export declare const terminalStdinRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
65
|
+
id: string;
|
|
66
|
+
data: string;
|
|
67
|
+
}, TerminalActionResponse, TerminalsHttpDeps>;
|
|
68
|
+
/** `POST /api/terminals/:id/resize` — resizes the session's pty. */
|
|
69
|
+
export declare const terminalResizeRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
70
|
+
id: string;
|
|
71
|
+
cols: number;
|
|
72
|
+
rows: number;
|
|
73
|
+
}, TerminalActionResponse, TerminalsHttpDeps>;
|
|
74
|
+
/** `POST /api/terminals/:id/kill` — sends `SIGTERM` to the session's pty. */
|
|
75
|
+
export declare const terminalKillRoute: import("@jini-ai/http-kit").JsonRouteSpec<string, TerminalActionResponse, TerminalsHttpDeps>;
|
|
76
|
+
/** `DELETE /api/terminals/:id` — alias for `kill`, matching OD's dual `POST .../kill` / `DELETE` routes. */
|
|
77
|
+
export declare const terminalDeleteRoute: import("@jini-ai/http-kit").JsonRouteSpec<string, TerminalActionResponse, TerminalsHttpDeps>;
|
|
78
|
+
/** The one method {@link createDeferredEndGate} needs from `sse.ts`'s channel — narrowed so the gate is unit-testable against a bare fake, not a real `SseChannel`. */
|
|
79
|
+
interface DeferredEndChannel {
|
|
80
|
+
end(): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The `channelOpened`/`endRequestedBeforeOpen` state machine `registerTerminalEventStream`'s
|
|
84
|
+
* `TerminalSseSink.end()` needs, extracted into its own directly-testable unit — see that
|
|
85
|
+
* function's inline comment for *why* the deferral exists (an already-exited session's replay
|
|
86
|
+
* path can call `end()` before `open()` has ever run).
|
|
87
|
+
*
|
|
88
|
+
* **On the `markOpened()`-then-`end()` ordering (opened=true when `end()` runs):** this is the
|
|
89
|
+
* ordering every *currently reachable* production call graph in this repo takes for a live
|
|
90
|
+
* session that exits while already streaming — but it is unreachable through that graph, not
|
|
91
|
+
* unreachable in principle. `@jini-ai/platform`'s `TerminalService.finish()` (`packages/platform/src/
|
|
92
|
+
* terminal.ts`) calls `sink.send('exit', ...)` immediately before `sink.end()` for every live
|
|
93
|
+
* client; `send()` here forwards straight to `channel.enqueue()`, whose `isEndEvent: (e) => e.kind
|
|
94
|
+
* === 'exit'` match auto-closes the channel *synchronously inside that same `enqueue()` call* —
|
|
95
|
+
* which synchronously runs `registerTerminalEventStream`'s own `channel.onClose` callback, which
|
|
96
|
+
* detaches this sink from `TerminalService`'s `session.clients` before `finish()`'s own, separate
|
|
97
|
+
* `sink.end()` loop ever runs. That loop iterates `session.clients` fresh at call time (not a
|
|
98
|
+
* snapshot taken before `emit()`), so by the time it runs, this sink has already removed itself —
|
|
99
|
+
* `sink.end()` is consequently never invoked while `channelOpened` is `true` via any call graph
|
|
100
|
+
* this repo's own `@jini-ai/platform` + `@jini-ai/daemon/http` composition can currently produce (confirmed by
|
|
101
|
+
* instrumenting the real call path directly, not inferred from reading the source — a synchronous
|
|
102
|
+
* console probe in `end()` never fired across this file's full "live-exit-while-streaming" test).
|
|
103
|
+
* That is a fact about today's specific `TerminalService`/`SseChannel` wiring, not a proof that no
|
|
104
|
+
* future caller of `TerminalSseSink` could ever call `end()` post-open (a hypothetical non-SSE
|
|
105
|
+
* sink, or a future `TerminalService` exit reason not preceded by a matching `send()`, would hit
|
|
106
|
+
* exactly this branch) — so the branch stays as real, intentional defensive code, tested directly
|
|
107
|
+
* here rather than deleted for being unreachable *today* or left silently uncovered.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createDeferredEndGate({ channel }: {
|
|
110
|
+
readonly channel: DeferredEndChannel;
|
|
111
|
+
}, _optional?: Record<string, never>): {
|
|
112
|
+
markOpened(): void;
|
|
113
|
+
end(): void;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* `GET /api/terminals/:id/stream` — SSE, with `Last-Event-ID`/`afterCursor`
|
|
117
|
+
* reconnect replay (via `sse.ts`'s `requestedAfterCursor`, the same helper
|
|
118
|
+
* `runs.ts` uses). Adapts `deps.manager.attach`'s push/end
|
|
119
|
+
* `TerminalSseSink` to `sse.ts`'s generic channel, mirroring
|
|
120
|
+
* `registerRunEventStream`'s adaptation of `RunLifecycle.stream`.
|
|
121
|
+
*
|
|
122
|
+
* Bypasses `mountJsonRoute` (raw `app.get`, for the SSE response shape), so it does not get
|
|
123
|
+
* `requireSameOrigin` for free the way the JSON routes above do — the guard is applied here
|
|
124
|
+
* directly, before the SSE channel is opened, so a cross-origin request never gets a stream.
|
|
125
|
+
*/
|
|
126
|
+
export declare function registerTerminalEventStream({ app, deps, adapter }: {
|
|
127
|
+
readonly app: Express;
|
|
128
|
+
readonly deps: TerminalsHttpDeps;
|
|
129
|
+
readonly adapter: AdapterContext;
|
|
130
|
+
}, _optional?: Record<string, never>): void;
|
|
131
|
+
/** Mounts every terminal route (`list`/`create`/`stdin`/`resize`/`kill`/`delete`/`stream`) on `app`. A pack's `http(app, services)` calls this directly. */
|
|
132
|
+
export declare function registerTerminalRoutes({ app, deps, adapter }: {
|
|
133
|
+
readonly app: Express;
|
|
134
|
+
readonly deps: TerminalsHttpDeps;
|
|
135
|
+
readonly adapter: AdapterContext;
|
|
136
|
+
}, _optional?: Record<string, never>): void;
|
|
137
|
+
export {};
|
|
138
|
+
//# sourceMappingURL=terminals.d.ts.map
|