@jini-ai/daemon 0.6.15 → 0.6.17
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 +18 -0
- package/README.md +14 -4
- package/dist/agent-executor/codex/isolation.js +2 -3
- package/dist/agent-executor/contracts.d.ts +1 -1
- package/dist/agent-executor/contracts.js +0 -1
- package/dist/agent-executor/event-translation.d.ts +3 -3
- package/dist/agent-executor/event-translation.js +4 -4
- package/dist/agent-executor/events.js +1 -3
- package/dist/agent-executor/index.d.ts +5 -5
- package/dist/agent-executor/index.js +5 -5
- package/dist/ai-trace-lifecycle.d.ts +1 -1
- package/dist/ai-trace-lifecycle.js +1 -1
- package/dist/core/tool-audit.js +11 -7
- package/dist/frontend-capability-tools.d.ts +2 -0
- package/dist/frontend-capability-tools.js +7 -1
- package/dist/frontend-session-registry.d.ts +42 -3
- package/dist/frontend-session-registry.js +61 -9
- package/dist/http/frontend-control.d.ts +13 -0
- package/dist/http/frontend-control.js +14 -3
- package/dist/http/frontend-guest-scope.d.ts +32 -0
- package/dist/http/frontend-guest-scope.js +63 -0
- package/dist/http/frontend-sessions.d.ts +29 -6
- package/dist/http/frontend-sessions.js +106 -28
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/store/event-log/sqlite-kernel.js +2 -1
- package/dist/terminal-session.d.ts +51 -20
- package/dist/terminal-session.js +6 -89
- package/dist/terminal.adapter.d.ts +22 -0
- package/dist/terminal.adapter.js +47 -0
- package/dist/tool-executor.d.ts +4 -2
- package/dist/tool-executor.js +50 -14
- package/dist/tool-result-media.d.ts +4 -0
- package/dist/tool-result-media.js +3 -2
- package/package.json +29 -38
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Opaque guest cookie plus independently minted document credential for public frontends. */
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { timingSafeTokenMatch } from '@jini-ai/core';
|
|
4
|
+
/** Per-instance keys isolate visitor control from every other frontend registry.
|
|
5
|
+
* Tokens are bounded and expire; never put either credential in URLs or diagnostics.
|
|
6
|
+
* O(1) bounded cookie/token parsing and signing, no persistent guest database. */
|
|
7
|
+
export function createFrontendGuestScope({ workspaceId, cookieName, cookiePath, allowsRequest }, { now = Date.now, lifetimeMs = 30 * 60_000 } = {}) {
|
|
8
|
+
const key = randomBytes(32);
|
|
9
|
+
const sign = (text) => createHmac('sha256', key).update(text).digest('base64url');
|
|
10
|
+
const mint = (value) => { const payload = Buffer.from(JSON.stringify(value)).toString('base64url'); return `${payload}.${sign(payload)}`; };
|
|
11
|
+
function read(value) {
|
|
12
|
+
if (value.length > 1024)
|
|
13
|
+
return null;
|
|
14
|
+
const parts = value.split('.');
|
|
15
|
+
if (parts.length !== 2)
|
|
16
|
+
return null;
|
|
17
|
+
const [payload, signature] = parts;
|
|
18
|
+
if (!payload || !signature)
|
|
19
|
+
return null;
|
|
20
|
+
const expected = sign(payload);
|
|
21
|
+
// The shared matcher checks UTF-8 byte lengths before native comparison can throw.
|
|
22
|
+
if (!timingSafeTokenMatch({ presented: signature, expected, timingSafeEqual: ({ left, right }) => timingSafeEqual(left, right) }))
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
26
|
+
return typeof parsed.expires === 'number' && parsed.expires > now() ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function guest(req) {
|
|
33
|
+
const cookie = (req.get('cookie') ?? '').split(';').map(part => part.trim()).find(part => part.startsWith(`${cookieName}=`));
|
|
34
|
+
const value = read(cookie?.slice(cookieName.length + 1) ?? '');
|
|
35
|
+
return value?.kind === 'guest' && typeof value.guest === 'string' ? value.guest : undefined;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
/** Authenticate the cookie independently of a tab for credential-minting budgets. */
|
|
39
|
+
async resolveGuest({ req }, _options = {}) {
|
|
40
|
+
return await allowsRequest({ req }) ? guest(req) : undefined;
|
|
41
|
+
},
|
|
42
|
+
async issue({ req, res }, _options = {}) {
|
|
43
|
+
if (!await allowsRequest({ req }))
|
|
44
|
+
return null;
|
|
45
|
+
const principalId = guest(req) ?? randomBytes(24).toString('base64url');
|
|
46
|
+
const expires = now() + lifetimeMs;
|
|
47
|
+
// A JS-readable tab token must never authenticate as the HttpOnly guest cookie.
|
|
48
|
+
res.cookie(cookieName, mint({ kind: 'guest', guest: principalId, expires }), { httpOnly: true, sameSite: 'strict', secure: req.secure, path: cookiePath, maxAge: lifetimeMs });
|
|
49
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
50
|
+
return { tabCredential: mint({ kind: 'tab', guest: principalId, tab: randomBytes(24).toString('base64url'), expires }) };
|
|
51
|
+
},
|
|
52
|
+
async resolveOwner({ req }, _options = {}) {
|
|
53
|
+
if (!await allowsRequest({ req }))
|
|
54
|
+
return null;
|
|
55
|
+
const principalId = guest(req);
|
|
56
|
+
const tab = read(req.get('x-frontend-tab') ?? '');
|
|
57
|
+
if (!principalId || tab?.kind !== 'tab' || tab.guest !== principalId || typeof tab.tab !== 'string')
|
|
58
|
+
return null;
|
|
59
|
+
return { workspaceId, principalId, tabId: tab.tab };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=frontend-guest-scope.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ToolApprovalDecision } from '@jini-ai/core';
|
|
1
2
|
import type { Express, Request, Response } from 'express';
|
|
2
|
-
import type { FrontendSessionRegistry } from '../
|
|
3
|
+
import type { FrontendSessionRegistry, FrontendSessionOwner } from '../frontend-session-registry.js';
|
|
3
4
|
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
4
5
|
export declare const FRONTEND_SESSION_STREAM_ROUTE_PATH = "/api/frontend-sessions/stream";
|
|
5
6
|
export declare const FRONTEND_SESSION_RESPONSE_ROUTE_PATH = "/api/frontend-sessions/:sessionId/responses";
|
|
@@ -29,7 +30,10 @@ export interface FrontendSessionErrorEvent {
|
|
|
29
30
|
readonly type: 'error';
|
|
30
31
|
readonly message: string;
|
|
31
32
|
}
|
|
32
|
-
export type FrontendSessionStreamEvent =
|
|
33
|
+
export type FrontendSessionStreamEvent = {
|
|
34
|
+
readonly type: 'cancelled';
|
|
35
|
+
readonly invocationId: string;
|
|
36
|
+
} | FrontendSessionAttachedEvent | FrontendSessionInvocationEvent | FrontendSessionErrorEvent;
|
|
33
37
|
/**
|
|
34
38
|
* A surface's answer, as a discriminated union rather than a flat record with two optional fields.
|
|
35
39
|
*
|
|
@@ -40,6 +44,11 @@ export type FrontendSessionStreamEvent = FrontendSessionAttachedEvent | Frontend
|
|
|
40
44
|
* to fabricate an impossible input.
|
|
41
45
|
*/
|
|
42
46
|
export type FrontendSessionResponseRequest = {
|
|
47
|
+
readonly sessionId: string;
|
|
48
|
+
readonly invocationId: string;
|
|
49
|
+
readonly phase: 'approval-pending' | 'execution-started';
|
|
50
|
+
readonly decision?: ToolApprovalDecision;
|
|
51
|
+
} | {
|
|
43
52
|
readonly sessionId: string;
|
|
44
53
|
readonly invocationId: string;
|
|
45
54
|
readonly ok: true;
|
|
@@ -56,6 +65,20 @@ export interface FrontendSessionResponseBody {
|
|
|
56
65
|
}
|
|
57
66
|
export interface FrontendSessionsHttpDeps {
|
|
58
67
|
readonly registry: FrontendSessionRegistry;
|
|
68
|
+
/** An isolated public instance supplies its own endpoints and authenticated guest/tab owner. */
|
|
69
|
+
readonly routePrefix?: string;
|
|
70
|
+
/** Must authenticate same-origin metadata as well as credentials on every request. */
|
|
71
|
+
readonly resolveOwner?: (args: {
|
|
72
|
+
req: Request;
|
|
73
|
+
res: Response;
|
|
74
|
+
}) => Promise<FrontendSessionOwner | null>;
|
|
75
|
+
readonly allowedCapabilities?: readonly string[];
|
|
76
|
+
readonly sessionTtlMs?: number;
|
|
77
|
+
/** Trusted quota identities supplied by the host, not query parameters. */
|
|
78
|
+
readonly resolveCapacityKeys?: (args: {
|
|
79
|
+
req: Request;
|
|
80
|
+
owner?: FrontendSessionOwner;
|
|
81
|
+
}) => readonly string[];
|
|
59
82
|
/**
|
|
60
83
|
* Mints session ids. Defaults to `randomUUID`. A surface never supplies its own: an id the
|
|
61
84
|
* caller chose could collide with, or deliberately impersonate, a session already attached.
|
|
@@ -79,14 +102,14 @@ export declare function parseCapabilityQuery({ raw }: {
|
|
|
79
102
|
/**
|
|
80
103
|
* Handles one surface's stream from open to close.
|
|
81
104
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* left. Same precedent as `run-stream.ts`.
|
|
105
|
+
* Invalid capability queries use a final SSE error event. Capacity admission happens before
|
|
106
|
+
* opening the stream so resource refusals retain a real HTTP status and create no connection.
|
|
85
107
|
*/
|
|
86
|
-
export declare function handleFrontendSessionStream({ req, res, deps }: {
|
|
108
|
+
export declare function handleFrontendSessionStream({ req, res, deps, owner }: {
|
|
87
109
|
readonly req: Request;
|
|
88
110
|
readonly res: Response;
|
|
89
111
|
readonly deps: FrontendSessionsHttpDeps;
|
|
112
|
+
readonly owner?: FrontendSessionOwner;
|
|
90
113
|
}, _optional?: Record<string, never>): void;
|
|
91
114
|
/**
|
|
92
115
|
* `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { randomUUID } from 'node:crypto';
|
|
32
32
|
import { createApiError } from '@jini-ai/protocol';
|
|
33
|
+
import { FrontendSessionCapacityError } from '../frontend-session-registry.js';
|
|
33
34
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
34
35
|
import { guardSameOrigin } from '@jini-ai/http-kit';
|
|
35
36
|
import { createSseResponse } from '@jini-ai/http-kit';
|
|
@@ -72,20 +73,34 @@ export function parseCapabilityQuery({ raw }, _optional = {}) {
|
|
|
72
73
|
/**
|
|
73
74
|
* Handles one surface's stream from open to close.
|
|
74
75
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* left. Same precedent as `run-stream.ts`.
|
|
76
|
+
* Invalid capability queries use a final SSE error event. Capacity admission happens before
|
|
77
|
+
* opening the stream so resource refusals retain a real HTTP status and create no connection.
|
|
78
78
|
*/
|
|
79
|
-
export function handleFrontendSessionStream({ req, res, deps }, _optional = {}) {
|
|
80
|
-
const
|
|
79
|
+
export function handleFrontendSessionStream({ req, res, deps, owner }, _optional = {}) {
|
|
80
|
+
const requested = parseCapabilityQuery({ raw: req.query['capability'] });
|
|
81
|
+
const capabilities = requested?.filter(id => !deps.allowedCapabilities || deps.allowedCapabilities.includes(id)) ?? null;
|
|
81
82
|
// Assigned below, after the connection exists — `attach` needs something to deliver through, and
|
|
82
83
|
// `onClose` has to be armed before then so a connection that dies during setup still detaches.
|
|
83
84
|
let handle;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
let expiry;
|
|
86
|
+
// Reserve capacity before committing SSE headers, so a full scope gets a real 429.
|
|
87
|
+
// Delivery cannot occur until the attached token has been sent to the client.
|
|
88
|
+
let connection;
|
|
89
|
+
if (capabilities !== null) {
|
|
90
|
+
handle = attachFrontendSession({ req, deps, capabilities, owner,
|
|
91
|
+
deliver: invocation => connection.send({ data: { type: 'invocation', ...invocation } }),
|
|
92
|
+
cancel: ({ invocationId }) => connection.send({ data: { type: 'cancelled', invocationId } }) });
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
connection = createSseResponse({ req, res }, {
|
|
96
|
+
// Close and expiry release the same quota reservation, without a separate counter lifecycle.
|
|
97
|
+
onClose: () => { clearTimeout(expiry); handle?.detach({}); },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
handle?.detach({});
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
89
104
|
if (capabilities === null) {
|
|
90
105
|
connection.send({
|
|
91
106
|
data: {
|
|
@@ -96,16 +111,22 @@ export function handleFrontendSessionStream({ req, res, deps }, _optional = {})
|
|
|
96
111
|
connection.close();
|
|
97
112
|
return;
|
|
98
113
|
}
|
|
99
|
-
|
|
100
|
-
handle = deps.registry.attach({ descriptor: { sessionId, capabilities }, deliver: (invocation) => connection.send({ data: { type: 'invocation', ...invocation } }) });
|
|
114
|
+
expiry = expireScopedSession({ owner, handle: handle, close: () => connection.close(), ttl: deps.sessionTtlMs });
|
|
101
115
|
connection.send({
|
|
102
116
|
data: {
|
|
103
117
|
type: 'attached',
|
|
104
|
-
sessionId,
|
|
118
|
+
sessionId: handle.sessionId,
|
|
105
119
|
bindToken: handle.bindToken,
|
|
106
120
|
}
|
|
107
121
|
});
|
|
108
122
|
}
|
|
123
|
+
/** Reserve the registry's capacity before SSE commits headers. Host keys are never client input. */
|
|
124
|
+
function attachFrontendSession({ req, deps, capabilities, owner, deliver, cancel }) {
|
|
125
|
+
const sessionId = (deps.newSessionId ?? randomUUID)();
|
|
126
|
+
const capacityKeys = deps.resolveCapacityKeys?.({ req, ...(owner ? { owner } : {}) });
|
|
127
|
+
return deps.registry.attach({ descriptor: { sessionId, capabilities, ...(owner ? { owner } : {}), ...(capacityKeys ? { capacityKeys } : {}) },
|
|
128
|
+
deliver, cancel });
|
|
129
|
+
}
|
|
109
130
|
function isRecord(value) {
|
|
110
131
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
111
132
|
}
|
|
@@ -120,12 +141,28 @@ function parseFrontendSessionResponse(input) {
|
|
|
120
141
|
if (typeof invocationId !== 'string' || invocationId.trim().length === 0) {
|
|
121
142
|
return err({ error: validationError({ message: 'invocationId must be a non-empty string' }, { issues: [{ path: 'invocationId', message: 'required non-empty string' }] }) });
|
|
122
143
|
}
|
|
123
|
-
|
|
144
|
+
return parseResponseOutcome({ sessionId, invocationId, body: input.body });
|
|
145
|
+
}
|
|
146
|
+
/** Progress is scoped to a pending invocation; only fixed, value-free decisions enter the audit. */
|
|
147
|
+
function parseApprovalProgress({ sessionId, invocationId, phase, decision }, _options = {}) {
|
|
148
|
+
if (decision === undefined)
|
|
149
|
+
return ok({ value: { sessionId, invocationId, phase } });
|
|
150
|
+
if (phase !== 'execution-started' || typeof decision !== 'string' || !['approved', 'declined', 'expired', 'abandoned'].includes(decision)) {
|
|
151
|
+
return err({ error: validationError({ message: 'invalid approval decision' }) });
|
|
152
|
+
}
|
|
153
|
+
return ok({ value: { sessionId, invocationId, phase, decision: decision } });
|
|
154
|
+
}
|
|
155
|
+
/** Identity parsing precedes outcome parsing so progress and results share exactly one scope. */
|
|
156
|
+
function parseResponseOutcome({ sessionId, invocationId, body }, _options = {}) {
|
|
157
|
+
const phase = body['phase'];
|
|
158
|
+
if (phase === 'approval-pending' || phase === 'execution-started')
|
|
159
|
+
return parseApprovalProgress({ sessionId, invocationId, phase, decision: body['decision'] });
|
|
160
|
+
const okFlag = body['ok'];
|
|
124
161
|
if (typeof okFlag !== 'boolean') {
|
|
125
162
|
return err({ error: validationError({ message: 'ok must be a boolean' }, { issues: [{ path: 'ok', message: 'required boolean' }] }) });
|
|
126
163
|
}
|
|
127
164
|
if (!okFlag) {
|
|
128
|
-
const message =
|
|
165
|
+
const message = body['message'];
|
|
129
166
|
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
130
167
|
// A failure with no reason gives the agent nothing to act on, which is how a caller ends up
|
|
131
168
|
// retrying the same refusal forever.
|
|
@@ -133,7 +170,7 @@ function parseFrontendSessionResponse(input) {
|
|
|
133
170
|
}
|
|
134
171
|
return ok({ value: { sessionId, invocationId, ok: false, message } });
|
|
135
172
|
}
|
|
136
|
-
return ok({ value: { sessionId, invocationId, ok: true, output:
|
|
173
|
+
return ok({ value: { sessionId, invocationId, ok: true, output: body['output'] } });
|
|
137
174
|
}
|
|
138
175
|
/**
|
|
139
176
|
* `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
|
|
@@ -142,6 +179,8 @@ function parseFrontendSessionResponse(input) {
|
|
|
142
179
|
*/
|
|
143
180
|
export const frontendSessionResponseRoute = defineJsonRoute({
|
|
144
181
|
method: 'post', path: FRONTEND_SESSION_RESPONSE_ROUTE_PATH, parse: parseFrontendSessionResponse, handle: ({ input, deps }) => {
|
|
182
|
+
if ('phase' in input)
|
|
183
|
+
return ok({ value: { settled: deps.registry.progress({ sessionId: input.sessionId, invocationId: input.invocationId, pending: input.phase === 'approval-pending', ...(input.decision ? { decision: input.decision } : {}) }) } });
|
|
145
184
|
const settled = deps.registry.settle({ sessionId: input.sessionId, invocationId: input.invocationId, outcome: input.ok
|
|
146
185
|
? { ok: true, output: input.output }
|
|
147
186
|
: { ok: false, message: input.message } });
|
|
@@ -157,26 +196,65 @@ export const frontendSessionResponseRoute = defineJsonRoute({
|
|
|
157
196
|
* never gets a stream (and, notably, never mints a session id or `bindToken` either).
|
|
158
197
|
*/
|
|
159
198
|
export function registerFrontendSessionRoutes({ app, deps, adapter }, _optional = {}) {
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
const prefix = deps.routePrefix ?? '/api/frontend-sessions';
|
|
200
|
+
app.get(`${prefix}/stream`, async (req, res) => {
|
|
201
|
+
// Scoped hosts authenticate origin together with guest/tab ownership in resolveOwner.
|
|
202
|
+
const origin = deps.resolveOwner ? ok({ value: undefined }) : guardSameOrigin({ req, origin: adapter });
|
|
162
203
|
if (!origin.ok) {
|
|
163
204
|
sendApiError({ res, status: statusForError({ error: origin.error }), error: origin.error });
|
|
164
205
|
return;
|
|
165
206
|
}
|
|
166
207
|
try {
|
|
167
|
-
|
|
208
|
+
const owner = deps.resolveOwner ? await deps.resolveOwner({ req, res }) : undefined;
|
|
209
|
+
if (owner === null) {
|
|
210
|
+
res.sendStatus(403);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
handleFrontendSessionStream({ req, res, deps, ...(owner ? { owner } : {}) });
|
|
168
214
|
}
|
|
169
215
|
catch (error) {
|
|
170
|
-
|
|
171
|
-
if (res.headersSent) {
|
|
172
|
-
if (!res.writableEnded)
|
|
173
|
-
res.end();
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
177
|
-
}
|
|
216
|
+
endStreamOnError({ error, res, deps });
|
|
178
217
|
}
|
|
179
218
|
});
|
|
180
|
-
|
|
219
|
+
const responsePath = `${prefix}/:sessionId/responses`;
|
|
220
|
+
if (deps.resolveOwner)
|
|
221
|
+
app.post(responsePath, async (req, res, next) => {
|
|
222
|
+
try {
|
|
223
|
+
const owner = await deps.resolveOwner({ req, res });
|
|
224
|
+
const bindToken = req.get('x-frontend-bind-token') ?? '';
|
|
225
|
+
if (!owner || !deps.registry.ownsSession({ sessionId: String(req.params['sessionId']), owner, bindToken })) {
|
|
226
|
+
res.sendStatus(403);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
res.sendStatus(403);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
mountJsonRoute({ app, spec: { ...frontendSessionResponseRoute, path: responsePath, requireSameOrigin: !deps.resolveOwner }, deps, adapter });
|
|
236
|
+
}
|
|
237
|
+
/** Capacity is ordinary admission refusal, not a loggable internal failure. */
|
|
238
|
+
function endStreamOnError({ error, res, deps }) {
|
|
239
|
+
if (error instanceof FrontendSessionCapacityError && !res.headersSent) {
|
|
240
|
+
res.sendStatus(429);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
reportInternalError(deps, 'stream', error);
|
|
244
|
+
if (res.headersSent) {
|
|
245
|
+
if (!res.writableEnded)
|
|
246
|
+
res.end();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Expiry tears down addressing and the stream together, including clients which never disconnect. */
|
|
253
|
+
function expireScopedSession({ owner, handle, close, ttl = 30 * 60_000 }) {
|
|
254
|
+
if (!owner)
|
|
255
|
+
return undefined;
|
|
256
|
+
const timer = setTimeout(() => { handle.detach({}); close(); }, ttl);
|
|
257
|
+
timer.unref();
|
|
258
|
+
return timer;
|
|
181
259
|
}
|
|
182
260
|
//# sourceMappingURL=frontend-sessions.js.map
|
package/dist/http/index.d.ts
CHANGED
|
@@ -57,4 +57,5 @@ export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './
|
|
|
57
57
|
export type { RemoteAttachmentLookupOptional, RemoteAttachmentLookupRequired } from '../remote-attachment-lookup.js';
|
|
58
58
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
59
59
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
60
|
+
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
60
61
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -30,4 +30,5 @@ export { DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR, regi
|
|
|
30
30
|
export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './attachment-lookup-routes.js';
|
|
31
31
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
32
32
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
33
|
+
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
33
34
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
* solution belongs at the lifecycle or transport boundary, rather than a
|
|
73
73
|
* restart-unsafe recorder cache that makes the two paths disagree.
|
|
74
74
|
*
|
|
75
|
-
* node-pty
|
|
76
|
-
* C/C++ compiler
|
|
75
|
+
* The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
|
|
76
|
+
* Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
|
|
77
77
|
* Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
|
|
78
78
|
* See the owning modules and packages/daemon/README.md for detailed contracts.
|
|
79
79
|
*/
|
package/dist/index.js
CHANGED
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
* solution belongs at the lifecycle or transport boundary, rather than a
|
|
73
73
|
* restart-unsafe recorder cache that makes the two paths disagree.
|
|
74
74
|
*
|
|
75
|
-
* node-pty
|
|
76
|
-
* C/C++ compiler
|
|
75
|
+
* The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
|
|
76
|
+
* Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
|
|
77
77
|
* Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
|
|
78
78
|
* See the owning modules and packages/daemon/README.md for detailed contracts.
|
|
79
79
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Async SQLite execution over the host kernel; event-log policy lives in sqlite-policy. */
|
|
2
|
+
import { loadOptionalPeer } from '@jini-ai/core/optional-peer';
|
|
2
3
|
import { createSystemClock } from '@jini-ai/core/primitives';
|
|
3
4
|
import { SQLITE_EVENT_LOG_SCHEMA, resolveMaxEntriesPerRun, assertValidReplayCursor, existingAppendResult, replayResult, rowToEntry, } from './sqlite-policy.js';
|
|
4
5
|
/**
|
|
@@ -57,7 +58,7 @@ export async function createKernelSqliteEventLog(required, optional = {}) {
|
|
|
57
58
|
async function setupSchema({ db }, options = {}) {
|
|
58
59
|
if (options.schemaSetup !== 'host') {
|
|
59
60
|
// Keep the existing synchronous factory free of a new eager peer import.
|
|
60
|
-
const { sql } = await import('kysely');
|
|
61
|
+
const { sql } = await loadOptionalPeer({ feature: 'SQL storage adapters', dependency: 'kysely', load: () => import('kysely') });
|
|
61
62
|
// Kysely executes one statement at a time; the owner DDL contains no embedded semicolons.
|
|
62
63
|
for (const statement of SQLITE_EVENT_LOG_SCHEMA.split(';').filter(part => part.trim())) {
|
|
63
64
|
await sql.raw(statement).execute(db);
|
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module terminal-session
|
|
3
|
+
*
|
|
4
|
+
* Interactive-terminal session manager: the `@jini-ai/daemon`-side wiring for
|
|
5
|
+
* OD's `apps/daemon/src/terminals.ts` + `apps/daemon/src/routes/terminal.ts`
|
|
6
|
+
* (see `ADS-memory/reports/proposals/PROP-http-route-packs-terminal-pty-2026-07-21.md`
|
|
7
|
+
* for the full design discussion this module implements a specific decision
|
|
8
|
+
* from). The generic ring-buffer/coalescing/SSE-agnostic session engine
|
|
9
|
+
* already exists as `@jini-ai/platform`'s `terminal.ts` (ported 2026-07-18,
|
|
10
|
+
* before this task) — this module does **not** reimplement that. It adds
|
|
11
|
+
* exactly the three things that engine deliberately left to a caller:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A host-injected `PtySpawn` or `TerminalService`.** The native adapter
|
|
14
|
+
* lives exclusively at `@jini-ai/daemon/terminal`; this default-entry
|
|
15
|
+
* session manager never imports it. The host opts in by supplying
|
|
16
|
+
* `loadSpawnPty: () => loadRealSpawnPty({})` from that subpath.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Session-token gating.** The proposal's blocker was that a
|
|
19
|
+
* *session-scoped* dangerous capability (once a shell is spawned, every
|
|
20
|
+
* subsequent `stdin` write is arbitrary unaudited input for the
|
|
21
|
+
* session's lifetime) does not fit `ToolExecutor`'s call-scoped shape.
|
|
22
|
+
* The resolved design: `ToolExecutor.execute('terminal.create', ...)`
|
|
23
|
+
* (via {@link createTerminalToolRegistrations}) is the one full
|
|
24
|
+
* deny-by-default gated call — it authorizes *creating* a session.
|
|
25
|
+
* Every subsequent `write`/`resize`/`kill`/`attach` call against that
|
|
26
|
+
* session id is validated by a lighter, still-explicit check: this
|
|
27
|
+
* module records which `Principal` created each session at `create()`
|
|
28
|
+
* time, and every later call verifies the calling principal matches,
|
|
29
|
+
* denying (as a `'not-found'` result, never a distinguishable "forbidden"
|
|
30
|
+
* — see {@link checkOwnership}'s doc for why) otherwise. This is not
|
|
31
|
+
* "the session id is unguessable therefore secure" — it is a real,
|
|
32
|
+
* per-call identity check, independent of how hard the id is to guess.
|
|
33
|
+
*
|
|
34
|
+
* 3. **The kill/stdin/resize race fix.** OD's origin (and `@jini-ai/platform`'s
|
|
35
|
+
* faithful port of it) has no lock between a `kill` and a concurrent
|
|
36
|
+
* `write`/`resize` on the same session id: `kill()` only *requests* the
|
|
37
|
+
* real OS process die (sends `SIGTERM`) — the session's `status` does not
|
|
38
|
+
* flip to `'exited'` until the pty's own `onExit` callback fires,
|
|
39
|
+
* asynchronously, once the real process actually dies. A `write`/`resize`
|
|
40
|
+
* processed in that window still observes `status: 'running'` and still
|
|
41
|
+
* reaches the real pty. This module closes that window with its own
|
|
42
|
+
* immediate, lock-protected `killed` flag (see {@link runExclusive} and
|
|
43
|
+
* the `write`/`resize`/`kill` implementations below): once a `kill` call
|
|
44
|
+
* has run its own critical section, any `write`/`resize` queued behind it
|
|
45
|
+
* for the same session id sees `killed: true` and is rejected — it never
|
|
46
|
+
* depends on the real process having actually exited yet.
|
|
47
|
+
*/
|
|
1
48
|
import type { Principal, ToolPolicy, ToolRegistration } from '@jini-ai/core';
|
|
2
49
|
import { type PtySpawn, type TerminalService, type TerminalSseSink } from '@jini-ai/platform';
|
|
3
50
|
export type { TerminalSseSink } from '@jini-ai/platform';
|
|
@@ -13,24 +60,6 @@ export declare const TERMINAL_CREATE_TOOL_ID = "terminal.create";
|
|
|
13
60
|
* merely by registering the tool.
|
|
14
61
|
*/
|
|
15
62
|
export declare const denyAllTerminalCreatePolicy: ToolPolicy;
|
|
16
|
-
/**
|
|
17
|
-
* The real, `node-pty`-backed {@link PtySpawn}. Dynamically imports
|
|
18
|
-
* `node-pty` (mirroring OD's origin `await import('node-pty')`, not a
|
|
19
|
-
* static top-level import) so a host process whose platform/environment
|
|
20
|
-
* lacks a usable `node-pty` addon can still boot — only an actual
|
|
21
|
-
* `terminal.create` call fails, cleanly, rather than crashing at
|
|
22
|
-
* module-eval time. Before the first spawn, repairs `node-pty`'s bundled
|
|
23
|
-
* `spawn-helper` binary's executable bit (pnpm unpacks it non-executable —
|
|
24
|
-
* see `@jini-ai/platform`'s `terminal.ts` module doc for the full story) via
|
|
25
|
-
* that package's own `spawnHelperCandidatePaths`/`ensureSpawnHelperExecutable`,
|
|
26
|
-
* but with the resolver anchored at **this** module
|
|
27
|
-
* (`createRequire(import.meta.url)` here, not `@jini-ai/platform`'s own
|
|
28
|
-
* default): `@jini-ai/platform` deliberately does not depend on `node-pty`, so
|
|
29
|
-
* its default `require.resolve('node-pty')` would not find the copy this
|
|
30
|
-
* package (`@jini-ai/daemon`) actually declares — this is exactly the
|
|
31
|
-
* resolver injection seam `spawnHelperCandidatePaths` was built for.
|
|
32
|
-
*/
|
|
33
|
-
export declare function loadRealSpawnPty(_requiredArgs: Record<string, never>): Promise<PtySpawn>;
|
|
34
63
|
/** The public, principal-neutral session snapshot this module exposes — deliberately its own shape rather than re-exporting `@jini-ai/platform`'s `TerminalSession` verbatim, since that type carries an OD-shaped per-session grouping key (see `@jini-ai/platform`'s `terminal.ts`) this module replaces entirely with the caller-supplied `resourceRef` (see {@link CreateTerminalSessionOptions}), tracked only in this module's own metadata and never forwarded to `@jini-ai/platform` at all — this package's own identifier-neutrality lint (`__tests__/identifier-lint.test.ts`) forbids that origin field's exact name from appearing anywhere in this package's source. */
|
|
35
64
|
export interface TerminalSessionInfo {
|
|
36
65
|
readonly id: string;
|
|
@@ -129,9 +158,9 @@ export interface TerminalSessionManager {
|
|
|
129
158
|
}): Promise<void>;
|
|
130
159
|
}
|
|
131
160
|
export interface CreateTerminalSessionManagerOptions {
|
|
132
|
-
/**
|
|
161
|
+
/** Host-owned session service. Otherwise a fresh platform service uses the injected `loadSpawnPty`. */
|
|
133
162
|
readonly terminalService?: TerminalService;
|
|
134
|
-
/**
|
|
163
|
+
/** Host-owned PTY loader, e.g. `() => loadRealSpawnPty({})` from `@jini-ai/daemon/terminal`. Required for creation unless `terminalService` is supplied. */
|
|
135
164
|
readonly loadSpawnPty?: () => Promise<PtySpawn>;
|
|
136
165
|
readonly maxEvents?: number;
|
|
137
166
|
readonly maxBufferBytes?: number;
|
|
@@ -146,6 +175,8 @@ export interface CreateTerminalSessionManagerOptions {
|
|
|
146
175
|
* around a `@jini-ai/platform` `TerminalService` adding session ownership,
|
|
147
176
|
* the kill/write/resize lock, and (via {@link createTerminalToolRegistrations})
|
|
148
177
|
* `ToolExecutor` gating for creation. See module doc for the full design.
|
|
178
|
+
* Without a host-owned service or loader, boot is inert and `create()` rejects
|
|
179
|
+
* with injection guidance. Wrapper setup is O(1); session work stays in the platform service.
|
|
149
180
|
*/
|
|
150
181
|
export declare function createTerminalSessionManager(_requiredArgs: Record<string, never>, options?: Pick<CreateTerminalSessionManagerOptions, "terminalService" | "loadSpawnPty" | "maxEvents" | "maxBufferBytes" | "exitTailBytes" | "flushIntervalMs" | "flushThresholdBytes" | "ttlMs" | "shutdownGraceMs">): TerminalSessionManager;
|
|
151
182
|
export interface CreateTerminalToolRegistrationsOptions {
|
package/dist/terminal-session.js
CHANGED
|
@@ -1,54 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @module terminal-session
|
|
3
|
-
*
|
|
4
|
-
* Interactive-terminal session manager: the `@jini-ai/daemon`-side wiring for
|
|
5
|
-
* OD's `apps/daemon/src/terminals.ts` + `apps/daemon/src/routes/terminal.ts`
|
|
6
|
-
* (see `ADS-memory/reports/proposals/PROP-http-route-packs-terminal-pty-2026-07-21.md`
|
|
7
|
-
* for the full design discussion this module implements a specific decision
|
|
8
|
-
* from). The generic ring-buffer/coalescing/SSE-agnostic session engine
|
|
9
|
-
* already exists as `@jini-ai/platform`'s `terminal.ts` (ported 2026-07-18,
|
|
10
|
-
* before this task) — this module does **not** reimplement that. It adds
|
|
11
|
-
* exactly the three things that engine deliberately left to a caller:
|
|
12
|
-
*
|
|
13
|
-
* 1. **A real `node-pty`-backed `PtySpawn`.** `@jini-ai/platform` intentionally
|
|
14
|
-
* does not depend on `node-pty` (a native compiled addon); `@jini-ai/daemon`
|
|
15
|
-
* does (see this package's `package.json` and `archived provenance ledger`'s dated
|
|
16
|
-
* section for why that dependency now lives here, not in `@jini-ai/http-kit` or
|
|
17
|
-
* `@jini-ai/platform`). {@link loadRealSpawnPty} is the real, dynamically
|
|
18
|
-
* imported default the reference session manager below uses.
|
|
19
|
-
*
|
|
20
|
-
* 2. **Session-token gating.** The proposal's blocker was that a
|
|
21
|
-
* *session-scoped* dangerous capability (once a shell is spawned, every
|
|
22
|
-
* subsequent `stdin` write is arbitrary unaudited input for the
|
|
23
|
-
* session's lifetime) does not fit `ToolExecutor`'s call-scoped shape.
|
|
24
|
-
* The resolved design: `ToolExecutor.execute('terminal.create', ...)`
|
|
25
|
-
* (via {@link createTerminalToolRegistrations}) is the one full
|
|
26
|
-
* deny-by-default gated call — it authorizes *creating* a session.
|
|
27
|
-
* Every subsequent `write`/`resize`/`kill`/`attach` call against that
|
|
28
|
-
* session id is validated by a lighter, still-explicit check: this
|
|
29
|
-
* module records which `Principal` created each session at `create()`
|
|
30
|
-
* time, and every later call verifies the calling principal matches,
|
|
31
|
-
* denying (as a `'not-found'` result, never a distinguishable "forbidden"
|
|
32
|
-
* — see {@link checkOwnership}'s doc for why) otherwise. This is not
|
|
33
|
-
* "the session id is unguessable therefore secure" — it is a real,
|
|
34
|
-
* per-call identity check, independent of how hard the id is to guess.
|
|
35
|
-
*
|
|
36
|
-
* 3. **The kill/stdin/resize race fix.** OD's origin (and `@jini-ai/platform`'s
|
|
37
|
-
* faithful port of it) has no lock between a `kill` and a concurrent
|
|
38
|
-
* `write`/`resize` on the same session id: `kill()` only *requests* the
|
|
39
|
-
* real OS process die (sends `SIGTERM`) — the session's `status` does not
|
|
40
|
-
* flip to `'exited'` until the pty's own `onExit` callback fires,
|
|
41
|
-
* asynchronously, once the real process actually dies. A `write`/`resize`
|
|
42
|
-
* processed in that window still observes `status: 'running'` and still
|
|
43
|
-
* reaches the real pty. This module closes that window with its own
|
|
44
|
-
* immediate, lock-protected `killed` flag (see {@link runExclusive} and
|
|
45
|
-
* the `write`/`resize`/`kill` implementations below): once a `kill` call
|
|
46
|
-
* has run its own critical section, any `write`/`resize` queued behind it
|
|
47
|
-
* for the same session id sees `killed: true` and is rejected — it never
|
|
48
|
-
* depends on the real process having actually exited yet.
|
|
49
|
-
*/
|
|
50
|
-
import { createRequire } from 'node:module';
|
|
51
|
-
import { createTerminalService, ensureSpawnHelperExecutable, spawnHelperCandidatePaths, } from '@jini-ai/platform';
|
|
1
|
+
import { createTerminalService, } from '@jini-ai/platform';
|
|
52
2
|
/** The `ToolRegistry` id `terminal.create` executes through — see module doc §2. */
|
|
53
3
|
export const TERMINAL_CREATE_TOOL_ID = 'terminal.create';
|
|
54
4
|
/**
|
|
@@ -63,43 +13,6 @@ export const TERMINAL_CREATE_TOOL_ID = 'terminal.create';
|
|
|
63
13
|
export const denyAllTerminalCreatePolicy = {
|
|
64
14
|
authorize: () => 'deny',
|
|
65
15
|
};
|
|
66
|
-
const daemonRequire = createRequire(import.meta.url);
|
|
67
|
-
/** Adapts a real `node-pty` `IPty` to `@jini-ai/platform`'s narrower `PtyProcess` port — explicit per-member mapping rather than relying on structural assignability, matching this package's established "explicit adapter, no implicit coercion" convention (e.g. `agent-executor.ts`'s `toStringEnvRecord`). */
|
|
68
|
-
function toPtyProcess(pty) {
|
|
69
|
-
return {
|
|
70
|
-
onData: (cb) => {
|
|
71
|
-
pty.onData(cb);
|
|
72
|
-
},
|
|
73
|
-
onExit: (cb) => {
|
|
74
|
-
pty.onExit(cb);
|
|
75
|
-
},
|
|
76
|
-
write: (input) => pty.write(input),
|
|
77
|
-
resize: (cols, rows) => pty.resize(cols, rows),
|
|
78
|
-
kill: (signal) => pty.kill(signal),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* The real, `node-pty`-backed {@link PtySpawn}. Dynamically imports
|
|
83
|
-
* `node-pty` (mirroring OD's origin `await import('node-pty')`, not a
|
|
84
|
-
* static top-level import) so a host process whose platform/environment
|
|
85
|
-
* lacks a usable `node-pty` addon can still boot — only an actual
|
|
86
|
-
* `terminal.create` call fails, cleanly, rather than crashing at
|
|
87
|
-
* module-eval time. Before the first spawn, repairs `node-pty`'s bundled
|
|
88
|
-
* `spawn-helper` binary's executable bit (pnpm unpacks it non-executable —
|
|
89
|
-
* see `@jini-ai/platform`'s `terminal.ts` module doc for the full story) via
|
|
90
|
-
* that package's own `spawnHelperCandidatePaths`/`ensureSpawnHelperExecutable`,
|
|
91
|
-
* but with the resolver anchored at **this** module
|
|
92
|
-
* (`createRequire(import.meta.url)` here, not `@jini-ai/platform`'s own
|
|
93
|
-
* default): `@jini-ai/platform` deliberately does not depend on `node-pty`, so
|
|
94
|
-
* its default `require.resolve('node-pty')` would not find the copy this
|
|
95
|
-
* package (`@jini-ai/daemon`) actually declares — this is exactly the
|
|
96
|
-
* resolver injection seam `spawnHelperCandidatePaths` was built for.
|
|
97
|
-
*/
|
|
98
|
-
export async function loadRealSpawnPty(_requiredArgs) {
|
|
99
|
-
ensureSpawnHelperExecutable(spawnHelperCandidatePaths({ resolve: (specifier) => daemonRequire.resolve(specifier) }));
|
|
100
|
-
const nodePty = await import('node-pty');
|
|
101
|
-
return (shell, args, options) => toPtyProcess(nodePty.spawn(shell, args, options));
|
|
102
|
-
}
|
|
103
16
|
function toSessionInfo(session, resourceRef) {
|
|
104
17
|
return {
|
|
105
18
|
id: session.id,
|
|
@@ -144,11 +57,15 @@ async function runExclusive(locks, id, fn) {
|
|
|
144
57
|
* around a `@jini-ai/platform` `TerminalService` adding session ownership,
|
|
145
58
|
* the kill/write/resize lock, and (via {@link createTerminalToolRegistrations})
|
|
146
59
|
* `ToolExecutor` gating for creation. See module doc for the full design.
|
|
60
|
+
* Without a host-owned service or loader, boot is inert and `create()` rejects
|
|
61
|
+
* with injection guidance. Wrapper setup is O(1); session work stays in the platform service.
|
|
147
62
|
*/
|
|
148
63
|
export function createTerminalSessionManager(_requiredArgs, options = {}) {
|
|
149
64
|
const terminalService = options.terminalService ??
|
|
150
65
|
createTerminalService({
|
|
151
|
-
loadSpawnPty: options.loadSpawnPty ?? (() =>
|
|
66
|
+
loadSpawnPty: options.loadSpawnPty ?? (() => {
|
|
67
|
+
throw new Error('Interactive terminals require a host-supplied terminalService or loadSpawnPty. Import loadRealSpawnPty from @jini-ai/daemon/terminal to enable node-pty.');
|
|
68
|
+
}),
|
|
152
69
|
...definedOnly({
|
|
153
70
|
maxEvents: options.maxEvents,
|
|
154
71
|
maxBufferBytes: options.maxBufferBytes,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PtySpawn } from '@jini-ai/platform';
|
|
2
|
+
/**
|
|
3
|
+
* The real, `node-pty`-backed {@link PtySpawn}. Dynamically imports
|
|
4
|
+
* `node-pty` (mirroring OD's origin `await import('node-pty')`, not a
|
|
5
|
+
* static top-level import) so a host process whose platform/environment
|
|
6
|
+
* lacks a usable `node-pty` addon can still boot — only an actual
|
|
7
|
+
* `terminal.create` call fails, cleanly, rather than crashing at
|
|
8
|
+
* module-eval time. Before the first spawn, repairs `node-pty`'s bundled
|
|
9
|
+
* `spawn-helper` binary's executable bit (pnpm unpacks it non-executable —
|
|
10
|
+
* see `@jini-ai/platform`'s `terminal.ts` module doc for the full story) via
|
|
11
|
+
* that package's own `spawnHelperCandidatePaths`/`ensureSpawnHelperExecutable`,
|
|
12
|
+
* but with the resolver anchored at **this** module
|
|
13
|
+
* (`createRequire(import.meta.url)` here, not `@jini-ai/platform`'s own
|
|
14
|
+
* default): `@jini-ai/platform` deliberately does not depend on `node-pty`, so
|
|
15
|
+
* its default `require.resolve('node-pty')` would not find the copy this
|
|
16
|
+
* package (`@jini-ai/daemon`) actually declares — this is exactly the
|
|
17
|
+
* resolver injection seam `spawnHelperCandidatePaths` was built for.
|
|
18
|
+
* @throws A feature-specific load error with the original resolution/ABI error as `cause`.
|
|
19
|
+
* @complexity O(1) adapter work; module loading and helper repair belong to the platform.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadRealSpawnPty(_requiredArgs: Record<string, never>, _optionalArgs?: Record<string, never>): Promise<PtySpawn>;
|
|
22
|
+
//# sourceMappingURL=terminal.adapter.d.ts.map
|