@jini-ai/server 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ## Unreleased
1
+ ## 0.5.1 — 2026-10-08
2
2
 
3
3
  - Align pack contributions, bindings, HTTP mounting, retry classification, executor settings and
4
4
  event-log cleanup with the current core/daemon object argument contracts.
@@ -151,7 +151,10 @@ export function projectDetectedAgent(agent) {
151
151
  available: agent.available,
152
152
  ...(agent.version !== undefined ? { version: agent.version } : {}),
153
153
  ...(agent.authStatus !== undefined ? { authStatus: agent.authStatus } : {}),
154
- models: agent.models.map(({ id, label }) => ({ id, label })),
154
+ models: agent.models.map(({ id, label, identityKind, resolvedId }) => ({ id, label, ...(identityKind ? { identityKind } : {}), ...(resolvedId ? { resolvedId } : {}) })),
155
+ ...(agent.modelCatalog ? { modelCatalog: agent.modelCatalog } : {}),
156
+ ...(agent.defaultModelResolution ? { defaultModelResolution: agent.defaultModelResolution } : {}),
157
+ ...(agent.supportsConcreteModelSelection !== undefined ? { supportsConcreteModelSelection: agent.supportsConcreteModelSelection } : {}),
155
158
  ...(agent.reasoningOptions !== undefined
156
159
  ? { reasoningOptions: agent.reasoningOptions.map(({ id, label }) => ({ id, label })) }
157
160
  : {}),
@@ -229,6 +232,9 @@ export function createBuiltInFeatures(options = {}) {
229
232
  registerRunRoutes({ app: app, deps: {
230
233
  lifecycle: services.context.kernel.lifecycle,
231
234
  ...(onStarted === undefined ? {} : { onStarted }),
235
+ // A mid-run message reaches whichever live run the kernel's own executor is driving;
236
+ // a run some other driver owns reads as not-running, never as delivered.
237
+ deliverUserMessage: (input) => services.context.kernel.agentExecutor.sendUserMessage(input),
232
238
  }, adapter: services.context.adapter });
233
239
  },
234
240
  }),
@@ -241,13 +247,14 @@ export function createBuiltInFeatures(options = {}) {
241
247
  compose: (context) => {
242
248
  // Promise-cached so concurrent clients never spawn duplicate probe sets; invalidated by
243
249
  // POST /api/agents/rescan.
244
- const detector = options.agents?.detector ?? (() => detectAgents({}));
250
+ const detector = options.agents?.detector ?? ((force) => detectAgents({}, { force }));
245
251
  let scan = null;
252
+ let expiresAt = 0;
246
253
  const scanAgents = (force) => {
247
- if (force)
254
+ if (force || Date.now() >= expiresAt)
248
255
  scan = null;
249
256
  if (!scan) {
250
- const pending = detector()
257
+ const pending = detector(force)
251
258
  // Filtered before projection so the executor's answer is computed from the full def —
252
259
  // see `isExecutableDetectedAgent`'s doc for why the projected shape is not enough.
253
260
  .then((agents) => agents.filter(isExecutableDetectedAgent).map(projectDetectedAgent))
@@ -261,6 +268,7 @@ export function createBuiltInFeatures(options = {}) {
261
268
  throw error;
262
269
  });
263
270
  scan = pending;
271
+ expiresAt = Date.now() + 15 * 60_000;
264
272
  }
265
273
  return scan;
266
274
  };
@@ -6,7 +6,7 @@ import type { Bindings, Principal, ToolRegistration } from '@jini-ai/core';
6
6
  import type { AnyPack, MissingTokenIds } from '@jini-ai/core/composition';
7
7
  import type { ResolveRunInput, RunLifecycle } from '@jini-ai/daemon';
8
8
  import { type AdapterContext } from '@jini-ai/http-kit';
9
- import { type DelegatedToolExecuteRequest, type RunStartHandler, type WorkspaceRootResolver } from '@jini-ai/daemon/http';
9
+ import type { DelegatedToolExecuteRequest, RunStartHandler, WorkspaceRootResolver } from '@jini-ai/daemon/http';
10
10
  import { type JiniKernelSecurity, type KernelBoundIds } from './compose-jini-kernel.js';
11
11
  import type { CapabilityId, JiniProfileId } from './feature.js';
12
12
  export { buildDaemonDbOperations, projectDetectedAgent } from './builtin-features.js';
@@ -33,7 +33,6 @@
33
33
  import { createRequire } from 'node:module';
34
34
  import express, {} from 'express';
35
35
  import { installRouteRegistrationGuard, mountPackHttp } from '@jini-ai/http-kit';
36
- import {} from '@jini-ai/daemon/http';
37
36
  import { removeDaemonRegistryRecordIfCurrent, resolveDaemonRegistryPath, writeDaemonRegistryRecord } from '@jini-ai/sidecar';
38
37
  import { composeJiniKernel } from './compose-jini-kernel.js';
39
38
  import { closeHttpServer, normalizeDaemonBindHost } from './host-bootstrap.js';
package/dist/index.d.ts CHANGED
@@ -1,141 +1,34 @@
1
1
  /**
2
2
  * @module @jini-ai/server
3
3
  *
4
- * The Node.js host preset (extraction-plan.md §2.4): `createLocalNodeDaemon`, the piece that
5
- * assembles `@jini-ai/core`/`@jini-ai/daemon`/concern-owned storage adapters/`@jini-ai/http-kit` into an actually-runnable
6
- * daemon process, plus the generic host-lifecycle primitives it's built on. See `archived provenance ledger`
7
- * for full provenance and scope-decision notes.
8
-
9
- * Archived provenance rationale:
10
- * ## Design decisions
11
- *
12
- * **1. Two function overloads instead of one generic signature with a defaulted `BoundIds`.** The
13
- * task brief's literal shown signature was
14
- * `createLocalNodeDaemon<const Packs, BoundIds extends string = KernelBoundIds>(config: ...)`.
15
- * Empirically verified against this repo's TypeScript (5.9.3, `strict`) that this shape does not
16
- * actually preserve `createDaemon`'s compile-time "missing binding" gate: when a type parameter both
17
- * has a default *and* is referenced inside a conditional type in the same position where it also
18
- * needs inference from a nested callback's return type, TypeScript resolves it to the default
19
- * instead of inferring from the callback — silently defeating the gate on exactly the call shape
20
- * (`bindings` customizer provided) where it matters most. Fixed by splitting into two overloads — one
21
- * where `bindings` is absent and `BoundIds` is the concrete `KernelBoundIds`, one where `bindings` is
22
- * required and `BoundIds` is inferred fresh with no default — which sidesteps the inference conflict
23
- * entirely. Both directions (and an under-binding customizer, and a satisfied customizer) are proven
24
- * in `create-local-node-daemon.typecheck.ts`. Documented in full inline on the exported function.
25
- *
26
- * **2. `resolveBoundPort`/`resolveReportHost` extracted as small pure, exported helpers.** The
27
- * origin's `.listen()` tail inlined both the "is `server.address()` actually usable" check and the
28
- * "substitute 127.0.0.1 for an all-interfaces bind host" substitution directly in the `'listening'`
29
- * callback. Pulling them out makes two genuinely-defensive branches (a `server.address()` that is
30
- * `null`/a Unix-socket-path string/a non-positive port — provably unreachable via any real TCP
31
- * listener once `'listening'` has fired, per the origin's own "belt-and-braces" framing) directly
32
- * unit-testable without mocking Express or the underlying socket at all.
33
- *
34
- * **3. `@jini/core/internal`'s `AnyPack`/`RequiredTokenIds`/`MissingTokenIds` re-export.** Needed so
35
- * this package could re-derive `createDaemon`'s exact compile-time gate rather than duplicate the
36
- * type-level logic. See `packages/core/src/internal.ts`'s own module doc and
37
- * `packages/core/src/daemon.ts` — these three types are now `export`ed there (previously
38
- * module-private) but deliberately *not* re-exported from `@jini/core`'s public `index.ts` (which
39
- * switched from a `daemon.js` wildcard re-export to an explicit named list for exactly this reason).
40
- *
41
- * **4. `createDaemon`'s own compile-time gate is bypassed (via a cast) inside
42
- * `createLocalNodeDaemon`'s implementation body**, the same way
43
- * `packages/core/src/__tests__/index.test.ts`'s `createDaemonUnsafe` reaches `createDaemon`'s runtime
44
- * path directly: `Packs`/`BoundIds` are still abstract, unresolved type parameters inside a generic
45
- * function body, so `createDaemon`'s own conditional gate can never collapse to a definite branch
46
- * there regardless of which overload the real call site matched. Safety was already established by
47
- * this function's own equivalent gate (present on both exported overloads) at the real call site.
48
- *
49
- * **5. `stop()` always closes the durable `EventLog`, even when the caller's `onShutdown` hook
50
- * rejects** — `try { await config.onShutdown?.() } finally { await eventLog.close() }` — so a
51
- * caller-supplied hook failing can never leak an open sqlite file handle; the original rejection
52
- * still propagates to whoever awaited `stop()`.
53
- *
54
- * **6. `env[JINI_BIND_HOST]` is set before serving any request — a documented, only-partially-fixable
55
- * wrinkle.** `@jini/http`'s own `guardSameOrigin` (used by `registerDaemonStatusRoutes`' shutdown
56
- * route) resolves `bindHost` purely from real `process.env.JINI_BIND_HOST`, with **no parameter path**
57
- * of its own — unlike most of that module's other functions, it never accepts an injected `env`.
58
- * `createLocalNodeDaemon` sets `env[JINI_BIND_HOST] = host` (where `env` is `config.env ??
59
- * process.env`) before returning, which correctly fixes the common case (no `env` override — the
60
- * default, real `process.env` gets the right value). When a caller *does* inject a distinct `env`
61
- * object for full test isolation, this line cannot make `guardSameOrigin`'s hardwired real-`process.env`
62
- * read reflect it — a pre-existing `@jini/http` limitation this call cannot reach around without
63
- * editing `origin.ts` itself (out of scope here, same reasoning as the origin-validation duplication
64
- * flagged in `packages/http/archived provenance ledger`).
65
- *
66
- * ## 2026-07-29 — post-merge audit fixes (six teardown/config/URL defects)
67
- *
68
- * Independent review of the `feat/agentic-capability-layer` merge (OpenAI Codex `gpt-5.6-sol` as peer
69
- * reviewer, then re-verified against source here) surfaced six real defects in this package. Each was
70
- * reproduced with a failing test before any fix landed.
71
- *
72
- * **1. Caller packs were never disposed.** `composeJiniKernel` calls `createDaemon` on `config.packs`,
73
- * so it — not the caller — builds their services; but neither the failure-cleanup path nor `close()`
74
- * ever ran their `dispose`. A caller pack whose `services()` opened a socket, timer or database handle
75
- * leaked it past `createLocalNodeDaemon().stop()`, and the wrapper exposes no caller daemon for the
76
- * host to tear down itself. Added `JiniKernel.disposeCallerPacks()` — memoized, best-effort, symmetric
77
- * with `disposeFeatures()` — called by `close()`, by the composition-failure `catch`, and by the
78
- * daemon's `stop()`. Disposal order is reverse of construction: features first (they were composed
79
- * against an already-built caller daemon), caller packs second.
80
- *
81
- * **2. `journal.db` opened outside the cleanup block.** `createJiniKernelBase` opened `events.db`, then
82
- * `journal.db`, and only *then* entered the `try` whose `catch` closed them. `journal.db` failing (a
83
- * corrupt file, or a directory sitting where a database was expected) left the `events.db` handle open
84
- * in a process that has no kernel. Every acquisition now happens inside the one block, tracked in an
85
- * `opened` array the cleanup drains — the shape the module's own comment already claimed.
86
- *
87
- * **3. Capability keys were not runtime-validated.** Feature ids in `config.features` threw on a typo;
88
- * capability ids in `config.capabilities` did not. `{'host:exce': false}` deleted a grant nobody held,
89
- * so `host:exec` survived and `terminal`/`hostTools` stayed mounted — a security switch that fails
90
- * *open* on a typo, in the direction that reads as applied. `feature.ts` now carries the runtime half
91
- * of `CapabilityId` as a `Record<CapabilityId, true>` (drift is a compile error in both directions, so
92
- * the list cannot silently fall behind the union it validates) plus `CAPABILITY_IDS`/`isCapabilityId`;
93
- * `resolveFeatureActivation` validates the keys next to where it validates feature ids.
94
- *
95
- * **4. An injected `config.env` never reached the per-route same-origin guard.** `env[JINI_BIND_HOST] =
96
- * host` writes into the caller's object when one is supplied, and `@jini-ai/http-kit`'s `guardSameOrigin`
97
- * read real `process.env`. The origin-guard *middleware* was already handed the injected `env`, so the
98
- * two halves of one decision read two different environments: the middleware admitted an origin the
99
- * host configured through `config.env` and the route then rejected it. Fixed at the seam —
100
- * `OriginContext` (and so `AdapterContext`) grew an optional `env`, defaulting to `process.env`, and
101
- * this preset sets it. That covers `JINI_BIND_HOST`, `JINI_ALLOWED_ORIGINS` and `JINI_WEB_PORT` alike.
102
- *
103
- * **5. Shutdown could skip its own teardown, or crash the process.** `closeHttpServer` rejecting (which
104
- * its own contract documents) skipped feature disposal, discovery-record removal, `onShutdown` and the
105
- * sqlite close — leaving a half-torn-down daemon, strictly worse than the failure that caused it. It is
106
- * a `try`/`finally` now; the original rejection still propagates once teardown finishes. Separately,
107
- * `requestShutdown` dropped `stop()`'s promise with a bare `void`, so a failing shutdown became an
108
- * unhandled rejection — which on Node's default policy terminates the process this was gracefully
109
- * shutting down. It is reported through `console.error` now; the rejection stays on `stopPromise` so an
110
- * explicit `stop()` caller still observes it.
111
- *
112
- * **6. `resolveReportHost` produced an unparseable URL for an IPv6 bind host.** `'::1'` passed through
113
- * unchanged, so the reported base URL was `http://::1:54321` — which `new URL()` and `fetch` both
114
- * reject, and which was written verbatim into the discovery record. It now returns the bracketed
115
- * authority form (RFC 3986 §3.2.2), idempotently.
116
- *
117
- * Two lower-severity items from the same review, also fixed:
118
- *
119
- * - **`agents`' scan cache was not keyed by promise identity.** A slow scan that failed *after* `POST
120
- * /api/agents/rescan` had already replaced it with a successful one cleared the newer entry, forcing
121
- * a duplicate probe of every agent CLI on the machine. The `catch` now clears only its own entry.
122
- * - **`createFrontendControl`'s bind boundary did not cover `resolveBindToken` or its own error sink.**
123
- * A `RunStartHandler` throwing is not a no-op — `@jini-ai/http-kit`'s run-start route marks the run
124
- * `failed` and answers 500 — so a host-supplied `resolveBindToken` throwing (it parses an opaque host
125
- * blob) produced exactly the killed run the `onBindError` doc promises never to cause. Both are inside
126
- * the boundary now; a throwing sink is swallowed, because the reporting channel is what failed.
127
- *
128
- * **One pre-existing gap closed while here.** The package's committed 100% function-coverage gate was
129
- * already failing on `main` (99.09%): `sidecar-strict`'s origin-guard `getResolvedPort` closure had no
130
- * end-to-end coverage, because both of that mode's gates are request-time middleware. Added six real
131
- * `security: sidecar-strict` tests (unauthenticated 401, authenticated 201, cross-origin 403 with a
132
- * valid token, unset-token 503, probe routes reachable tokenless, `exemptPaths`) — the one block in
133
- * `compose-jini-kernel.test.ts` that opens a socket, and only because it has to.
134
- *
135
- * **Verified, personally, this session**: `packages/server` **231/231 passing** across 8 files (was
136
- * 215 before the sidecar-strict block), **100/100/100/100 coverage**; `packages/http-kit` 1245/1245 and
137
- * 100% ; `packages/chat-core` 261/261; `packages/chat-react` 610/610 and 100%;
138
- * `examples/reference-web` 41/41. Repo-wide `pnpm typecheck` and `pnpm guard` clean.
4
+ * Node host preset: createLocalNodeDaemon assembles core, daemon, concern-owned
5
+ * storage and http-kit into a listening service. composeJiniKernel is the same
6
+ * composition core for hosts mounting their own app without a second listener.
7
+ *
8
+ * Composition and lifecycle invariants:
9
+ * - The preset's two overloads preserve the missing-binding type gate: defaulted
10
+ * BoundIds would defeat inference from a custom bindings callback. Core's
11
+ * internal token-gate types are reused, rather than reimplemented. The cast
12
+ * inside the generic body is protected by both exported overloads' gate.
13
+ * - resolveBoundPort and resolveReportHost are pure helpers so defensive address
14
+ * checks are directly testable without a listener. Wildcard hosts are reported
15
+ * as loopback and IPv6 authorities are bracketed so discovery URLs are usable.
16
+ * - Resource acquisition stays inside cleanup boundaries so a later database
17
+ * open cannot leak an earlier handle. Features dispose before caller packs,
18
+ * reversing construction; the preset constructed those services and owns
19
+ * teardown. Listener-close or onShutdown failures cannot skip storage close,
20
+ * feature disposal or discovery removal. Explicit stop callers observe failures;
21
+ * requested shutdown failures are reported rather than dropped as rejections.
22
+ * - Feature and capability keys are runtime-validated: silently accepting a typo
23
+ * in a disabled capability would leave the real security-sensitive grant active.
24
+ * - Bind-host configuration is set before serving requests. The injected env is
25
+ * shared with origin middleware and route guards so both enforce the same policy.
26
+ * - Agent scan cache cleanup checks promise identity so an older failed scan
27
+ * cannot clear its replacement. Frontend binding contains resolver and error-sink
28
+ * failures so a host binding error cannot fail the underlying run.
29
+ *
30
+ * Frontend control belongs to daemon/http; this entry retains its public forwarding
31
+ * exports. See owning modules and packages/server/README.md for detailed contracts.
139
32
  */
140
33
  export type { CreateLocalNodeDaemonConfig, KernelBoundIds, LocalNodeDaemon, LocalNodeHttpExtension, LocalNodeHttpExtensionContext, } from './create-local-node-daemon.js';
141
34
  export { buildDaemonDbOperations, classifyRunFailureForRetry, createLocalNodeDaemon, projectDetectedAgent, resolveBoundPort, resolveReportHost, } from './create-local-node-daemon.js';
@@ -70,8 +70,8 @@ export async function createJiniKernelBase(options) {
70
70
  if (sqliteStorage && eventsDbPath !== null && dataDir !== null) {
71
71
  // A *second* connection to the same `events.db` the log above owns — safe: both run in WAL
72
72
  // mode, which permits multiple concurrently open handles on one file within a single process.
73
- featureConnection = openSqliteConnection({ filePath: eventsDbPath, open(file, settings) {
74
- featureConnection = sqliteStorage.open(file, settings);
73
+ featureConnection = openSqliteConnection({ filePath: eventsDbPath, open({ filePath }, settings) {
74
+ featureConnection = sqliteStorage.open(filePath, settings);
75
75
  return featureConnection;
76
76
  } });
77
77
  sqlite = { connection: featureConnection, eventsDbPath, dataDir };
@@ -39,7 +39,7 @@ export function openDatabase({ projectRoot, open }, options = {}) {
39
39
  (options.filesystem ?? fs).mkdirSync(dir, { recursive: true });
40
40
  let db;
41
41
  try {
42
- db = openSqliteConnection({ filePath: file, open(file, settings) { db = open(file, settings); return db; }, pragmas: ['journal_mode = WAL', 'foreign_keys = ON'] });
42
+ db = openSqliteConnection({ filePath: file, open({ filePath }, settings) { db = open(filePath, settings); return db; } }, { pragmas: ['journal_mode = WAL', 'foreign_keys = ON'] });
43
43
  migrate({ db });
44
44
  dbInstance = db;
45
45
  dbFile = file;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/server",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,7 +11,12 @@
11
11
  "domain": "server",
12
12
  "kind": "host",
13
13
  "runtime": "node",
14
- "admission": "locked"
14
+ "admission": "locked",
15
+ "isolation": {
16
+ "entries": {
17
+ "./store/projects/sqlite": "storage"
18
+ }
19
+ }
15
20
  },
16
21
  "type": "module",
17
22
  "main": "./dist/index.js",
@@ -54,35 +59,68 @@
54
59
  "access": "public",
55
60
  "registry": "https://registry.npmjs.org"
56
61
  },
57
- "dependencies": {
58
- "express": "^4.21.0",
59
- "@jini-ai/agent-runtime": "0.4.0",
60
- "@jini-ai/core": "0.4.0",
61
- "@jini-ai/http-kit": "0.4.0",
62
- "@jini-ai/daemon": "0.5.0",
63
- "@jini-ai/sidecar": "0.4.0",
64
- "@jini-ai/db": "0.2.0",
65
- "@jini-ai/registry": "0.5.0",
66
- "@jini-ai/chat": "0.5.0"
67
- },
68
62
  "devDependencies": {
69
63
  "@types/better-sqlite3": "^7.6.11",
70
64
  "@types/express": "^4.17.21",
71
65
  "@vitest/coverage-v8": "^2.1.9",
72
- "better-sqlite3": "^13.0.0"
66
+ "better-sqlite3": "^13.0.0",
67
+ "express": "^4.21.0",
68
+ "@jini-ai/agent-runtime": "0.4.4",
69
+ "@jini-ai/daemon": "0.6.0",
70
+ "@jini-ai/sidecar": "0.4.1",
71
+ "@jini-ai/http-kit": "0.4.1",
72
+ "@jini-ai/core": "0.4.2",
73
+ "@jini-ai/registry": "0.5.2",
74
+ "@jini-ai/db": "0.3.1",
75
+ "@jini-ai/chat": "0.5.7"
76
+ },
77
+ "dependencies": {
78
+ "@jini-ai/core": "^0.4.2"
73
79
  },
74
80
  "peerDependencies": {
75
- "better-sqlite3": "^13.0.0"
81
+ "better-sqlite3": "^13.0.0",
82
+ "@jini-ai/agent-runtime": "^0.4.4",
83
+ "@jini-ai/daemon": "^0.6.0",
84
+ "@jini-ai/http-kit": "^0.4.0",
85
+ "@jini-ai/sidecar": "^0.4.0",
86
+ "express": "^4.21.0",
87
+ "@jini-ai/db": "^0.3.0",
88
+ "@jini-ai/chat": "^0.5.7",
89
+ "@jini-ai/registry": "^0.5.1"
76
90
  },
77
91
  "peerDependenciesMeta": {
78
92
  "better-sqlite3": {
79
93
  "optional": true
94
+ },
95
+ "@jini-ai/agent-runtime": {
96
+ "optional": true
97
+ },
98
+ "@jini-ai/daemon": {
99
+ "optional": true
100
+ },
101
+ "@jini-ai/http-kit": {
102
+ "optional": true
103
+ },
104
+ "@jini-ai/sidecar": {
105
+ "optional": true
106
+ },
107
+ "express": {
108
+ "optional": true
109
+ },
110
+ "@jini-ai/db": {
111
+ "optional": true
112
+ },
113
+ "@jini-ai/chat": {
114
+ "optional": true
115
+ },
116
+ "@jini-ai/registry": {
117
+ "optional": true
80
118
  }
81
119
  },
82
120
  "sideEffects": false,
83
121
  "scripts": {
84
- "build": "tsc -p tsconfig.json",
85
- "typecheck": "tsc -p tsconfig.json --noEmit",
122
+ "build": "tsc -p tsconfig.tests.json && tsc -p tsconfig.json",
123
+ "typecheck": "tsc -p tsconfig.tests.json && tsc -p tsconfig.json --noEmit",
86
124
  "test": "vitest run",
87
125
  "test:coverage": "vitest run --coverage"
88
126
  }
@@ -1,76 +0,0 @@
1
- import type { ToolPolicy, ToolRegistration } from '@jini-ai/core';
2
- import { type FrontendCapabilitySpec } from '@jini-ai/daemon';
3
- import { type RunCreateRequest, type RunStartHandler } from '@jini-ai/http-kit';
4
- import type { LocalNodeHttpExtension } from './create-local-node-daemon.js';
5
- export interface FrontendBindErrorContext {
6
- readonly runId: string;
7
- readonly error: unknown;
8
- }
9
- export interface CreateFrontendControlOptions {
10
- /**
11
- * The capabilities to expose. `@jini-ai/agentic`'s `PAGE_CAPABILITIES` and `@jini-ai/chat-core`'s
12
- * `CHAT_CAPABILITIES` satisfy this structurally — the engine never imports that vocabulary
13
- * (see `frontend-capability-tools.ts`'s module doc for why the edge points one way).
14
- */
15
- readonly capabilities: readonly FrontendCapabilitySpec[];
16
- /**
17
- * Reads this run's bind token out of the host's own run request, or returns `undefined` when the
18
- * run has no originating surface.
19
- *
20
- * Required, and deliberately not defaulted. A default would have to guess where a host keeps the
21
- * token, and guessing wrong fails *open* in the confusing direction: the run starts, the agent
22
- * gets its tools, and every page call fails later with "no frontend is bound" — a symptom that
23
- * looks like a browser problem and is actually a config one.
24
- *
25
- * Returning `undefined` is normal, not an error. A run started from a CLI has no surface, and
26
- * capabilities whose `surface` is `'server'` do not need one.
27
- */
28
- readonly resolveBindToken: (request: RunCreateRequest) => string | undefined;
29
- /** @default `@jini-ai/daemon`'s `denyAllFrontendCapabilityPolicy` — a host grants access explicitly. */
30
- readonly policy?: ToolPolicy;
31
- /** @default `@jini-ai/daemon`'s `DEFAULT_FRONTEND_CAPABILITY_TIMEOUT_MS`. */
32
- readonly timeoutMs?: number;
33
- /** Applied to every registration. Frontend output is untrusted page text; bound it. */
34
- readonly maxOutputBytes?: number;
35
- /**
36
- * Host-owned sink for a bind failure. Defaults to `console.error`.
37
- *
38
- * A failed bind does not fail the run. The run is still legitimate — the agent simply cannot
39
- * drive the page, and every capability call it makes will say so by name. Killing a run because
40
- * one optional channel could not be established would turn a degraded session into no session.
41
- */
42
- readonly onBindError?: (context: FrontendBindErrorContext) => void;
43
- }
44
- export interface FrontendControl {
45
- /** Mounts the frontend-session stream and response routes. Pass in `httpExtensions`. */
46
- readonly httpExtension: LocalNodeHttpExtension;
47
- /** One gated tool per capability. Pass in `toolRegistrations`. */
48
- readonly toolRegistrations: readonly ToolRegistration[];
49
- /**
50
- * Binds each starting run to the surface that originated it, and releases the binding when the
51
- * run reaches a terminal state so a long-lived tab does not accumulate dead bindings.
52
- *
53
- * Compose it with whatever the host already does on run start — this hook only binds, it never
54
- * executes anything:
55
- *
56
- * ```ts
57
- * onRunStarted: (context) => {
58
- * frontend.bindOnStarted(context);
59
- * void startMyAgent(context);
60
- * }
61
- * ```
62
- */
63
- readonly bindOnStarted: RunStartHandler;
64
- }
65
- /**
66
- * Builds the frontend-control bundle.
67
- *
68
- * @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
69
- * @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
70
- * @throws Never at construction. A duplicate capability id throws later, when the registrations are
71
- * registered, naming the id (`ToolRegistry.register`'s own error).
72
- *
73
- * @complexity O(n) in `capabilities`.
74
- */
75
- export declare function createFrontendControl(options: CreateFrontendControlOptions): FrontendControl;
76
- //# sourceMappingURL=frontend-control.d.ts.map
@@ -1,77 +0,0 @@
1
- import { createFrontendCapabilityRegistrations, createFrontendSessionRegistry, } from '@jini-ai/daemon';
2
- import { registerFrontendSessionRoutes, } from '@jini-ai/http-kit';
3
- function defaultBindErrorSink(context) {
4
- // eslint-disable-next-line no-console
5
- console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, context.error);
6
- }
7
- /**
8
- * Builds the frontend-control bundle.
9
- *
10
- * @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
11
- * @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
12
- * @throws Never at construction. A duplicate capability id throws later, when the registrations are
13
- * registered, naming the id (`ToolRegistry.register`'s own error).
14
- *
15
- * @complexity O(n) in `capabilities`.
16
- */
17
- export function createFrontendControl(options) {
18
- // Closed over, never exposed. See the module doc: reaching this is reaching an ungated `invoke`.
19
- const registry = createFrontendSessionRegistry();
20
- const onBindError = options.onBindError ?? defaultBindErrorSink;
21
- const toolRegistrations = createFrontendCapabilityRegistrations({
22
- registry,
23
- capabilities: options.capabilities,
24
- ...(options.policy !== undefined ? { policy: options.policy } : {}),
25
- ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
26
- ...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
27
- });
28
- const httpExtension = (app, context) => {
29
- registerFrontendSessionRoutes(app, { registry }, context.adapter);
30
- };
31
- /**
32
- * The error boundary the `onBindError` contract actually needs. A `RunStartHandler` throwing is
33
- * not a no-op: `@jini-ai/http-kit`'s run-start route catches it, marks the run `failed` and answers
34
- * 500 — so anything inside this hook that can throw can produce exactly the killed run that
35
- * contract promises never to cause. `resolveBindToken` is host-supplied and reads an opaque host
36
- * blob (a `JSON.parse` away from throwing), and the sink itself is host-supplied too. A sink that
37
- * throws leaves nowhere left to report, so that one is swallowed rather than escalated.
38
- */
39
- const reportBindError = (context) => {
40
- try {
41
- onBindError(context);
42
- }
43
- catch {
44
- // Deliberately terminal: the reporting channel is what failed.
45
- }
46
- };
47
- const bindOnStarted = ({ request, run, lifecycle }) => {
48
- let bindToken;
49
- try {
50
- bindToken = options.resolveBindToken(request);
51
- }
52
- catch (error) {
53
- reportBindError({ runId: run.id, error });
54
- return;
55
- }
56
- if (bindToken === undefined)
57
- return;
58
- let release;
59
- try {
60
- release = registry.bindRunByToken(run.id, bindToken);
61
- }
62
- catch (error) {
63
- reportBindError({ runId: run.id, error });
64
- return;
65
- }
66
- // Release on terminal so a tab that outlives many runs does not accumulate bindings for runs
67
- // that ended. `waitForTerminal` rejecting is itself a reason to release, not to keep waiting,
68
- // so both paths run it — and the whole chain is swallowed, because a bookkeeping failure must
69
- // never surface as an unhandled rejection that takes the process down.
70
- void lifecycle
71
- .waitForTerminal(run.id)
72
- .then(release, release)
73
- .catch(() => undefined);
74
- };
75
- return { httpExtension, toolRegistrations, bindOnStarted };
76
- }
77
- //# sourceMappingURL=frontend-control.js.map