@jini-ai/daemon 0.6.17 → 0.6.19
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 +6 -0
- package/dist/agent-executor/claude/mcp-config.d.ts +17 -0
- package/dist/agent-executor/claude/mcp-config.js +29 -0
- package/dist/agent-executor/codex/home-config.d.ts +7 -6
- package/dist/agent-executor/codex/home-config.js +15 -3
- package/dist/agent-executor/codex/home-staging.d.ts +4 -2
- package/dist/agent-executor/codex/home-staging.js +5 -3
- package/dist/agent-executor/contracts.d.ts +15 -0
- package/dist/agent-executor/event-translation.js +3 -0
- package/dist/agent-executor/events.js +2 -1
- package/dist/agent-executor/gemini/tool-calls.d.ts +7 -0
- package/dist/agent-executor/gemini/tool-calls.js +9 -0
- package/dist/agent-executor/http-mcp.d.ts +23 -0
- package/dist/agent-executor/http-mcp.js +74 -0
- package/dist/agent-executor/launch.js +43 -11
- package/dist/agent-executor/mcp-bridge.d.ts +10 -2
- package/dist/agent-executor/mcp-bridge.js +2 -2
- package/dist/agent-executor/opencode/mcp-config.d.ts +10 -0
- package/dist/agent-executor/opencode/mcp-config.js +11 -0
- package/dist/agent-executor/opencode/tool-calls.d.ts +7 -0
- package/dist/agent-executor/opencode/tool-calls.js +12 -0
- package/dist/agent-executor/tool-call-receipts.d.ts +7 -0
- package/dist/agent-executor/tool-call-receipts.js +9 -0
- package/dist/core/read-only-tools.d.ts +2 -0
- package/dist/core/read-only-tools.js +19 -3
- package/dist/delegated-tool-bridge.js +6 -1
- package/dist/http/attachments.d.ts +2 -101
- package/dist/http/attachments.js +13 -1
- package/dist/http/connectors.js +3 -1
- package/dist/http/db-ops.js +3 -1
- package/dist/http/delegated-tools.d.ts +1 -1
- package/dist/http/delegated-tools.js +5 -3
- package/dist/http/frontend-control.d.ts +0 -36
- package/dist/http/frontend-control.js +39 -1
- package/dist/http/frontend-sessions.js +4 -2
- package/dist/http/index.d.ts +2 -1
- package/dist/http/index.js +1 -0
- package/dist/http/media.js +3 -1
- package/dist/http/model-proxy.js +4 -2
- package/dist/http/remote-run-events.js +3 -1
- package/dist/http/research.js +3 -2
- package/dist/http/runs.js +3 -1
- package/dist/http/surface-restoration.d.ts +17 -0
- package/dist/http/surface-restoration.js +50 -0
- package/dist/http/terminals.js +3 -1
- package/dist/http/tool-catalog.d.ts +15 -1
- package/dist/http/tool-execution-routes.d.ts +3 -1
- package/dist/http/tool-execution-routes.js +4 -3
- package/dist/http/xai.js +3 -2
- package/dist/idle-monitor.d.ts +29 -0
- package/dist/idle-monitor.js +64 -0
- package/dist/image-prompt-delivery.d.ts +1 -1
- package/dist/image-prompt-delivery.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/legacy-data-migration.js +3 -1
- package/dist/remote-attachment-lookup.d.ts +3 -1
- package/dist/remote-attachment-lookup.js +2 -2
- package/dist/routines/scheduler.js +6 -4
- package/dist/routing-tool-executor.d.ts +6 -1
- package/dist/routing-tool-executor.js +15 -2
- package/dist/run-lifecycle.d.ts +0 -16
- package/dist/run-lifecycle.js +24 -6
- package/dist/surface-exchanges.d.ts +5 -1
- package/dist/surface-exchanges.js +41 -7
- package/dist/surface-restoration.d.ts +38 -0
- package/dist/surface-restoration.js +152 -0
- package/dist/tool-recovery/index.d.ts +1 -0
- package/dist/tool-recovery/index.js +1 -0
- package/dist/tool-recovery/stack.js +2 -1
- package/dist/tool-recovery/surface-lifetime.d.ts +11 -0
- package/dist/tool-recovery/surface-lifetime.js +41 -0
- package/package.json +14 -14
|
@@ -31,6 +31,8 @@ export function principalIsReadOnlyConstrained({ principal }, _optional = {}) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Hide writes and unclassified tools from a constrained caller's search and schema discovery.
|
|
33
33
|
* Uses the same classification as dispatch; discovery never grants execution authority.
|
|
34
|
+
* Guidance entries (`targetToolIds`) are shown only when every target is allowed; the catalog is
|
|
35
|
+
* told the allowed ids so an overview can list just those.
|
|
34
36
|
* @param required - Per-call identity, live descriptors, and the existing catalog owner.
|
|
35
37
|
* @returns A filtered catalog, or the original catalog for an unconstrained caller.
|
|
36
38
|
* @example filterReadOnlyToolCatalog({ principal, registry, catalog });
|
|
@@ -49,13 +51,27 @@ export function filterReadOnlyToolCatalog(required, _optional = {}) {
|
|
|
49
51
|
.filter(hit => allowed.has(hit.id)).slice(0, limit)
|
|
50
52
|
.map(hit => ({ ...hit, ...toolDiscoveryMetadata({ descriptor: allowed.get(hit.id) }) }));
|
|
51
53
|
},
|
|
52
|
-
describe(input) {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
54
|
+
describe(input, options = {}) {
|
|
55
|
+
const allowed = allowedTools();
|
|
56
|
+
const descriptor = allowed.get(input.id);
|
|
57
|
+
// A caller may further narrow discovery; read-only attenuation must never widen that set.
|
|
58
|
+
const visible = new Map([...allowed].filter(([id]) => !options.visibleToolIds || options.visibleToolIds.has(id)));
|
|
59
|
+
if (!descriptor)
|
|
60
|
+
return readOnlyGuidance({ entry: required.catalog.describe(input, { visibleToolIds: new Set(visible.keys()) }), allowed: visible });
|
|
61
|
+
const entry = required.catalog.describe(input, options);
|
|
55
62
|
return entry ? { ...entry, ...toolDiscoveryMetadata({ descriptor }) } : null;
|
|
56
63
|
},
|
|
57
64
|
};
|
|
58
65
|
}
|
|
66
|
+
// An id that is not a registered read-only tool can still name guidance (a retired alias, a
|
|
67
|
+
// namespace overview). Guidance is judged by what it points at, never by its own id; guidance
|
|
68
|
+
// without declared targets cannot be judged, so it stays hidden (fail closed).
|
|
69
|
+
function readOnlyGuidance({ entry, allowed }) {
|
|
70
|
+
const targets = entry?.targetToolIds;
|
|
71
|
+
if (!targets || targets.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
return targets.every(id => allowed.has(id)) ? entry : null;
|
|
74
|
+
}
|
|
59
75
|
/** @returns Required host wording for a registered non-read-only tool. */
|
|
60
76
|
export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {}) {
|
|
61
77
|
return messages.toolRefusalMessage({ toolId });
|
|
@@ -64,6 +64,10 @@ export { serializeDelegatedToolOutput, resultContent } from './tool-result-media
|
|
|
64
64
|
function linkCancellation({ lifecycle, runId }, { signal }) {
|
|
65
65
|
const controller = new AbortController();
|
|
66
66
|
const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => controller.abort() });
|
|
67
|
+
const terminalWatch = new AbortController();
|
|
68
|
+
// A CLI can time out/exit without requesting cancellation. Its unanswered cards must stop
|
|
69
|
+
// holding the daemon busy, while the exchange owner retains their safe restoration handle.
|
|
70
|
+
void lifecycle.waitForTerminal({ runId }, { signal: terminalWatch.signal }).then(() => controller.abort(), () => { });
|
|
67
71
|
const abortFromTransport = () => controller.abort();
|
|
68
72
|
if (signal?.aborted)
|
|
69
73
|
controller.abort();
|
|
@@ -73,6 +77,7 @@ function linkCancellation({ lifecycle, runId }, { signal }) {
|
|
|
73
77
|
controller,
|
|
74
78
|
dispose: () => {
|
|
75
79
|
unsubscribeCancel();
|
|
80
|
+
terminalWatch.abort();
|
|
76
81
|
signal?.removeEventListener('abort', abortFromTransport);
|
|
77
82
|
},
|
|
78
83
|
};
|
|
@@ -209,7 +214,7 @@ export function createDelegatedToolBridge(requiredArgs, optionalArgs = {}) {
|
|
|
209
214
|
const call = { lifecycle, runId, toolUseId, ...(options.delegatedResultCorrelation ? { delegatedResultCorrelation: options.delegatedResultCorrelation } : {}) };
|
|
210
215
|
await lifecycle.emit({ runId: runId, input: {
|
|
211
216
|
event: 'agent',
|
|
212
|
-
data: { type: 'tool_use', id: toolUseId, name: toolId, input },
|
|
217
|
+
data: { type: 'tool_use', id: toolUseId, name: toolId, input, ...{ toolOrigin: 'bridge' } },
|
|
213
218
|
} });
|
|
214
219
|
const cancellation = linkCancellation(call, invocation);
|
|
215
220
|
const seam = createSurfaceSeam(call);
|
|
@@ -1,104 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module attachments
|
|
3
|
-
*
|
|
4
|
-
* `POST /api/attachments`, `DELETE /api/attachments` — an HTTP route pack for user file/image
|
|
5
|
-
* uploads staged for one agent run, over a narrow, transport-owned `AttachmentStore` port plus a
|
|
6
|
-
* batteries-included disk implementation (`createDiskAttachmentStore`).
|
|
7
|
-
*
|
|
8
|
-
* **The capability this closes.** A chat composer that accepts drag-and-drop needs somewhere to put
|
|
9
|
-
* the bytes, a way to hand the agent a path it is actually allowed to read, and a guarantee that
|
|
10
|
-
* those bytes disappear when the run ends. That is three separate concerns (an upload endpoint, a
|
|
11
|
-
* trusted-metadata registry, and a run-scoped lifetime) which every host would otherwise hand-roll.
|
|
12
|
-
* This module owns all three; `@jini-ai/chat-react`'s `createDaemonAttachmentUploader` is the
|
|
13
|
-
* matching client half.
|
|
14
|
-
*
|
|
15
|
-
* **Trust model.** A renderer supplies bytes, a filename, and a batch id. None of that is trusted:
|
|
16
|
-
* - the filename is reduced to a basename over a conservative character allowlist
|
|
17
|
-
* (`sanitizeAttachmentName`) and is only ever used as *display* text plus a short extension
|
|
18
|
-
* suffix — never as the stored filename, which is a fresh `randomUUID()`;
|
|
19
|
-
* - the `kind` (`'image' | 'file'`) is sniffed from the leading bytes (`detectAttachmentKind`),
|
|
20
|
-
* never taken from a renderer-controlled MIME type or file extension;
|
|
21
|
-
* - the byte count is measured while streaming, never read from a client-supplied length;
|
|
22
|
-
* - the returned `path` is an opaque `attachment:<uuid>` capability id, not a filesystem path, so a
|
|
23
|
-
* renderer never learns where the upload root is and cannot name a file it did not upload.
|
|
24
|
-
* `claim()` is what exchanges those ids for real paths, server-side.
|
|
25
|
-
*
|
|
26
|
-
* **What `claim()` guarantees.** Exactly once per registered attachment, and only if the file is
|
|
27
|
-
* still the same file: `lstat` must report a regular non-symlink whose `dev`/`ino`/`size` and
|
|
28
|
-
* recorded modification/change timestamps match, and `realpath` must equal the recorded path
|
|
29
|
-
* (so neither the file nor any parent directory was swapped for a symlink between upload and run
|
|
30
|
-
* start). Legacy sidecars establish their timestamp baseline on first verified read. Every
|
|
31
|
-
* claimed attachment must belong to one batch, so the single `batchDirectory` a host grants the
|
|
32
|
-
* agent read access to cannot be widened by mixing batches.
|
|
33
|
-
*
|
|
34
|
-
* **Ownership is opt-in and host-attached, not self-asserted.** `register()`'s optional `ownerId`
|
|
35
|
-
* (and `listPendingForOwner`, the discovery method it enables) is never read from anything a
|
|
36
|
-
* renderer sends — a host wires `AttachmentsHttpDeps.resolveOwnerId` to pull it from a channel IT
|
|
37
|
-
* already trusts (its own session-verified principal header, say), never from `req` unauthenticated.
|
|
38
|
-
* A store with no `resolveOwnerId` wired registers every attachment ownerless, and
|
|
39
|
-
* `listPendingForOwner` never returns an ownerless record for any caller — see that method's own doc
|
|
40
|
-
* for why an absent `ownerId` must never become a wildcard match.
|
|
41
|
-
*
|
|
42
|
-
* **Storage lifetime is daemon-lifetime by default, and opt-in restart-surviving.** With
|
|
43
|
-
* `retainAcrossRestarts` left off (the default, and this pack's only behavior before that option
|
|
44
|
-
* existed), `createDiskAttachmentStore` empties its upload directory on construction: files left
|
|
45
|
-
* behind by an interrupted previous process cannot be authenticated against an in-memory registry
|
|
46
|
-
* that no longer exists, so they are removed rather than adopted. Unclaimed uploads also expire by
|
|
47
|
-
* TTL (`pruneExpired`), and a run's claimed files are deleted by `cleanupRun`.
|
|
48
|
-
*
|
|
49
|
-
* That default is wrong for any host whose process restarts while a person is still using the
|
|
50
|
-
* composer. A file-watching dev server restarting the daemon on an unrelated source edit is not a
|
|
51
|
-
* crash, but the construction wipe cannot tell the two apart, so it destroys uploads the user is
|
|
52
|
-
* about to send — real data loss, not a tidy-up. `retainAcrossRestarts: true` replaces "wipe
|
|
53
|
-
* everything, always" with "adopt exactly what can still be authenticated": see
|
|
54
|
-
* {@link prepareAttachmentStorage} for the whole mechanism and {@link loadPersistedAttachments} for
|
|
55
|
-
* why an adopted record is no less trusted than one this process registered itself.
|
|
56
|
-
*
|
|
57
|
-
* **This pack does not auto-wire itself into a run's lifecycle**, because no generic hook for that
|
|
58
|
-
* exists — the same deliberate choice `@jini-ai/daemon`'s `createRunScopedContextStore` makes. A
|
|
59
|
-
* host claims in its own `onRunStarted` and cleans up in a `finally`, roughly:
|
|
60
|
-
*
|
|
61
|
-
* ```ts
|
|
62
|
-
* const store = await createDiskAttachmentStore({ uploadDirectory });
|
|
63
|
-
* // ... httpExtensions: [(app, { adapter }) => registerAttachmentRoutes({ app, deps: { store }, adapter })]
|
|
64
|
-
* onRunStarted: (context) => {
|
|
65
|
-
* void (async () => {
|
|
66
|
-
* try {
|
|
67
|
-
* const claimed = await store.claim({ attachments: attachmentRefsFrom(context.request), runId: context.run.id });
|
|
68
|
-
* await executor.run({
|
|
69
|
-
* runId: context.run.id,
|
|
70
|
-
* // ... prompt, cwd, agentId
|
|
71
|
-
* }, {
|
|
72
|
-
* ...(claimed.batchDirectory === undefined ? {} : {
|
|
73
|
-
* imagePaths: claimed.attachments.filter((a) => a.kind === 'image').map((a) => a.path),
|
|
74
|
-
* extraAllowedDirs: [claimed.batchDirectory],
|
|
75
|
-
* uploadRoot: claimed.batchDirectory,
|
|
76
|
-
* }),
|
|
77
|
-
* });
|
|
78
|
-
* } finally {
|
|
79
|
-
* await store.cleanupRun({ runId: context.run.id });
|
|
80
|
-
* }
|
|
81
|
-
* })();
|
|
82
|
-
* }
|
|
83
|
-
* ```
|
|
84
|
-
*
|
|
85
|
-
* `imagePaths`/`extraAllowedDirs`/`uploadRoot` are pre-existing `AgentExecutor.run()` options; this
|
|
86
|
-
* module only produces real values for them.
|
|
87
|
-
*
|
|
88
|
-
* **Body-parser ordering (`POST` reads the raw request stream).** The upload route streams
|
|
89
|
-
* `request` straight to disk, so any body-parsing middleware that has already consumed the stream
|
|
90
|
-
* leaves nothing to write. The classic way to hit this is a JSON body parser mounted app-wide: a
|
|
91
|
-
* user drops a `.json` file, the browser sets `content-type: application/json`, and
|
|
92
|
-
* `express.json()` eats the body — which without a guard shows up as the deeply unhelpful
|
|
93
|
-
* "attachment is empty". `registerAttachmentRoutes` detects an already-consumed stream up front and
|
|
94
|
-
* reports it as a host misconfiguration (`'attachment-body-consumed'`) instead of failing silently.
|
|
95
|
-
* Mount this pack before any global body parser, or scope the parser so it skips this path.
|
|
96
|
-
*
|
|
97
|
-
* **SEC-005 redaction**: a store failure can carry filesystem paths a caller must not see, so
|
|
98
|
-
* anything that is not an explicitly-classified `AttachmentRejectedError` becomes a
|
|
99
|
-
* correlation-id-bearing generic `INTERNAL_ERROR`, with the real error reaching a host-owned sink —
|
|
100
|
-
* the same `reportInternalError` shape `media.ts`/`delegated-tools.ts` use.
|
|
101
|
-
*/
|
|
102
1
|
import { type FileHandle } from 'node:fs/promises';
|
|
103
2
|
import type { Express, Request, Response } from 'express';
|
|
104
3
|
import type { AdapterContext } from '@jini-ai/http-kit';
|
|
@@ -688,6 +587,8 @@ export interface AttachmentsInternalErrorContext {
|
|
|
688
587
|
}
|
|
689
588
|
export interface AttachmentsHttpDeps {
|
|
690
589
|
readonly store: AttachmentStore;
|
|
590
|
+
/** Host busy lease follows handler completion, including cleanup after an aborted response. */
|
|
591
|
+
readonly acquireActivity?: () => () => void;
|
|
691
592
|
/**
|
|
692
593
|
* Uploads accepted at once before further requests are rate-limited. Bounds concurrent
|
|
693
594
|
* filesystem work, which matters because each in-flight upload holds an open handle and may be
|
package/dist/http/attachments.js
CHANGED
|
@@ -99,6 +99,8 @@
|
|
|
99
99
|
* correlation-id-bearing generic `INTERNAL_ERROR`, with the real error reaching a host-owned sink —
|
|
100
100
|
* the same `reportInternalError` shape `media.ts`/`delegated-tools.ts` use.
|
|
101
101
|
*/
|
|
102
|
+
import { inspect } from 'node:util';
|
|
103
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
102
104
|
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, rmdir } from 'node:fs/promises';
|
|
103
105
|
import { randomUUID } from 'node:crypto';
|
|
104
106
|
import { basename, dirname, extname, resolve } from 'node:path';
|
|
@@ -971,7 +973,7 @@ export async function createDiskAttachmentStore(requiredArgs, optionalArgs = {})
|
|
|
971
973
|
export const ATTACHMENTS_ROUTE_PATH = '/api/attachments';
|
|
972
974
|
function defaultInternalErrorSink(context) {
|
|
973
975
|
// eslint-disable-next-line no-console
|
|
974
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
976
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
975
977
|
}
|
|
976
978
|
function reportInternalError(deps, source, error, batchId) {
|
|
977
979
|
const correlationId = randomUUID();
|
|
@@ -1135,7 +1137,9 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
|
|
|
1135
1137
|
// second host) must not share one upload budget.
|
|
1136
1138
|
const state = { activeUploads: 0 };
|
|
1137
1139
|
app.post(ATTACHMENTS_ROUTE_PATH, async (req, res) => {
|
|
1140
|
+
let release;
|
|
1138
1141
|
try {
|
|
1142
|
+
release = deps.acquireActivity?.();
|
|
1139
1143
|
if (!passesOriginGuard(req, res, deps, adapter))
|
|
1140
1144
|
return;
|
|
1141
1145
|
await handleAttachmentUpload({ req, res, deps, state });
|
|
@@ -1151,9 +1155,14 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
|
|
|
1151
1155
|
if (!res.headersSent)
|
|
1152
1156
|
respondToUploadFailure(res, deps, error, null, 'attachment-upload');
|
|
1153
1157
|
}
|
|
1158
|
+
finally {
|
|
1159
|
+
release?.();
|
|
1160
|
+
}
|
|
1154
1161
|
});
|
|
1155
1162
|
app.delete(ATTACHMENTS_ROUTE_PATH, async (req, res) => {
|
|
1163
|
+
let release;
|
|
1156
1164
|
try {
|
|
1165
|
+
release = deps.acquireActivity?.();
|
|
1157
1166
|
if (!passesOriginGuard(req, res, deps, adapter))
|
|
1158
1167
|
return;
|
|
1159
1168
|
await handleAttachmentCleanup({ req, res, deps });
|
|
@@ -1162,6 +1171,9 @@ export function registerAttachmentRoutes({ app, deps, adapter }, _optional = {})
|
|
|
1162
1171
|
if (!res.headersSent)
|
|
1163
1172
|
sendApiError({ res, status: 500, error: reportInternalError(deps, 'attachment-cleanup', error, null) });
|
|
1164
1173
|
}
|
|
1174
|
+
finally {
|
|
1175
|
+
release?.();
|
|
1176
|
+
}
|
|
1165
1177
|
});
|
|
1166
1178
|
}
|
|
1167
1179
|
//# sourceMappingURL=attachments.js.map
|
package/dist/http/connectors.js
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
* to the provider.
|
|
45
45
|
* - `RealtimeProvider.subscribe` has no route here — see the realtime section below for why.
|
|
46
46
|
*/
|
|
47
|
+
import { inspect } from 'node:util';
|
|
48
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
47
49
|
import { randomUUID } from 'node:crypto';
|
|
48
50
|
import { createApiError } from '@jini-ai/protocol';
|
|
49
51
|
import { ClientFacingError, defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -53,7 +55,7 @@ import { err, ok } from '@jini-ai/http-kit';
|
|
|
53
55
|
const BODY_NOT_OBJECT_MESSAGE = 'body must be a JSON object';
|
|
54
56
|
function defaultInternalErrorSink(context) {
|
|
55
57
|
// eslint-disable-next-line no-console
|
|
56
|
-
console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, context.error);
|
|
58
|
+
console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
57
59
|
}
|
|
58
60
|
function notConfiguredError(capability) {
|
|
59
61
|
return createApiError({ code: 'NOT_CONFIGURED', message: `${capability} provider not configured` });
|
package/dist/http/db-ops.js
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* boundary — see {@link denyAllDaemonDbPolicy}'s doc for why the default
|
|
37
37
|
* policy denies every call rather than allowing one.
|
|
38
38
|
*/
|
|
39
|
+
import { inspect } from 'node:util';
|
|
40
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
39
41
|
import { randomUUID } from 'node:crypto';
|
|
40
42
|
import { createApiError } from '@jini-ai/protocol';
|
|
41
43
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -116,7 +118,7 @@ export function createDaemonDbToolRegistrations(requiredArgs, optionalArgs = {})
|
|
|
116
118
|
}
|
|
117
119
|
function defaultDaemonDbInternalErrorSink(context) {
|
|
118
120
|
// eslint-disable-next-line no-console
|
|
119
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
121
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
120
122
|
}
|
|
121
123
|
/** Every DB-ops call gets its own opaque, single-use `RunRef` — these are one-off operations requests, not steps of a longer-lived agent run, so there is no real run to attach the audit trail to. */
|
|
122
124
|
function freshRun() {
|
|
@@ -70,7 +70,7 @@ export interface DelegatedToolsHttpDeps {
|
|
|
70
70
|
* call it cannot verify is refused outright (the configured refusal message), never
|
|
71
71
|
* waived. Unconstrained calls behave identically with or without it.
|
|
72
72
|
*/
|
|
73
|
-
readonly toolRegistry?: ToolRegistry
|
|
73
|
+
readonly toolRegistry?: Pick<ToolRegistry, 'list'>;
|
|
74
74
|
/**
|
|
75
75
|
* Host opt-in: returns `true` for a `failed` result whose `error` text the HOST has already made
|
|
76
76
|
* safe to show the model — secret values blanked, an error id minted — so this route answers it
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
1
2
|
/**
|
|
2
3
|
* @module delegated-tools
|
|
3
4
|
*
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import { randomUUID } from 'node:crypto';
|
|
29
30
|
import { createApiError } from '@jini-ai/protocol';
|
|
30
|
-
import { batchOutcomeFor, ToolInputError } from '@jini-ai/core';
|
|
31
|
+
import { redactSecrets, batchOutcomeFor, ToolInputError } from '@jini-ai/core';
|
|
31
32
|
import { createDelegatedToolBridge } from '../index.js';
|
|
32
33
|
import { defineJsonRoute, err, mountJsonRoute, ok, validationError } from '@jini-ai/http-kit';
|
|
33
34
|
import * as readOnlyTools from '../core/read-only-tools.js';
|
|
@@ -44,8 +45,9 @@ function checkReadOnlyConstraint(deps, input) {
|
|
|
44
45
|
}
|
|
45
46
|
/** Logs the real failure server-side and returns the generic, correlation-id-bearing public error (SEC-005: never the raw exception). */
|
|
46
47
|
function defaultInternalErrorSink(context) {
|
|
48
|
+
// Exceptions can echo credentials; inspect preserves stacks and codes before shared redaction.
|
|
47
49
|
// eslint-disable-next-line no-console
|
|
48
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
50
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* The host's {@link DelegatedToolsHttpDeps.describeInternalError} text for `context`, or `undefined`
|
|
@@ -62,7 +64,7 @@ function describeSafely(deps, context) {
|
|
|
62
64
|
}
|
|
63
65
|
catch (error) {
|
|
64
66
|
// eslint-disable-next-line no-console
|
|
65
|
-
console.error(`[@jini-ai/http-kit] describeInternalError threw (correlationId=${context.correlationId})`, error);
|
|
67
|
+
console.error(`[@jini-ai/http-kit] describeInternalError threw (correlationId=${context.correlationId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
66
68
|
return undefined;
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -1,39 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module frontend-control
|
|
3
|
-
*
|
|
4
|
-
* Assembles the three halves of agent-driven frontend control into one thing a host can wire up as
|
|
5
|
-
* a config entry rather than a checklist.
|
|
6
|
-
*
|
|
7
|
-
* The parts already existed and none of them composed: `@jini-ai/daemon`'s `FrontendSessionRegistry`
|
|
8
|
-
* addresses a run to a surface, this package's own frontend-session routes carry invocations to a
|
|
9
|
-
* browser and answers back, and `createFrontendCapabilityRegistrations` projects a manifest into
|
|
10
|
-
* gated tools. A host had to construct all three, keep them consistent, and remember to bind a run
|
|
11
|
-
* when it started. Every one of those is a chance to get it subtly wrong, and one of them —
|
|
12
|
-
* binding — is a security decision.
|
|
13
|
-
*
|
|
14
|
-
* **Moved here from `@jini-ai/server` (2026-07-31).** It never needed that package: its real
|
|
15
|
-
* dependencies are `core`, `daemon`, `express` and this module's own siblings, all of which
|
|
16
|
-
* `http-kit` already has. The only thing pinning it there was a *type-only* import of
|
|
17
|
-
* `LocalNodeHttpExtension` — so a helper any Express host could use sat behind a package that also
|
|
18
|
-
* pulls in `@jini-ai/sidecar` and `better-sqlite3`. The reference implementation hit exactly this
|
|
19
|
-
* and hand-reimplemented the composition instead. `@jini-ai/server` re-exports it, so existing
|
|
20
|
-
* imports keep working.
|
|
21
|
-
*
|
|
22
|
-
* **This facade never returns the registry**, and that is its most important property. The
|
|
23
|
-
* registry's `invoke` executes a capability on a real user's screen with no policy check, no
|
|
24
|
-
* confirmation, no timeout, and no audit record — it is safe only because the sole thing that can
|
|
25
|
-
* reach it is a `ToolHandler` that `ToolExecutor` already gated. Handing a host the registry so it
|
|
26
|
-
* could "wire something custom" would hand it a bypass, and a bypass that exists gets used. The
|
|
27
|
-
* host gets an HTTP extension, a set of registrations, and a bind hook; none of them can execute a
|
|
28
|
-
* capability directly.
|
|
29
|
-
*
|
|
30
|
-
* **The bind token is not a field on the run DTO.** A run is a neutral kernel noun and a browser
|
|
31
|
-
* surface is not; putting `frontendBindToken` on `RunCreateRequest` would push a browser concept
|
|
32
|
-
* into a protocol every consumer shares, to serve one transport. Instead the host supplies
|
|
33
|
-
* {@link CreateFrontendControlOptions.resolveBindToken}, which reads the token out of whatever
|
|
34
|
-
* envelope that host already uses — `contextRef` is an opaque host blob precisely so hosts can put
|
|
35
|
-
* their own metadata in it. The engine stays neutral and the host keeps one place to look.
|
|
36
|
-
*/
|
|
37
1
|
import type { Express } from 'express';
|
|
38
2
|
import type { FrontendSessionOwner } from '../frontend-session-registry.js';
|
|
39
3
|
import type { FrontendSessionsHttpDeps } from './frontend-sessions.js';
|
|
@@ -1,8 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module frontend-control
|
|
3
|
+
*
|
|
4
|
+
* Assembles the three halves of agent-driven frontend control into one thing a host can wire up as
|
|
5
|
+
* a config entry rather than a checklist.
|
|
6
|
+
*
|
|
7
|
+
* The parts already existed and none of them composed: `@jini-ai/daemon`'s `FrontendSessionRegistry`
|
|
8
|
+
* addresses a run to a surface, this package's own frontend-session routes carry invocations to a
|
|
9
|
+
* browser and answers back, and `createFrontendCapabilityRegistrations` projects a manifest into
|
|
10
|
+
* gated tools. A host had to construct all three, keep them consistent, and remember to bind a run
|
|
11
|
+
* when it started. Every one of those is a chance to get it subtly wrong, and one of them —
|
|
12
|
+
* binding — is a security decision.
|
|
13
|
+
*
|
|
14
|
+
* **Moved here from `@jini-ai/server` (2026-07-31).** It never needed that package: its real
|
|
15
|
+
* dependencies are `core`, `daemon`, `express` and this module's own siblings, all of which
|
|
16
|
+
* `http-kit` already has. The only thing pinning it there was a *type-only* import of
|
|
17
|
+
* `LocalNodeHttpExtension` — so a helper any Express host could use sat behind a package that also
|
|
18
|
+
* pulls in `@jini-ai/sidecar` and `better-sqlite3`. The reference implementation hit exactly this
|
|
19
|
+
* and hand-reimplemented the composition instead. `@jini-ai/server` re-exports it, so existing
|
|
20
|
+
* imports keep working.
|
|
21
|
+
*
|
|
22
|
+
* **This facade never returns the registry**, and that is its most important property. The
|
|
23
|
+
* registry's `invoke` executes a capability on a real user's screen with no policy check, no
|
|
24
|
+
* confirmation, no timeout, and no audit record — it is safe only because the sole thing that can
|
|
25
|
+
* reach it is a `ToolHandler` that `ToolExecutor` already gated. Handing a host the registry so it
|
|
26
|
+
* could "wire something custom" would hand it a bypass, and a bypass that exists gets used. The
|
|
27
|
+
* host gets an HTTP extension, a set of registrations, and a bind hook; none of them can execute a
|
|
28
|
+
* capability directly.
|
|
29
|
+
*
|
|
30
|
+
* **The bind token is not a field on the run DTO.** A run is a neutral kernel noun and a browser
|
|
31
|
+
* surface is not; putting `frontendBindToken` on `RunCreateRequest` would push a browser concept
|
|
32
|
+
* into a protocol every consumer shares, to serve one transport. Instead the host supplies
|
|
33
|
+
* {@link CreateFrontendControlOptions.resolveBindToken}, which reads the token out of whatever
|
|
34
|
+
* envelope that host already uses — `contextRef` is an opaque host blob precisely so hosts can put
|
|
35
|
+
* their own metadata in it. The engine stays neutral and the host keeps one place to look.
|
|
36
|
+
*/
|
|
37
|
+
import { inspect } from 'node:util';
|
|
38
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
39
|
import { createFrontendCapabilityRegistrations, createFrontendSessionRegistry, } from '../index.js';
|
|
2
40
|
import { registerFrontendSessionRoutes } from './frontend-sessions.js';
|
|
3
41
|
function defaultBindErrorSink(context) {
|
|
4
42
|
// eslint-disable-next-line no-console
|
|
5
|
-
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, context.error);
|
|
43
|
+
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
6
44
|
}
|
|
7
45
|
/**
|
|
8
46
|
* Builds the frontend-control bundle.
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* composition root can make (it is the thing that starts runs), so it calls `registry.bindRun`
|
|
29
29
|
* directly — see `@jini-ai/daemon`'s own module doc for why attach and bind are separate.
|
|
30
30
|
*/
|
|
31
|
+
import { inspect } from 'node:util';
|
|
32
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
31
33
|
import { randomUUID } from 'node:crypto';
|
|
32
34
|
import { createApiError } from '@jini-ai/protocol';
|
|
33
35
|
import { FrontendSessionCapacityError } from '../frontend-session-registry.js';
|
|
@@ -45,12 +47,12 @@ function reportInternalError(deps, source, error) {
|
|
|
45
47
|
deps.onInternalError({ source, error });
|
|
46
48
|
// eslint-disable-next-line no-console
|
|
47
49
|
else
|
|
48
|
-
console.error(`[@jini-ai/http-kit] internal error (frontend-sessions:${source})`, error);
|
|
50
|
+
console.error(`[@jini-ai/http-kit] internal error (frontend-sessions:${source})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
49
51
|
}
|
|
50
52
|
catch (sinkError) {
|
|
51
53
|
// A diagnostic sink must never turn a contained failure into an unhandled rejection of its own.
|
|
52
54
|
// eslint-disable-next-line no-console
|
|
53
|
-
console.error(`[@jini-ai/http-kit] internal error sink failed (frontend-sessions:${source})`, sinkError);
|
|
55
|
+
console.error(`[@jini-ai/http-kit] internal error sink failed (frontend-sessions:${source})`, redactSecrets({ input: inspect(sinkError) }, { redactPii: false }));
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
/**
|
package/dist/http/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { RoutineDeleteResponse, RoutineHttpDeps, RoutineListResponse, Routi
|
|
|
21
21
|
export { registerRoutineRoutes, routineCreateRoute, routineDeleteRoute, routineGetRoute, routineListRoute, routineRunNowRoute, routineRunsListRoute, routineUpdateRoute, } from './routines.js';
|
|
22
22
|
export type { CreateDaemonDbToolRegistrationsOptions, DaemonDbHttpDeps, DaemonDbInternalErrorContext, DaemonDbOperations, DaemonDbStatusReport, DaemonDbTableInfo, DaemonDbToolRegistrations, DaemonDbVacuumResult, DbIntegrityIssue, DbIntegrityIssueKind, DbIntegrityReport, } from './db-ops.js';
|
|
23
23
|
export { createDaemonDbToolRegistrations, daemonDbInspectRoute, daemonDbVacuumRoute, daemonDbVerifyRoute, DB_INSPECT_TOOL_ID, DB_VACUUM_TOOL_ID, DB_VERIFY_TOOL_ID, denyAllDaemonDbPolicy, registerDaemonDbRoutes, } from './db-ops.js';
|
|
24
|
-
export type { ToolCatalogEntry, ToolCatalogHttpDeps, ToolCatalogQuery, ToolCatalogSearchHit, } from './tool-catalog.js';
|
|
24
|
+
export type { ToolCatalogEntry, ToolCatalogDescribeOptions, ToolCatalogHttpDeps, ToolCatalogQuery, ToolCatalogSearchHit, } from './tool-catalog.js';
|
|
25
25
|
export { registerToolCatalogRoutes, toolCatalogDescribeRoute, toolCatalogSearchRoute, } from './tool-catalog.js';
|
|
26
26
|
export type { ComponentCatalogEntry, ComponentCatalogHttpDeps, ComponentCatalogQuery, ComponentCatalogSearchHit, } from './component-catalog.js';
|
|
27
27
|
export { registerComponentCatalogRoutes, componentCatalogDescribeRoute, componentCatalogSearchRoute, } from './component-catalog.js';
|
|
@@ -58,4 +58,5 @@ export type { RemoteAttachmentLookupOptional, RemoteAttachmentLookupRequired } f
|
|
|
58
58
|
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
|
+
export { tryRestoreSurface } from './surface-restoration.js';
|
|
61
62
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -31,4 +31,5 @@ export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './
|
|
|
31
31
|
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
|
+
export { tryRestoreSurface } from './surface-restoration.js';
|
|
34
35
|
//# sourceMappingURL=index.js.map
|
package/dist/http/media.js
CHANGED
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
* itself can throw before ever creating a task — an unregistered `toolId`-shaped programming
|
|
40
40
|
* error, per `MediaDispatchEngine.generate`'s own contract).
|
|
41
41
|
*/
|
|
42
|
+
import { inspect } from 'node:util';
|
|
43
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
42
44
|
import { randomUUID } from 'node:crypto';
|
|
43
45
|
import { createApiError } from '@jini-ai/protocol';
|
|
44
46
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -49,7 +51,7 @@ const AUDIO_KINDS = new Set(['music', 'speech', 'sfx']);
|
|
|
49
51
|
const SPEECH_FORMATS = new Set(['mp3', 'wav', 'flac', 'aac', 'opus']);
|
|
50
52
|
function defaultInternalErrorSink(context) {
|
|
51
53
|
// eslint-disable-next-line no-console
|
|
52
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
54
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
53
55
|
}
|
|
54
56
|
function reportInternalError(deps, source, error, taskId, ownerRef) {
|
|
55
57
|
const correlationId = randomUUID();
|
package/dist/http/model-proxy.js
CHANGED
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
* over routing for the five providers that already have a dedicated, more
|
|
82
82
|
* specifically-typed path.
|
|
83
83
|
*/
|
|
84
|
+
import { inspect } from 'node:util';
|
|
85
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
84
86
|
import { randomUUID } from 'node:crypto';
|
|
85
87
|
import { runAnthropicToolTurn, runAzureToolTurn, runGoogleToolTurn, runOllamaToolTurn, runOpenAiToolTurn, } from '@jini-ai/agent-runtime';
|
|
86
88
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -92,7 +94,7 @@ import { err, ok } from '@jini-ai/http-kit';
|
|
|
92
94
|
import { firstOptionalFieldError, isArray, isNumber, isRecord, isString, nonEmptyString, pickDefined, requiredFieldError, } from './model-proxy-fields.js';
|
|
93
95
|
function defaultInternalErrorSink(context) {
|
|
94
96
|
// eslint-disable-next-line no-console
|
|
95
|
-
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, context.error);
|
|
97
|
+
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
96
98
|
}
|
|
97
99
|
const TOOLS_CHECK = ['tools', isArray, 'tools must be an array when provided'];
|
|
98
100
|
const SYSTEM_CHECK = ['system', isString, 'system must be a string when provided'];
|
|
@@ -382,7 +384,7 @@ function reportCatchAllError(providerParam, onInternalError, error) {
|
|
|
382
384
|
}
|
|
383
385
|
else {
|
|
384
386
|
// eslint-disable-next-line no-console
|
|
385
|
-
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, error);
|
|
387
|
+
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
386
388
|
}
|
|
387
389
|
return correlationId;
|
|
388
390
|
}
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* would also present, and this route grants direct run-event-injection, a materially different
|
|
23
23
|
* capability from what the general API token was designed to gate.
|
|
24
24
|
*/
|
|
25
|
+
import { inspect } from 'node:util';
|
|
26
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
25
27
|
import { randomUUID } from 'node:crypto';
|
|
26
28
|
import { createApiError, isTerminalRunState } from '@jini-ai/protocol';
|
|
27
29
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -31,7 +33,7 @@ import { sendApiError, statusForError } from '@jini-ai/http-kit';
|
|
|
31
33
|
import { err, ok } from '@jini-ai/http-kit';
|
|
32
34
|
function defaultInternalErrorSink(context) {
|
|
33
35
|
// eslint-disable-next-line no-console
|
|
34
|
-
console.error(`[@jini-ai/http-kit] internal error (remote-run-events/${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
36
|
+
console.error(`[@jini-ai/http-kit] internal error (remote-run-events/${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
35
37
|
}
|
|
36
38
|
const DEFAULT_TOKEN_ENV_VAR = 'JINI_REMOTE_TOOL_BRIDGE_TOKEN';
|
|
37
39
|
/**
|
package/dist/http/research.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { redactSecrets } from '@jini-ai/core';
|
|
2
1
|
/**
|
|
3
2
|
* @module research
|
|
4
3
|
*
|
|
@@ -95,6 +94,8 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
95
94
|
* token out of any upstream error text before it is even logged, belt-and-braces alongside the
|
|
96
95
|
* generic-message substitution.
|
|
97
96
|
*/
|
|
97
|
+
import { inspect } from 'node:util';
|
|
98
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
98
99
|
import { randomUUID } from 'node:crypto';
|
|
99
100
|
import { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
100
101
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -103,7 +104,7 @@ import { defineJsonRoute, err, mountJsonRoute, ok, validationError } from '@jini
|
|
|
103
104
|
const MAX_QUERY_LENGTH = 1000;
|
|
104
105
|
function defaultInternalErrorSink(context) {
|
|
105
106
|
// eslint-disable-next-line no-console
|
|
106
|
-
console.error(`[@jini-ai/http-kit] internal error (research/search, correlationId=${context.correlationId})`, context.error);
|
|
107
|
+
console.error(`[@jini-ai/http-kit] internal error (research/search, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
107
108
|
}
|
|
108
109
|
/**
|
|
109
110
|
* Default `ResearchHttpDeps.resolveCredentials`: reads `TAVILY_API_KEY` from the environment.
|
package/dist/http/runs.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* agent, tool, or product vocabulary: a host optionally supplies `onStarted`
|
|
4
4
|
* to attach its chosen driver after the lifecycle has durably recorded start.
|
|
5
5
|
*/
|
|
6
|
+
import { inspect } from 'node:util';
|
|
7
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
6
8
|
import { randomUUID } from 'node:crypto';
|
|
7
9
|
import { createApiError } from '@jini-ai/protocol';
|
|
8
10
|
import { createSseChannel, defineJsonRoute, err, mountJsonRoute, ok, requestedAfterCursor, sendApiError, sendRawApiError, validationError, } from '@jini-ai/http-kit';
|
|
9
11
|
/** Default sink when a host does not supply `onInternalError`: still observable, never silent. */
|
|
10
12
|
function defaultInternalErrorSink(context) {
|
|
11
13
|
// eslint-disable-next-line no-console
|
|
12
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
14
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Logs the real failure server-side and returns the generic, correlation-id-bearing public error
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { SurfaceExchangeStore } from '@jini-ai/core';
|
|
3
|
+
/** Stream a restored card through the existing authenticated callback route. Only the exchange
|
|
4
|
+
* handle is accepted: tool identity, principal and original arguments come from the shared store.
|
|
5
|
+
* Returns false without writing a response when another process may own the exchange.
|
|
6
|
+
* @complexity O(emitted bytes); no answer or credential is retained by this adapter.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tryRestoreSurface({ res, store, exchangeId, principalId, toolId }: {
|
|
9
|
+
res: Response;
|
|
10
|
+
store: SurfaceExchangeStore;
|
|
11
|
+
exchangeId: string;
|
|
12
|
+
principalId: string;
|
|
13
|
+
toolId: string;
|
|
14
|
+
}, { params }?: {
|
|
15
|
+
params?: Record<string, unknown>;
|
|
16
|
+
}): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=surface-restoration.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Stream a restored card through the existing authenticated callback route. Only the exchange
|
|
2
|
+
* handle is accepted: tool identity, principal and original arguments come from the shared store.
|
|
3
|
+
* Returns false without writing a response when another process may own the exchange.
|
|
4
|
+
* @complexity O(emitted bytes); no answer or credential is retained by this adapter.
|
|
5
|
+
*/
|
|
6
|
+
export async function tryRestoreSurface({ res, store, exchangeId, principalId, toolId }, { params } = {}) {
|
|
7
|
+
if (!store.restore)
|
|
8
|
+
return false;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const onClose = () => { if (!res.writableFinished)
|
|
11
|
+
controller.abort(); };
|
|
12
|
+
res.on('close', onClose);
|
|
13
|
+
const write = (value) => {
|
|
14
|
+
if (res.destroyed)
|
|
15
|
+
return;
|
|
16
|
+
if (!res.headersSent)
|
|
17
|
+
res.status(200).set({ 'content-type': 'application/x-ndjson', 'cache-control': 'no-store' });
|
|
18
|
+
res.write(`${JSON.stringify(value)}\n`);
|
|
19
|
+
};
|
|
20
|
+
// The first card frame opens the response. Subsequent heartbeats keep Node fetch's idle-body
|
|
21
|
+
// limit from expiring a legitimate human wait at the proxy hop.
|
|
22
|
+
const heartbeat = setInterval(() => { if (res.headersSent)
|
|
23
|
+
write({ type: 'keepalive' }); }, 15_000);
|
|
24
|
+
heartbeat.unref();
|
|
25
|
+
try {
|
|
26
|
+
const result = await store.restore({ exchangeId, principalId, emit: async (emission) => {
|
|
27
|
+
write({ type: 'surface', emission });
|
|
28
|
+
} }, { toolId, signal: controller.signal, ...(params === undefined ? {} : { params }) });
|
|
29
|
+
if (!result.ok && result.reason === 'unknown-or-closed' && !res.headersSent)
|
|
30
|
+
return false;
|
|
31
|
+
if (!result.ok && !res.headersSent) {
|
|
32
|
+
res.status(409).json({ code: 'SURFACE_NOT_PENDING', reason: result.reason });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
write({ type: 'restored', ...result });
|
|
36
|
+
res.end();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Failure messages may contain host or credential details. Restoration never reports them.
|
|
41
|
+
write({ type: 'restored', ok: false });
|
|
42
|
+
res.end();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearInterval(heartbeat);
|
|
47
|
+
res.off('close', onClose);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=surface-restoration.js.map
|