@jini-ai/server 0.0.0-stage → 0.5.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 +118 -0
- package/LICENSE +201 -0
- package/README.md +56 -2
- package/dist/builtin-features.d.ts +138 -0
- package/dist/builtin-features.js +631 -0
- package/dist/compose-jini-kernel.d.ts +162 -0
- package/dist/compose-jini-kernel.js +208 -0
- package/dist/create-local-node-daemon.d.ts +209 -0
- package/dist/create-local-node-daemon.js +282 -0
- package/dist/feature-activation.d.ts +86 -0
- package/dist/feature-activation.js +112 -0
- package/dist/feature.d.ts +153 -0
- package/dist/feature.js +94 -0
- package/dist/frontend-control.d.ts +76 -0
- package/dist/frontend-control.js +77 -0
- package/dist/host-bootstrap.d.ts +103 -0
- package/dist/host-bootstrap.js +146 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +11 -0
- package/dist/kernel-base.d.ts +83 -0
- package/dist/kernel-base.js +125 -0
- package/dist/lifecycle/boot-lifecycle.d.ts +61 -0
- package/dist/lifecycle/boot-lifecycle.js +74 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/readiness-state.d.ts +6 -0
- package/dist/lifecycle/readiness-state.js +15 -0
- package/dist/storage/backend-config.d.ts +48 -0
- package/dist/storage/backend-config.js +61 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/storage/legacy/sqlite.d.ts +26 -0
- package/dist/storage/legacy/sqlite.js +63 -0
- package/dist/store/projects/sqlite.d.ts +161 -0
- package/dist/store/projects/sqlite.js +328 -0
- package/package.json +86 -3
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module builtin-features
|
|
3
|
+
*
|
|
4
|
+
* Every route family this package can mount, expressed as a `JiniFeature` whose `compose` returns
|
|
5
|
+
* one atomic `@jini-ai/core` `Pack`.
|
|
6
|
+
*
|
|
7
|
+
* The load-bearing property is that a family's **tools and its routes are the same pack**. Before
|
|
8
|
+
* this module, `create-local-node-daemon.ts` registered the terminal tool and the three `daemon.db.*`
|
|
9
|
+
* tools into a private registry in one place, and mounted `registerTerminalRoutes`/
|
|
10
|
+
* `registerDaemonDbRoutes` in another — so gating only the route registrations would have left
|
|
11
|
+
* `jini.terminal.create` registered and still reachable through the always-mounted
|
|
12
|
+
* `POST /api/delegated-tool-calls`. Here, a feature that is not selected contributes neither, and
|
|
13
|
+
* there is no second step to gate. `terminal: false` means the pty tool is *absent from the
|
|
14
|
+
* registry*, so a delegated call naming it is an unregistered-tool error rather than a shell.
|
|
15
|
+
*
|
|
16
|
+
* Features borrow kernel infrastructure (`ctx.kernel`) and never own it — see `kernel-base.ts` for
|
|
17
|
+
* why the sqlite connection in particular is shared rather than feature-owned.
|
|
18
|
+
*/
|
|
19
|
+
import { statSync } from 'node:fs';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { definePack } from '@jini-ai/core';
|
|
22
|
+
import { detectAgents, getAgentDef } from '@jini-ai/agent-runtime';
|
|
23
|
+
import { createDefaultRunStartHandler, createRemoteToolEventRecorder, isAgentExecutorSupported, createTerminalSessionManager, createTerminalToolRegistrations, } from '@jini-ai/daemon';
|
|
24
|
+
import { ensureToolCatalogTables, getToolCatalogEntry, reseedToolCatalog, searchToolCatalog, } from '@jini-ai/registry/tool-catalog/sqlite';
|
|
25
|
+
import { inspectSqliteDatabase, verifySqliteIntegrity } from '@jini-ai/db/sqlite';
|
|
26
|
+
import { createDaemonDbToolRegistrations, denyAllWorkspaceRoots, registerActiveContextRoutes, registerAgentRoutes, registerConnectorsRoutes, registerDaemonDbRoutes, registerDaemonStatusRoutes, registerDelegatedToolRoutes, registerFrontendSessionRoutes, registerHealthRoutes, registerHostToolsRoutes, registerMediaRoutes, registerMemoryRoutes, registerModelProxyRoutes, registerRemoteRunEventRoutes, registerResearchRoutes, registerRoutineRoutes, registerRunRoutes, registerTerminalRoutes, registerToolCatalogRoutes, registerXaiRoutes } from '@jini-ai/daemon/http';
|
|
27
|
+
import { defineJiniFeature } from './feature.js';
|
|
28
|
+
/**
|
|
29
|
+
* The identity every zero-config, `ToolExecutor`-gated tool this module registers (`terminal.create`,
|
|
30
|
+
* `daemon.db.*`) executes as. A composition using these has no multi-tenant identity subsystem of
|
|
31
|
+
* its own — the bearer gate is authenticated-or-not, not per-caller — so every authenticated caller
|
|
32
|
+
* already shares one trust boundary and a single fixed `Principal` removes no distinction that
|
|
33
|
+
* existed before it.
|
|
34
|
+
*/
|
|
35
|
+
export const LOCAL_DAEMON_PRINCIPAL = { id: 'local-daemon' };
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
/** This package's own version, the default a composition reports when a host names none. */
|
|
38
|
+
const packageVersion = require('../package.json').version;
|
|
39
|
+
/**
|
|
40
|
+
* The identity a delegated tool call runs as when the host supplies no resolver. Carries **no
|
|
41
|
+
* roles**, deliberately.
|
|
42
|
+
*
|
|
43
|
+
* ## What actually makes this inert, because an earlier version of this comment named the wrong
|
|
44
|
+
* mechanism
|
|
45
|
+
*
|
|
46
|
+
* It previously claimed inertness came from "every registered tool's own deny-by-default
|
|
47
|
+
* `ToolPolicy`". That is **false**, and believing it is dangerous. `@jini-ai/cms` registers every
|
|
48
|
+
* one of its tools with a pass-through `policy: { authorize: () => 'allow' }` — deliberately, and
|
|
49
|
+
* documented as such in `core/tools/registration-kit.ts`: each tool's permission is evaluated
|
|
50
|
+
* exactly once, inside the domain function or via that file's `requireToolPermission`, so a second
|
|
51
|
+
* evaluator in the policy would be the bug rather than the guard.
|
|
52
|
+
*
|
|
53
|
+
* What actually denies this principal is the identity layer being fail-closed on an UNKNOWN
|
|
54
|
+
* subject: `identity/authorize.ts` starts with `principals.findById(...)` and returns
|
|
55
|
+
* `{ allowed: false, reason: 'principal_disabled' }` when no row comes back. `anonymous-delegated`
|
|
56
|
+
* has no row, so every permission-gated tool refuses it.
|
|
57
|
+
*
|
|
58
|
+
* ## The residual risk that follows from that, stated plainly
|
|
59
|
+
*
|
|
60
|
+
* The protection is the *tool's own* permission check, not this identity and not the registry. A
|
|
61
|
+
* tool registered by some other host with a permissive `ToolPolicy` **and** no internal permission
|
|
62
|
+
* check would execute for this principal. That is exactly why `@jini-ai/http-kit`'s
|
|
63
|
+
* `DelegatedToolsHttpDeps.resolvePrincipal` documents itself as mandatory, and why the fallback
|
|
64
|
+
* below warns rather than staying silent.
|
|
65
|
+
*/
|
|
66
|
+
export const ANONYMOUS_DELEGATED_PRINCIPAL = { id: 'anonymous-delegated' };
|
|
67
|
+
/**
|
|
68
|
+
* The `resolvePrincipal` fallback, wrapped so the fallback is announced once at composition rather
|
|
69
|
+
* than taken silently. Warns at wiring time, not per request: a per-request warning on a route that
|
|
70
|
+
* can be called in a loop is a log-flood, and the thing worth knowing is a deployment fact, not a
|
|
71
|
+
* request fact.
|
|
72
|
+
*/
|
|
73
|
+
function warnAndUseAnonymousDelegatedPrincipal() {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.warn('[@jini-ai/server] delegatedToolCalls is enabled without `resolvePrincipal`; delegated tool ' +
|
|
76
|
+
`calls will run as "${ANONYMOUS_DELEGATED_PRINCIPAL.id}", which every permission-gated tool ` +
|
|
77
|
+
'refuses (the identity layer is fail-closed on an unknown subject) but a tool with a ' +
|
|
78
|
+
'permissive ToolPolicy and no internal permission check would still execute for.');
|
|
79
|
+
return () => ANONYMOUS_DELEGATED_PRINCIPAL;
|
|
80
|
+
}
|
|
81
|
+
/** Thrown when a feature is activated without the host-supplied options it cannot default. */
|
|
82
|
+
function requireOptions(value, featureId, optionPath) {
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
throw new Error(`jini: feature "${featureId}" is active but "${optionPath}" was not supplied — ` +
|
|
85
|
+
`this feature has no zero-config default. Supply it, or disable "${featureId}".`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function requireSqlite(context, featureId) {
|
|
90
|
+
if (context.kernel.sqlite === null) {
|
|
91
|
+
throw new Error(`jini: feature "${featureId}" needs sqlite storage but this composition uses memory storage — ` +
|
|
92
|
+
`use storage: { kind: 'sqlite', dataDir } or disable "${featureId}".`);
|
|
93
|
+
}
|
|
94
|
+
return context.kernel.sqlite;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `DaemonDbOperations` over a caller-owned connection. `vacuum` measures the primary file's on-disk
|
|
98
|
+
* size before/after (not the `-wal`/`-shm` sum), since a fresh `VACUUM` checkpoints and shrinks the
|
|
99
|
+
* primary file itself, which is what "reclaimed" means here.
|
|
100
|
+
*/
|
|
101
|
+
export function buildDaemonDbOperations(db, file) {
|
|
102
|
+
return {
|
|
103
|
+
inspect: () => inspectSqliteDatabase({ db, file }),
|
|
104
|
+
verify: ({ quick }) => verifySqliteIntegrity({ db, quick }),
|
|
105
|
+
vacuum: () => {
|
|
106
|
+
const startedAt = Date.now();
|
|
107
|
+
const beforeBytes = statSync(file).size;
|
|
108
|
+
db.exec('VACUUM');
|
|
109
|
+
const afterBytes = statSync(file).size;
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
beforeBytes,
|
|
113
|
+
afterBytes,
|
|
114
|
+
reclaimedBytes: Math.max(0, beforeBytes - afterBytes),
|
|
115
|
+
elapsedMs: Date.now() - startedAt,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Whether the `agents` feature should advertise this probe result.
|
|
122
|
+
*
|
|
123
|
+
* Discovery scans every runtime definition, but `AgentExecutor` refuses to drive any def its
|
|
124
|
+
* `assessAgentExecutorCompatibility` rejects — see `@jini-ai/daemon`'s own module doc. All currently
|
|
125
|
+
* registered defs pass that check, but a future def may not. Advertising one that doesn't yields an
|
|
126
|
+
* agent a user can select and then cannot run — so the executor's own compatibility answer is applied
|
|
127
|
+
* here, at the surface that offers the choice.
|
|
128
|
+
*
|
|
129
|
+
* Assessed against the **full** `RuntimeAgentDef` resolved by id, not the `DetectedAgent` passed in:
|
|
130
|
+
* the projected type omits `maxPromptArgBytes`, and the argv-bound defs (`aider`, `deepseek`) qualify
|
|
131
|
+
* solely through it, so judging the projection would drop two working agents.
|
|
132
|
+
*
|
|
133
|
+
* An id with no registered def is **kept**, not dropped. A host supplying its own `detector` may
|
|
134
|
+
* legitimately surface agents outside `AGENT_DEFS`, driven by something other than this executor;
|
|
135
|
+
* there is nothing to assess, and refusing to advertise them would break that host on no evidence.
|
|
136
|
+
*
|
|
137
|
+
* @param agent - One probe result from the active detector.
|
|
138
|
+
* @returns `true` when the agent should appear in `GET /api/agents`.
|
|
139
|
+
* @complexity O(1) — one registry lookup plus fixed field checks.
|
|
140
|
+
* @overallScore 100/100
|
|
141
|
+
*/
|
|
142
|
+
export function isExecutableDetectedAgent(agent) {
|
|
143
|
+
const def = getAgentDef({ id: agent.id });
|
|
144
|
+
return def === null || isAgentExecutorSupported({ def });
|
|
145
|
+
}
|
|
146
|
+
/** Projects a real `DetectedAgent` probe onto the HTTP summary shape. */
|
|
147
|
+
export function projectDetectedAgent(agent) {
|
|
148
|
+
return {
|
|
149
|
+
id: agent.id,
|
|
150
|
+
name: agent.name,
|
|
151
|
+
available: agent.available,
|
|
152
|
+
...(agent.version !== undefined ? { version: agent.version } : {}),
|
|
153
|
+
...(agent.authStatus !== undefined ? { authStatus: agent.authStatus } : {}),
|
|
154
|
+
models: agent.models.map(({ id, label }) => ({ id, label })),
|
|
155
|
+
...(agent.reasoningOptions !== undefined
|
|
156
|
+
? { reasoningOptions: agent.reasoningOptions.map(({ id, label }) => ({ id, label })) }
|
|
157
|
+
: {}),
|
|
158
|
+
modelsSource: agent.modelsSource,
|
|
159
|
+
...(agent.supportsCustomModel !== undefined ? { supportsCustomModel: agent.supportsCustomModel } : {}),
|
|
160
|
+
...(agent.diagnostics?.[0]?.message ? { diagnostic: agent.diagnostics[0].message } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Builds the full built-in feature catalog with `options` closed over.
|
|
165
|
+
*
|
|
166
|
+
* Order is composition order, and it is deliberately the order `createLocalNodeDaemon` has always
|
|
167
|
+
* registered these families in — so the route-registration inventory, the tool-registration order,
|
|
168
|
+
* and therefore which id a duplicate-registration error names, are all unchanged.
|
|
169
|
+
*/
|
|
170
|
+
export function createBuiltInFeatures(options = {}) {
|
|
171
|
+
const healthFeature = defineJiniFeature({
|
|
172
|
+
id: 'health',
|
|
173
|
+
// No capability: a liveness/readiness probe grants no authority and is always permitted.
|
|
174
|
+
provides: [],
|
|
175
|
+
phase: 'probe',
|
|
176
|
+
compose: (context) => ({
|
|
177
|
+
pack: definePack({
|
|
178
|
+
name: 'jini.health',
|
|
179
|
+
deps: [],
|
|
180
|
+
services: () => ({ context }),
|
|
181
|
+
}, {
|
|
182
|
+
http: ({ app, services }) => {
|
|
183
|
+
const sqlite = services.context.kernel.sqlite;
|
|
184
|
+
const isShuttingDown = options.health?.isShuttingDown ?? (() => false);
|
|
185
|
+
registerHealthRoutes({ app: app, deps: {
|
|
186
|
+
getVersion: options.health?.getVersion ?? (() => packageVersion),
|
|
187
|
+
// Borrows the kernel's connection — see kernel-base.ts on why this is not
|
|
188
|
+
// `daemonDb`'s to own. A readiness check must never itself throw and crash the
|
|
189
|
+
// route: a thrown pragma read is a legitimate "not ready" signal, not a 500.
|
|
190
|
+
checkReadiness: async () => {
|
|
191
|
+
const notShuttingDown = !isShuttingDown();
|
|
192
|
+
if (sqlite === null)
|
|
193
|
+
return { ok: notShuttingDown, checks: { notShuttingDown } };
|
|
194
|
+
let dbOk;
|
|
195
|
+
try {
|
|
196
|
+
dbOk = verifySqliteIntegrity({ db: sqlite.connection, quick: true }).ok;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
dbOk = false;
|
|
200
|
+
}
|
|
201
|
+
return { ok: dbOk && notShuttingDown, checks: { db: dbOk, notShuttingDown } };
|
|
202
|
+
},
|
|
203
|
+
}, adapter: services.context.adapter });
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
const runsFeature = defineJiniFeature({
|
|
209
|
+
id: 'runs',
|
|
210
|
+
provides: ['run:transport'],
|
|
211
|
+
compose: (context) => {
|
|
212
|
+
// `onRunStarted` always wins when supplied. Otherwise a supplied `resolveRunInput` gets the
|
|
213
|
+
// default handler built for it, driving the kernel's own `AgentExecutor`. With neither, runs
|
|
214
|
+
// durably start with no driver attached.
|
|
215
|
+
const onStarted = options.runs?.onRunStarted ??
|
|
216
|
+
(options.runs?.resolveRunInput === undefined
|
|
217
|
+
? undefined
|
|
218
|
+
: createDefaultRunStartHandler({
|
|
219
|
+
agentExecutor: context.kernel.agentExecutor,
|
|
220
|
+
resolveRunInput: options.runs.resolveRunInput,
|
|
221
|
+
}));
|
|
222
|
+
return {
|
|
223
|
+
pack: definePack({
|
|
224
|
+
name: 'jini.runs',
|
|
225
|
+
deps: [],
|
|
226
|
+
services: () => ({ context }),
|
|
227
|
+
}, {
|
|
228
|
+
http: ({ app, services }) => {
|
|
229
|
+
registerRunRoutes({ app: app, deps: {
|
|
230
|
+
lifecycle: services.context.kernel.lifecycle,
|
|
231
|
+
...(onStarted === undefined ? {} : { onStarted }),
|
|
232
|
+
}, adapter: services.context.adapter });
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const agentsFeature = defineJiniFeature({
|
|
239
|
+
id: 'agents',
|
|
240
|
+
provides: ['agent:discovery'],
|
|
241
|
+
compose: (context) => {
|
|
242
|
+
// Promise-cached so concurrent clients never spawn duplicate probe sets; invalidated by
|
|
243
|
+
// POST /api/agents/rescan.
|
|
244
|
+
const detector = options.agents?.detector ?? (() => detectAgents({}));
|
|
245
|
+
let scan = null;
|
|
246
|
+
const scanAgents = (force) => {
|
|
247
|
+
if (force)
|
|
248
|
+
scan = null;
|
|
249
|
+
if (!scan) {
|
|
250
|
+
const pending = detector()
|
|
251
|
+
// Filtered before projection so the executor's answer is computed from the full def —
|
|
252
|
+
// see `isExecutableDetectedAgent`'s doc for why the projected shape is not enough.
|
|
253
|
+
.then((agents) => agents.filter(isExecutableDetectedAgent).map(projectDetectedAgent))
|
|
254
|
+
.catch((error) => {
|
|
255
|
+
// Keyed by promise identity, not "whatever is cached when this failure lands": a slow
|
|
256
|
+
// scan that fails after `POST /api/agents/rescan` already replaced it with a newer,
|
|
257
|
+
// successful one must invalidate only its own entry. Clearing unconditionally evicts
|
|
258
|
+
// the newer result and forces a duplicate probe of every agent CLI on the machine.
|
|
259
|
+
if (scan === pending)
|
|
260
|
+
scan = null;
|
|
261
|
+
throw error;
|
|
262
|
+
});
|
|
263
|
+
scan = pending;
|
|
264
|
+
}
|
|
265
|
+
return scan;
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
pack: definePack({
|
|
269
|
+
name: 'jini.agents',
|
|
270
|
+
deps: [],
|
|
271
|
+
services: () => ({ context }),
|
|
272
|
+
}, {
|
|
273
|
+
http: ({ app, services }) => {
|
|
274
|
+
registerAgentRoutes({ app: app, deps: { listAgents: () => scanAgents(false), rescanAgents: () => scanAgents(true) }, adapter: services.context.adapter });
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const hostToolsFeature = defineJiniFeature({
|
|
281
|
+
id: 'hostTools',
|
|
282
|
+
// Lists editors on PATH (`host:read`) and launches one (`host:exec`).
|
|
283
|
+
provides: ['host:read', 'host:exec'],
|
|
284
|
+
compose: (context) => ({
|
|
285
|
+
pack: definePack({
|
|
286
|
+
name: 'jini.hostTools',
|
|
287
|
+
deps: [],
|
|
288
|
+
services: () => ({ context }),
|
|
289
|
+
}, {
|
|
290
|
+
http: ({ app, services }) => {
|
|
291
|
+
registerHostToolsRoutes({ app: app, adapter: services.context.adapter }, {
|
|
292
|
+
resolveRoot: options.hostTools?.resolveWorkspaceRoot ?? denyAllWorkspaceRoots,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
const modelProxyFeature = defineJiniFeature({
|
|
299
|
+
id: 'modelProxy',
|
|
300
|
+
provides: ['net:egress'],
|
|
301
|
+
compose: (context) => ({
|
|
302
|
+
pack: definePack({
|
|
303
|
+
name: 'jini.modelProxy',
|
|
304
|
+
deps: [],
|
|
305
|
+
services: () => ({ context }),
|
|
306
|
+
}, {
|
|
307
|
+
http: ({ app, services }) => registerModelProxyRoutes({ app: app, deps: {}, adapter: services.context.adapter }),
|
|
308
|
+
}),
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
311
|
+
const activeContextFeature = defineJiniFeature({
|
|
312
|
+
id: 'activeContext',
|
|
313
|
+
provides: ['host:read'],
|
|
314
|
+
compose: (context) => ({
|
|
315
|
+
pack: definePack({
|
|
316
|
+
name: 'jini.activeContext',
|
|
317
|
+
deps: [],
|
|
318
|
+
services: () => ({ context }),
|
|
319
|
+
}, {
|
|
320
|
+
http: ({ app, services }) => registerActiveContextRoutes({ app: app, deps: // Mandatory but with an honest, harmless answer this package can always give:
|
|
321
|
+
// "unknown". It has no Project/Workspace noun of its own to resolve a name from.
|
|
322
|
+
{ resolveResource: () => undefined }, adapter: services.context.adapter }),
|
|
323
|
+
}),
|
|
324
|
+
}),
|
|
325
|
+
});
|
|
326
|
+
const terminalFeature = defineJiniFeature({
|
|
327
|
+
id: 'terminal',
|
|
328
|
+
provides: ['host:exec'],
|
|
329
|
+
compose: (context) => {
|
|
330
|
+
const manager = createTerminalSessionManager({});
|
|
331
|
+
return {
|
|
332
|
+
pack: definePack({
|
|
333
|
+
name: 'jini.terminal',
|
|
334
|
+
deps: [],
|
|
335
|
+
services: () => ({ context, manager }),
|
|
336
|
+
}, {
|
|
337
|
+
// THE gap closure: the pty tool and the pty routes are one contribution. A composition
|
|
338
|
+
// without this feature never registers `jini.terminal.create` at all, so a delegated call
|
|
339
|
+
// naming it is an unregistered-tool error — not a shell.
|
|
340
|
+
tools: ({ services }) => [
|
|
341
|
+
createTerminalToolRegistrations({ manager: services.manager }).create,
|
|
342
|
+
],
|
|
343
|
+
http: ({ app, services }) => registerTerminalRoutes({ app: app, deps: {
|
|
344
|
+
manager: services.manager,
|
|
345
|
+
toolExecutor: services.context.kernel.toolExecutor,
|
|
346
|
+
principal: options.terminal?.principal ?? LOCAL_DAEMON_PRINCIPAL,
|
|
347
|
+
resolveRoot: options.terminal?.resolveWorkspaceRoot ?? denyAllWorkspaceRoots,
|
|
348
|
+
}, adapter: services.context.adapter }),
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
const daemonDbFeature = defineJiniFeature({
|
|
354
|
+
id: 'daemonDb',
|
|
355
|
+
provides: ['db:admin'],
|
|
356
|
+
compose: (context) => {
|
|
357
|
+
const sqlite = requireSqlite(context, 'daemonDb');
|
|
358
|
+
// Borrowed, never owned: `kernel.close()` closes this connection. See kernel-base.ts.
|
|
359
|
+
const registrations = createDaemonDbToolRegistrations({
|
|
360
|
+
operations: buildDaemonDbOperations(sqlite.connection, sqlite.eventsDbPath),
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
pack: definePack({
|
|
364
|
+
name: 'jini.daemonDb',
|
|
365
|
+
deps: [],
|
|
366
|
+
services: () => ({ context }),
|
|
367
|
+
}, {
|
|
368
|
+
tools: () => [registrations.inspect, registrations.verify, registrations.vacuum],
|
|
369
|
+
http: ({ app, services }) => registerDaemonDbRoutes({ app: app, deps: {
|
|
370
|
+
toolExecutor: services.context.kernel.toolExecutor,
|
|
371
|
+
principal: options.daemonDb?.principal ?? LOCAL_DAEMON_PRINCIPAL,
|
|
372
|
+
}, adapter: services.context.adapter }),
|
|
373
|
+
}),
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
const toolCatalogFeature = defineJiniFeature({
|
|
378
|
+
id: 'toolCatalog',
|
|
379
|
+
provides: ['tool:catalog'],
|
|
380
|
+
compose: (context) => {
|
|
381
|
+
const sqlite = requireSqlite(context, 'toolCatalog');
|
|
382
|
+
return {
|
|
383
|
+
pack: definePack({
|
|
384
|
+
name: 'jini.toolCatalog',
|
|
385
|
+
deps: [],
|
|
386
|
+
services: () => ({ context }),
|
|
387
|
+
}, {
|
|
388
|
+
http: ({ app, services }) => registerToolCatalogRoutes({ app: app, deps: {
|
|
389
|
+
catalog: {
|
|
390
|
+
search: ({ query }, { limit } = {}) => searchToolCatalog({ db: sqlite.connection, query }, limit === undefined ? {} : { limit }),
|
|
391
|
+
describe: ({ id }) => getToolCatalogEntry({ db: sqlite.connection, id }),
|
|
392
|
+
},
|
|
393
|
+
}, adapter: services.context.adapter }),
|
|
394
|
+
}),
|
|
395
|
+
// Runs once EVERY active feature's tools are registered — the one ordering a Pack cannot
|
|
396
|
+
// express itself. Seeding from inside this pack's own `tools` would snapshot a registry
|
|
397
|
+
// that is still being filled, and the catalog would be silently partial.
|
|
398
|
+
afterTools: () => {
|
|
399
|
+
ensureToolCatalogTables({ db: sqlite.connection });
|
|
400
|
+
reseedToolCatalog({ db: sqlite.connection, entries: context.kernel.registry.list({}).map((descriptor) => ({
|
|
401
|
+
id: descriptor.id,
|
|
402
|
+
description: descriptor.description ?? descriptor.id,
|
|
403
|
+
source: 'first-party',
|
|
404
|
+
...(descriptor.inputSchema === undefined ? {} : { inputSchema: descriptor.inputSchema }),
|
|
405
|
+
})) });
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
const resolveDelegatedPrincipal = options.delegatedToolCalls?.resolvePrincipal;
|
|
411
|
+
const delegatedToolCallsFeature = defineJiniFeature({
|
|
412
|
+
id: 'delegatedToolCalls',
|
|
413
|
+
provides: ['tool:delegated'],
|
|
414
|
+
requires: ['runs'],
|
|
415
|
+
compose: (context) => ({
|
|
416
|
+
pack: definePack({
|
|
417
|
+
name: 'jini.delegatedToolCalls',
|
|
418
|
+
deps: [],
|
|
419
|
+
services: () => ({ context }),
|
|
420
|
+
}, {
|
|
421
|
+
http: ({ app, services }) => registerDelegatedToolRoutes({ app: app, deps: {
|
|
422
|
+
lifecycle: services.context.kernel.lifecycle,
|
|
423
|
+
toolExecutor: services.context.kernel.toolExecutor,
|
|
424
|
+
// The same registry `toolExecutor` was built over, so a `requireReadOnly` call (the
|
|
425
|
+
// daemon-side half of `@jini-ai/mcp`'s `execute_readonly_delegated_tool`) is checked
|
|
426
|
+
// against the descriptor that will actually run. Omitting it would not weaken the
|
|
427
|
+
// gate — an unverifiable constraint is refused, never waived — it would just make the
|
|
428
|
+
// read-only gateway refuse everything, so a kernel host gets it wired by default.
|
|
429
|
+
toolRegistry: services.context.kernel.registry,
|
|
430
|
+
// `DelegatedToolsHttpDeps.resolvePrincipal` documents itself as MANDATORY — "there is
|
|
431
|
+
// no safe default identity this package could assume on a host's behalf" — and this
|
|
432
|
+
// line supplied one anyway, silently. The default stays (removing it is a breaking
|
|
433
|
+
// change for hosts already relying on it, and is the owner's call, not this file's)
|
|
434
|
+
// but it no longer happens quietly: a host that enables this route without deciding
|
|
435
|
+
// who its calls run as should learn that from a startup line, not from an audit.
|
|
436
|
+
// See ANONYMOUS_DELEGATED_PRINCIPAL for what does and does not make it inert.
|
|
437
|
+
resolvePrincipal: resolveDelegatedPrincipal
|
|
438
|
+
? ({ request }) => resolveDelegatedPrincipal(request)
|
|
439
|
+
: warnAndUseAnonymousDelegatedPrincipal(),
|
|
440
|
+
}, adapter: services.context.adapter }),
|
|
441
|
+
}),
|
|
442
|
+
}),
|
|
443
|
+
});
|
|
444
|
+
const connectorsFeature = defineJiniFeature({
|
|
445
|
+
id: 'connectors',
|
|
446
|
+
provides: ['net:egress'],
|
|
447
|
+
compose: (context) => ({
|
|
448
|
+
pack: definePack({
|
|
449
|
+
name: 'jini.connectors',
|
|
450
|
+
deps: [],
|
|
451
|
+
services: () => ({ context }),
|
|
452
|
+
}, {
|
|
453
|
+
http: ({ app, services }) => registerConnectorsRoutes({ app: app, deps: {}, adapter: services.context.adapter }),
|
|
454
|
+
}),
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
const researchFeature = defineJiniFeature({
|
|
458
|
+
id: 'research',
|
|
459
|
+
provides: ['net:egress'],
|
|
460
|
+
compose: (context) => ({
|
|
461
|
+
pack: definePack({
|
|
462
|
+
name: 'jini.research',
|
|
463
|
+
deps: [],
|
|
464
|
+
services: () => ({ context }),
|
|
465
|
+
}, {
|
|
466
|
+
http: ({ app, services }) => registerResearchRoutes({ app: app, deps: {}, adapter: services.context.adapter }),
|
|
467
|
+
}),
|
|
468
|
+
}),
|
|
469
|
+
});
|
|
470
|
+
const xaiFeature = defineJiniFeature({
|
|
471
|
+
id: 'xai',
|
|
472
|
+
provides: ['net:egress'],
|
|
473
|
+
compose: (context) => {
|
|
474
|
+
const sqlite = requireSqlite(context, 'xai');
|
|
475
|
+
// Owned here (rather than left to xai.ts's internal default) so this feature's `dispose` can
|
|
476
|
+
// close an in-flight OAuth loopback listener — otherwise the fixed 127.0.0.1:56121 socket
|
|
477
|
+
// outlives the composition by up to its 30-minute self-close timeout.
|
|
478
|
+
const listenerRef = options.xai?.listenerRef ?? { current: null };
|
|
479
|
+
return {
|
|
480
|
+
pack: definePack({
|
|
481
|
+
name: 'jini.xai',
|
|
482
|
+
deps: [],
|
|
483
|
+
services: () => ({ context, listenerRef }),
|
|
484
|
+
}, {
|
|
485
|
+
http: ({ app, services }) => registerXaiRoutes({ app: app, deps: { dataDir: sqlite.dataDir, listenerRef: services.listenerRef }, adapter: services.context.adapter }),
|
|
486
|
+
dispose: async ({ services }) => {
|
|
487
|
+
const listener = services.listenerRef.current;
|
|
488
|
+
services.listenerRef.current = null;
|
|
489
|
+
// Best-effort — the listener self-closes on its own timeout anyway.
|
|
490
|
+
if (listener)
|
|
491
|
+
await listener.stop();
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
const remoteRunEventsFeature = defineJiniFeature({
|
|
498
|
+
id: 'remoteRunEvents',
|
|
499
|
+
// Never granted by any shipped profile: writing agent events into a run this process does not
|
|
500
|
+
// own is strictly more authority than executing a tool through the policy-gated delegated route.
|
|
501
|
+
provides: ['run:inject'],
|
|
502
|
+
requires: ['runs'],
|
|
503
|
+
compose: (context) => ({
|
|
504
|
+
pack: definePack({
|
|
505
|
+
name: 'jini.remoteRunEvents',
|
|
506
|
+
deps: [],
|
|
507
|
+
services: () => ({ context }),
|
|
508
|
+
}, {
|
|
509
|
+
http: ({ app, services }) => registerRemoteRunEventRoutes({ app: app, deps: {
|
|
510
|
+
lifecycle: services.context.kernel.lifecycle,
|
|
511
|
+
recorder: createRemoteToolEventRecorder({ lifecycle: services.context.kernel.lifecycle }),
|
|
512
|
+
...(options.remoteRunEvents?.tokenConfig === undefined
|
|
513
|
+
? {}
|
|
514
|
+
: { tokenConfig: options.remoteRunEvents.tokenConfig }),
|
|
515
|
+
env: services.context.env,
|
|
516
|
+
}, adapter: services.context.adapter }),
|
|
517
|
+
}),
|
|
518
|
+
}),
|
|
519
|
+
});
|
|
520
|
+
const memoryFeature = defineJiniFeature({
|
|
521
|
+
id: 'memory',
|
|
522
|
+
provides: ['memory:store'],
|
|
523
|
+
compose: (context) => {
|
|
524
|
+
const deps = requireOptions(options.memory, 'memory', 'featureOptions.memory');
|
|
525
|
+
return {
|
|
526
|
+
pack: definePack({
|
|
527
|
+
name: 'jini.memory',
|
|
528
|
+
deps: [],
|
|
529
|
+
services: () => ({ context }),
|
|
530
|
+
}, {
|
|
531
|
+
http: ({ app, services }) => registerMemoryRoutes({ app: app, deps, adapter: services.context.adapter }),
|
|
532
|
+
}),
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
const routinesFeature = defineJiniFeature({
|
|
537
|
+
id: 'routines',
|
|
538
|
+
provides: ['routines:schedule'],
|
|
539
|
+
compose: (context) => {
|
|
540
|
+
const deps = requireOptions(options.routines, 'routines', 'featureOptions.routines');
|
|
541
|
+
return {
|
|
542
|
+
pack: definePack({
|
|
543
|
+
name: 'jini.routines',
|
|
544
|
+
deps: [],
|
|
545
|
+
services: () => ({ context }),
|
|
546
|
+
}, {
|
|
547
|
+
http: ({ app, services }) => registerRoutineRoutes({ app: app, deps, adapter: services.context.adapter }),
|
|
548
|
+
}),
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
const mediaFeature = defineJiniFeature({
|
|
553
|
+
id: 'media',
|
|
554
|
+
provides: ['media:generate'],
|
|
555
|
+
compose: (context) => {
|
|
556
|
+
const deps = requireOptions(options.media, 'media', 'featureOptions.media');
|
|
557
|
+
return {
|
|
558
|
+
pack: definePack({
|
|
559
|
+
name: 'jini.media',
|
|
560
|
+
deps: [],
|
|
561
|
+
services: () => ({ context }),
|
|
562
|
+
}, {
|
|
563
|
+
http: ({ app, services }) => registerMediaRoutes({ app: app, deps, adapter: services.context.adapter }),
|
|
564
|
+
}),
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
const frontendSessionsFeature = defineJiniFeature({
|
|
569
|
+
id: 'frontendSessions',
|
|
570
|
+
provides: ['ui:session'],
|
|
571
|
+
compose: (context) => {
|
|
572
|
+
const deps = requireOptions(options.frontendSessions, 'frontendSessions', 'featureOptions.frontendSessions');
|
|
573
|
+
return {
|
|
574
|
+
pack: definePack({
|
|
575
|
+
name: 'jini.frontendSessions',
|
|
576
|
+
deps: [],
|
|
577
|
+
services: () => ({ context }),
|
|
578
|
+
}, {
|
|
579
|
+
http: ({ app, services }) => registerFrontendSessionRoutes({ app: app, deps, adapter: services.context.adapter }),
|
|
580
|
+
}),
|
|
581
|
+
};
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
const daemonStatusFeature = defineJiniFeature({
|
|
585
|
+
id: 'daemonStatus',
|
|
586
|
+
provides: ['daemon:control'],
|
|
587
|
+
phase: 'status',
|
|
588
|
+
compose: (context) => {
|
|
589
|
+
const supplied = requireOptions(options.daemonStatus, 'daemonStatus', 'featureOptions.daemonStatus.requestShutdown');
|
|
590
|
+
const deps = {
|
|
591
|
+
getVersion: supplied.getVersion ?? (() => packageVersion),
|
|
592
|
+
host: supplied.host ?? '127.0.0.1',
|
|
593
|
+
getPort: supplied.getPort ?? (() => context.adapter.resolvedPortRef.current),
|
|
594
|
+
dataDir: supplied.dataDir ?? context.kernel.sqlite?.dataDir ?? '',
|
|
595
|
+
isShuttingDown: supplied.isShuttingDown ?? (() => false),
|
|
596
|
+
requestShutdown: supplied.requestShutdown,
|
|
597
|
+
};
|
|
598
|
+
return {
|
|
599
|
+
pack: definePack({
|
|
600
|
+
name: 'jini.daemonStatus',
|
|
601
|
+
deps: [],
|
|
602
|
+
services: () => ({ context }),
|
|
603
|
+
}, {
|
|
604
|
+
http: ({ app, services }) => registerDaemonStatusRoutes({ app: app, deps, adapter: services.context.adapter }),
|
|
605
|
+
}),
|
|
606
|
+
};
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
return [
|
|
610
|
+
healthFeature,
|
|
611
|
+
runsFeature,
|
|
612
|
+
agentsFeature,
|
|
613
|
+
hostToolsFeature,
|
|
614
|
+
modelProxyFeature,
|
|
615
|
+
activeContextFeature,
|
|
616
|
+
terminalFeature,
|
|
617
|
+
daemonDbFeature,
|
|
618
|
+
toolCatalogFeature,
|
|
619
|
+
delegatedToolCallsFeature,
|
|
620
|
+
connectorsFeature,
|
|
621
|
+
researchFeature,
|
|
622
|
+
xaiFeature,
|
|
623
|
+
remoteRunEventsFeature,
|
|
624
|
+
memoryFeature,
|
|
625
|
+
routinesFeature,
|
|
626
|
+
mediaFeature,
|
|
627
|
+
frontendSessionsFeature,
|
|
628
|
+
daemonStatusFeature,
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
//# sourceMappingURL=builtin-features.js.map
|