@jini-ai/server 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @module compose-jini-kernel
3
+ *
4
+ * The one composition path. Everything this package can mount goes through here — including
5
+ * `createLocalNodeDaemon`, which is now a listener/discovery wrapper over this function rather than
6
+ * a second, independently-drifting assembly.
7
+ *
8
+ * That single-path property is the point. When a preset assembles routes and tools inline *and* a
9
+ * separate "composable" surface exists beside it, the two disagree the moment someone adds a family
10
+ * to one and forgets the other — and the disagreement is invisible, because both compile. Here a
11
+ * new family is a `JiniFeature` in the catalog, and every consumer picks it up or explicitly does
12
+ * not; there is no second place to forget.
13
+ *
14
+ * Mounting order is fixed and load-bearing:
15
+ *
16
+ * 1. `probe`-phase routes — before the body parser and before any auth, so liveness/readiness never
17
+ * depends on either.
18
+ * 2. `sidecar-strict`'s bearer gate, when that mode is active — ahead of the body parser, so a
19
+ * request this gate will reject never has its body parsed at all. It needs only `req.path` and the
20
+ * `Authorization` header.
21
+ * 3. the JSON body parser, then (when this composition owns security) the remaining gates:
22
+ * `sidecar-strict`'s origin guard, or `jini-local`'s bearer + origin pair.
23
+ * 4. `api`-phase routes, in catalog order.
24
+ * 5. the caller's own `onAfterApiRoutes` hook — product HTTP extensions and pack routes.
25
+ * 6. `status`-phase routes, last, so a status surface reports on a fully assembled app.
26
+ *
27
+ * Tools are registered for every active feature **before any route mounts**, so a feature's routes
28
+ * can assume its own tools exist, and `afterTools` hooks (the tool catalog's durable snapshot) see
29
+ * the complete registry rather than a partial one.
30
+ */
31
+ import { type Express } from 'express';
32
+ import { type Bindings, type Daemon, type Pack, type ToolRegistration } from '@jini-ai/core';
33
+ import { type AdapterContext } from '@jini-ai/http-kit';
34
+ import { type BuiltInFeatureOptions } from './builtin-features.js';
35
+ import { type AnyPack, type CapabilityId, type JiniFeature, type JiniProfileId } from './feature.js';
36
+ import { type FeatureActivationPlan } from './feature-activation.js';
37
+ import { createJiniKernelBase, type JiniKernelBase, type JiniKernelStorage } from './kernel-base.js';
38
+ /** Replaceable server validation wording shared by every composition entry. */
39
+ export declare const defaultServerMessages: {
40
+ invalidSecurityMode: string;
41
+ };
42
+ /** The token ids every composition binds before any caller customization runs. */
43
+ export type KernelBoundIds = 'jini.eventLog' | 'jini.runLifecycle' | 'jini.agentExecutor';
44
+ /** How this composition's `/api` surface is guarded. */
45
+ export type JiniKernelSecurity =
46
+ /** The caller's app already owns authentication/origin policy. Nothing is installed. */
47
+ {
48
+ readonly mode: 'host';
49
+ }
50
+ /** This composition installs the loopback-daemon bearer gate and origin guard. */
51
+ | {
52
+ readonly mode: 'jini-local';
53
+ readonly host: string;
54
+ readonly apiToken?: {
55
+ readonly tokenEnvVar?: string;
56
+ readonly disableEnvVar?: string;
57
+ };
58
+ }
59
+ /**
60
+ * Like `jini-local`, but for a daemon whose threat model is **another process running as the same
61
+ * OS user**, not a remote attacker: every `/api` request must carry the bearer token, including
62
+ * one arriving from `127.0.0.1`, and an unconfigured token fails closed with 503 rather than
63
+ * silently serving unauthenticated callers.
64
+ *
65
+ * Use this when the daemon holds authority a co-resident process should not be able to borrow —
66
+ * starting real agent runs, executing tools against a real database. A loopback bind keeps remote
67
+ * hosts out but does nothing about a sibling process, which is precisely what `jini-local`'s
68
+ * loopback short-circuit trusts.
69
+ *
70
+ * `probe`-phase routes (health/readiness/version) mount before this gate and therefore stay
71
+ * reachable without a token — monitoring never needs a secret.
72
+ */
73
+ | {
74
+ readonly mode: 'sidecar-strict';
75
+ readonly host: string;
76
+ /**
77
+ * Env var carrying the required bearer token. Required with no default — this package does
78
+ * not name a host's secret, matching every other host-resolved seam in the engine.
79
+ */
80
+ readonly tokenEnvVar: string;
81
+ /**
82
+ * Exact `req.path` values this gate does not apply to. Defaults to none. Every entry is a hole
83
+ * in the gate, so each one belongs at the mount site with a stated reason — the intended use
84
+ * is a callback route whose only legitimate caller cannot yet present a credential.
85
+ */
86
+ readonly exemptPaths?: readonly string[];
87
+ };
88
+ export interface ComposeJiniKernelConfig {
89
+ /** The app to mount onto. Always caller-supplied — this function never opens a listener. */
90
+ readonly app: Express;
91
+ readonly adapter: AdapterContext;
92
+ readonly storage: JiniKernelStorage;
93
+ /** @default 'agent-core-v1' — the conservative embedding posture. */
94
+ readonly profile?: JiniProfileId;
95
+ /** Raises or lowers the profile's capability ceiling. A denial here always wins. */
96
+ readonly capabilities?: Readonly<Partial<Record<CapabilityId, boolean | undefined>>>;
97
+ /** Names built-in (or `extraFeatures`) features on/off within that ceiling. */
98
+ readonly features?: Readonly<Record<string, boolean | undefined>>;
99
+ readonly featureOptions?: BuiltInFeatureOptions;
100
+ /** Product features. Same shape, same activation rules, same atomicity — no second path. */
101
+ readonly extraFeatures?: readonly JiniFeature[];
102
+ /** Host-contributed tools, registered after every active feature's own. */
103
+ readonly toolRegistrations?: readonly ToolRegistration[];
104
+ /** Caller-owned `@jini-ai/core` packs, composed against the kernel bindings. */
105
+ readonly packs?: readonly AnyPack[];
106
+ readonly bindings?: (b: Bindings<KernelBoundIds>) => Bindings<string>;
107
+ readonly agentExecutor?: Parameters<typeof createJiniKernelBase>[0]['agentExecutor'];
108
+ /** Explicit trust policy; omitted and unknown modes are rejected before opening resources. */
109
+ readonly security: JiniKernelSecurity;
110
+ /** @default true — set false when the caller's app already parses JSON bodies. */
111
+ readonly installJsonBodyParser?: boolean;
112
+ /**
113
+ * Runs after `api`-phase routes and before `status`-phase ones.
114
+ *
115
+ * Receives `base` explicitly rather than letting the caller close over the `JiniKernel` this
116
+ * function returns: the hook fires *during* composition, so that binding does not exist yet and
117
+ * reading it would be a temporal-dead-zone `ReferenceError` at boot.
118
+ */
119
+ readonly onAfterApiRoutes?: (app: Express, daemon: Daemon<readonly AnyPack[]>, base: JiniKernelBase) => void;
120
+ /** @default process.env */
121
+ readonly env?: NodeJS.ProcessEnv;
122
+ }
123
+ export interface JiniKernel {
124
+ readonly base: JiniKernelBase;
125
+ /** The composed caller packs' services, exactly as `createDaemon` returns them. */
126
+ readonly daemon: Daemon<readonly AnyPack[]>;
127
+ /** What was mounted, why, and what it grants — the machine-readable answer to "is X on?". */
128
+ readonly activation: FeatureActivationPlan;
129
+ /**
130
+ * Runs every active feature pack's `dispose`, in reverse composition order, best-effort. Separate
131
+ * from {@link closeBase} so a composition root can interleave its own shutdown steps between the
132
+ * two — which is exactly what the standalone daemon does.
133
+ */
134
+ disposeFeatures(): Promise<void>;
135
+ /**
136
+ * Runs every **caller** pack's `dispose`, in reverse composition order, best-effort. Idempotent.
137
+ *
138
+ * This composition called `createDaemon` on `config.packs`, so it — not the caller — is what
139
+ * built their services; a pack whose `services()` opened a socket, a timer or a database handle
140
+ * has no other owner to release it. Kept separate from {@link disposeFeatures} for the same
141
+ * reason that one is separate from {@link closeBase}: a composition root shuts down in its own
142
+ * order, and the standalone daemon interleaves discovery-record removal and the host's
143
+ * `onShutdown` hook between these steps.
144
+ */
145
+ disposeCallerPacks(): Promise<void>;
146
+ /** Closes the kernel-owned sqlite handles. Idempotent. */
147
+ closeBase(): Promise<void>;
148
+ /** {@link disposeFeatures}, then {@link disposeCallerPacks}, then {@link closeBase}. */
149
+ close(): Promise<void>;
150
+ }
151
+ /**
152
+ * Composes the kernel, the selected feature set, and the caller's own packs onto `config.app`.
153
+ *
154
+ * @throws For any activation error (unknown feature id, denied capability under an explicit enable,
155
+ * unmet `requires`), any missing required feature option, a duplicate tool id, or a failed
156
+ * rehydration. Every kernel resource opened before the failure is disposed and closed before the
157
+ * error propagates, so a failed composition never leaks a sqlite file handle or a pty manager.
158
+ */
159
+ export declare function composeJiniKernel(config: ComposeJiniKernelConfig): Promise<JiniKernel>;
160
+ /** Re-exported so a caller can name the pack type without reaching into `@jini-ai/core`'s internals. */
161
+ export type { Pack };
162
+ //# sourceMappingURL=compose-jini-kernel.d.ts.map
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @module compose-jini-kernel
3
+ *
4
+ * The one composition path. Everything this package can mount goes through here — including
5
+ * `createLocalNodeDaemon`, which is now a listener/discovery wrapper over this function rather than
6
+ * a second, independently-drifting assembly.
7
+ *
8
+ * That single-path property is the point. When a preset assembles routes and tools inline *and* a
9
+ * separate "composable" surface exists beside it, the two disagree the moment someone adds a family
10
+ * to one and forgets the other — and the disagreement is invisible, because both compile. Here a
11
+ * new family is a `JiniFeature` in the catalog, and every consumer picks it up or explicitly does
12
+ * not; there is no second place to forget.
13
+ *
14
+ * Mounting order is fixed and load-bearing:
15
+ *
16
+ * 1. `probe`-phase routes — before the body parser and before any auth, so liveness/readiness never
17
+ * depends on either.
18
+ * 2. `sidecar-strict`'s bearer gate, when that mode is active — ahead of the body parser, so a
19
+ * request this gate will reject never has its body parsed at all. It needs only `req.path` and the
20
+ * `Authorization` header.
21
+ * 3. the JSON body parser, then (when this composition owns security) the remaining gates:
22
+ * `sidecar-strict`'s origin guard, or `jini-local`'s bearer + origin pair.
23
+ * 4. `api`-phase routes, in catalog order.
24
+ * 5. the caller's own `onAfterApiRoutes` hook — product HTTP extensions and pack routes.
25
+ * 6. `status`-phase routes, last, so a status surface reports on a fully assembled app.
26
+ *
27
+ * Tools are registered for every active feature **before any route mounts**, so a feature's routes
28
+ * can assume its own tools exist, and `afterTools` hooks (the tool catalog's durable snapshot) see
29
+ * the complete registry rather than a partial one.
30
+ */
31
+ import express, {} from 'express';
32
+ import { bindings, assertValidAllowedOrigins, configuredAllowedOrigins, createDaemon, disposePacks, registerPackTools, } from '@jini-ai/core';
33
+ import { AgentExecutorToken, EventLogToken, RunLifecycleToken } from '@jini-ai/daemon';
34
+ import { mountPackHttp, registerApiBearerAuthMiddleware, registerApiOriginGuardMiddleware, requireStrictBearerToken, } from '@jini-ai/http-kit';
35
+ import { createBuiltInFeatures } from './builtin-features.js';
36
+ import { JINI_PROFILES, } from './feature.js';
37
+ import { resolveFeatureActivation } from './feature-activation.js';
38
+ import { createJiniKernelBase } from './kernel-base.js';
39
+ /** Replaceable server validation wording shared by every composition entry. */
40
+ export const defaultServerMessages = { invalidSecurityMode: 'an explicit supported security mode is required' };
41
+ function mountPhase(phase, app, composed, daemon) {
42
+ for (const entry of composed) {
43
+ if ((entry.feature.phase ?? 'api') !== phase)
44
+ continue;
45
+ mountPackHttp({ app, packs: [entry.pack], daemon });
46
+ }
47
+ }
48
+ /**
49
+ * Composes the kernel, the selected feature set, and the caller's own packs onto `config.app`.
50
+ *
51
+ * @throws For any activation error (unknown feature id, denied capability under an explicit enable,
52
+ * unmet `requires`), any missing required feature option, a duplicate tool id, or a failed
53
+ * rehydration. Every kernel resource opened before the failure is disposed and closed before the
54
+ * error propagates, so a failed composition never leaks a sqlite file handle or a pty manager.
55
+ */
56
+ export async function composeJiniKernel(config) {
57
+ const security = config.security;
58
+ if (!security || !['host', 'jini-local', 'sidecar-strict'].includes(security.mode)) {
59
+ throw new Error(defaultServerMessages.invalidSecurityMode);
60
+ }
61
+ const env = config.env ?? process.env;
62
+ // Boot-time, unconditional — deliberately not scoped to `security.mode === 'sidecar-strict' |
63
+ // 'jini-local'` the way the two `configuredAllowedOrigins({ config, env })` calls below are. Those compute
64
+ // `extraAllowedOrigins` once at composition time too, but only for the two modes that mount
65
+ // `registerApiOriginGuardMiddleware`; the explicit `'host'` mode never reaches either call, so a
66
+ // malformed `JINI_ALLOWED_ORIGINS` would otherwise go undetected at boot regardless of mode and
67
+ // only surface later, inside `isLocalSameOrigin` (via `guardSameOrigin`, which every
68
+ // `mountJsonRoute` route and the attachment/model-proxy routes call), on whichever request
69
+ // happened to be first to need a same-origin decision. Asserting here — before any resource
70
+ // opens, matching the "fails before a single resource is opened" convention just below — turns
71
+ // that into a fail-fast boot error instead. `configuredAllowedOrigins` itself no longer throws
72
+ // (see its own doc): this is the ONE place that still does, deliberately.
73
+ const originConfig = { allowedOriginsEnvVar: 'JINI_ALLOWED_ORIGINS', webPortEnvVar: 'JINI_WEB_PORT', bindHostEnvVar: 'JINI_BIND_HOST' };
74
+ assertValidAllowedOrigins({ config: originConfig, env });
75
+ const profile = JINI_PROFILES[config.profile ?? 'agent-core-v1'];
76
+ const catalog = [
77
+ ...createBuiltInFeatures(config.featureOptions ?? {}),
78
+ ...(config.extraFeatures ?? []),
79
+ ];
80
+ // Pure, and first: an invalid selection fails before a single resource is opened.
81
+ const activation = resolveFeatureActivation({
82
+ features: catalog,
83
+ profile,
84
+ ...(config.capabilities === undefined ? {} : { capabilities: config.capabilities }),
85
+ ...(config.features === undefined ? {} : { featureOverrides: config.features }),
86
+ });
87
+ const base = await createJiniKernelBase({
88
+ storage: config.storage,
89
+ ...(config.agentExecutor === undefined ? {} : { agentExecutor: config.agentExecutor }),
90
+ });
91
+ const composed = [];
92
+ let featureDaemon = { services: {} };
93
+ let callerDaemon = { services: {} };
94
+ const callerPacks = config.packs ?? [];
95
+ try {
96
+ const kernelBindings = bindings({})
97
+ .bind({ token: EventLogToken, impl: base.eventLog })
98
+ .bind({ token: RunLifecycleToken, impl: base.lifecycle })
99
+ .bind({ token: AgentExecutorToken, impl: base.agentExecutor });
100
+ const boundBindings = config.bindings ? config.bindings(kernelBindings) : kernelBindings;
101
+ callerDaemon = createDaemon({ packs: callerPacks, bindings: boundBindings });
102
+ const featureContext = { kernel: base, adapter: config.adapter, env };
103
+ const byId = new Map(catalog.map((feature) => [feature.id, feature]));
104
+ for (const record of activation.active) {
105
+ const feature = byId.get(record.id);
106
+ const composition = feature.compose(featureContext);
107
+ composed.push({
108
+ feature,
109
+ pack: composition.pack,
110
+ ...(composition.afterTools === undefined ? {} : { afterTools: composition.afterTools }),
111
+ });
112
+ }
113
+ const featurePacks = composed.map((entry) => entry.pack);
114
+ featureDaemon = createDaemon({ packs: featurePacks, bindings: bindings({}) });
115
+ // Every tool, from every source, before any route mounts.
116
+ registerPackTools({ registry: base.registry, packs: featurePacks, daemon: featureDaemon });
117
+ // Host-contributed tools after the features' own, so a collision names the host's id rather
118
+ // than silently shadowing a built-in.
119
+ for (const registration of config.toolRegistrations ?? [])
120
+ base.registry.register(registration);
121
+ registerPackTools({ registry: base.registry, packs: callerPacks, daemon: callerDaemon });
122
+ // Only now is the registry complete — see `toolCatalog`'s own `afterTools` doc.
123
+ for (const entry of composed)
124
+ entry.afterTools?.();
125
+ const { app } = config;
126
+ mountPhase('probe', app, composed, featureDaemon);
127
+ // `sidecar-strict`'s bearer gate mounts BEFORE the body parser, unlike `jini-local`'s: a caller
128
+ // this gate is going to reject with 401 should never have had its body parsed in the first place.
129
+ // The gate reads only `req.path` and the `Authorization` header, so it needs nothing the parser
130
+ // provides. Its origin guard still mounts below, alongside `jini-local`'s.
131
+ if (security.mode === 'sidecar-strict') {
132
+ app.use(requireStrictBearerToken({
133
+ tokenEnvVar: security.tokenEnvVar,
134
+ env,
135
+ }, security.exemptPaths !== undefined ? { exemptPaths: security.exemptPaths } : {}));
136
+ }
137
+ if (config.installJsonBodyParser !== false)
138
+ app.use(express.json());
139
+ if (security.mode === 'sidecar-strict') {
140
+ registerApiOriginGuardMiddleware({ app, deps: {
141
+ host: security.host,
142
+ extraAllowedOrigins: configuredAllowedOrigins({ config: originConfig, env }),
143
+ getResolvedPort: () => config.adapter.resolvedPortRef.current,
144
+ env,
145
+ ...originConfig,
146
+ } });
147
+ }
148
+ if (security.mode === 'jini-local') {
149
+ registerApiBearerAuthMiddleware({ app,
150
+ tokenConfig: {
151
+ tokenEnvVar: security.apiToken?.tokenEnvVar ?? 'JINI_API_TOKEN',
152
+ disableEnvVar: security.apiToken?.disableEnvVar ?? 'JINI_DISABLE_API_AUTH',
153
+ },
154
+ env,
155
+ });
156
+ registerApiOriginGuardMiddleware({ app, deps: {
157
+ host: security.host,
158
+ extraAllowedOrigins: configuredAllowedOrigins({ config: originConfig, env }),
159
+ getResolvedPort: () => config.adapter.resolvedPortRef.current,
160
+ env,
161
+ ...originConfig,
162
+ } });
163
+ }
164
+ mountPhase('api', app, composed, featureDaemon);
165
+ config.onAfterApiRoutes?.(app, callerDaemon, base);
166
+ mountPhase('status', app, composed, featureDaemon);
167
+ }
168
+ catch (error) {
169
+ // Reverse of the order they were built: features composed against an already-constructed caller
170
+ // daemon, so they release first. The caller's packs are disposed here too — `createDaemon` ran
171
+ // their `services()` before the first feature composed, so a failure anywhere after that point
172
+ // leaves whatever those services opened with no owner at all.
173
+ await disposePacks({
174
+ packs: composed.map((entry) => entry.pack),
175
+ daemon: featureDaemon,
176
+ });
177
+ await disposePacks({ packs: callerPacks, daemon: callerDaemon });
178
+ await base.close();
179
+ throw error;
180
+ }
181
+ let disposedFeatures = null;
182
+ const disposeFeatures = () => {
183
+ disposedFeatures ??= disposePacks({
184
+ packs: composed.map((entry) => entry.pack),
185
+ daemon: featureDaemon,
186
+ }).then(() => undefined);
187
+ return disposedFeatures;
188
+ };
189
+ let disposedCallerPacks = null;
190
+ const disposeCallerPacks = () => {
191
+ disposedCallerPacks ??= disposePacks({ packs: callerPacks, daemon: callerDaemon }).then(() => undefined);
192
+ return disposedCallerPacks;
193
+ };
194
+ return {
195
+ base,
196
+ daemon: callerDaemon,
197
+ activation,
198
+ disposeFeatures,
199
+ disposeCallerPacks,
200
+ closeBase: () => base.close(),
201
+ async close() {
202
+ await disposeFeatures();
203
+ await disposeCallerPacks();
204
+ await base.close();
205
+ },
206
+ };
207
+ }
208
+ //# sourceMappingURL=compose-jini-kernel.js.map
@@ -0,0 +1,209 @@
1
+ import type { SqliteSyncOpener } from '@jini-ai/db/sqlite';
2
+ import type { Server } from 'node:http';
3
+ import { type Express } from 'express';
4
+ import type { DetectedAgent } from '@jini-ai/agent-runtime';
5
+ import type { Bindings, Principal, ToolRegistration } from '@jini-ai/core';
6
+ import type { AnyPack, MissingTokenIds } from '@jini-ai/core/composition';
7
+ import type { ResolveRunInput, RunLifecycle } from '@jini-ai/daemon';
8
+ import { type AdapterContext } from '@jini-ai/http-kit';
9
+ import type { DelegatedToolExecuteRequest, RunStartHandler, WorkspaceRootResolver } from '@jini-ai/daemon/http';
10
+ import { type JiniKernelSecurity, type KernelBoundIds } from './compose-jini-kernel.js';
11
+ import type { CapabilityId, JiniProfileId } from './feature.js';
12
+ export { buildDaemonDbOperations, projectDetectedAgent } from './builtin-features.js';
13
+ export { classifyRunFailureForRetry } from './kernel-base.js';
14
+ /**
15
+ * Extracts the real bound TCP port from `server.address()`'s result once a `'listening'` event
16
+ * has fired.
17
+ *
18
+ * @param address - The raw return value of `server.address()`.
19
+ * @returns The bound port, or `null` if `address` is `null` (not yet listening), a string (a Unix
20
+ * domain socket path — this module never listens on one), or an `AddressInfo` with a non-positive port.
21
+ * @complexity O(1).
22
+ * @overallScore 100/100
23
+ */
24
+ export declare function resolveBoundPort(address: {
25
+ port: number;
26
+ } | string | null): number | null;
27
+ /**
28
+ * The host a daemon's reported base URL should use: binding to every interface (`0.0.0.0` / `::`)
29
+ * is not itself a connectable address, so callers are told to use the IPv4 loopback address
30
+ * instead; and an IPv6 literal is returned in bracketed authority form.
31
+ *
32
+ * The brackets are not cosmetic. `http://${'::1'}:5000` is not a URL — `new URL()` throws on it and
33
+ * so does `fetch`, which made a daemon bound to `::1` hand back a base URL nothing could use and
34
+ * write that same unusable string into its discovery record. RFC 3986 §3.2.2 requires the brackets
35
+ * for any IPv6 literal in an authority, precisely so the address's own colons cannot be read as the
36
+ * port separator.
37
+ *
38
+ * @param bindHost - The literal host `createLocalNodeDaemon` bound to (already normalized).
39
+ * @returns `'127.0.0.1'` for an all-interfaces bind host, `'[…]'` for an IPv6 literal (already-
40
+ * bracketed input is returned unchanged), otherwise `bindHost` unchanged.
41
+ * @complexity O(1).
42
+ * @overallScore 100/100
43
+ */
44
+ export declare function resolveReportHost(bindHost: string): string;
45
+ export type { KernelBoundIds };
46
+ /**
47
+ * Product-level HTTP composition seam. Incubating capabilities register their routes here instead
48
+ * of becoming downward dependencies of this locked host preset.
49
+ */
50
+ export interface LocalNodeHttpExtensionContext {
51
+ readonly adapter: AdapterContext;
52
+ readonly lifecycle: RunLifecycle;
53
+ readonly dataDir: string;
54
+ }
55
+ export type LocalNodeHttpExtension = (app: Express, context: LocalNodeHttpExtensionContext) => void;
56
+ export interface CreateLocalNodeDaemonConfig<Packs extends readonly AnyPack[], BoundIds extends string = KernelBoundIds> {
57
+ /** Directory the daemon's durable state lives in. The directory itself must already exist. */
58
+ dataDir: string;
59
+ /** The external host supplies its driver; Jini never autoloads SQLite. */
60
+ open: SqliteSyncOpener;
61
+ packs: Packs;
62
+ /**
63
+ * Extends the kernel's own pre-bound `EventLog`/`RunLifecycle` bindings with whatever a pack's
64
+ * own deps require. A callback rather than a pre-built `Bindings` instance because
65
+ * `Bindings.bind()` mutates and returns `this`.
66
+ */
67
+ bindings?: (b: Bindings<KernelBoundIds>) => Bindings<BoundIds>;
68
+ /** TCP port to listen on. Defaults to `0` (ask the OS for an ephemeral free port). */
69
+ port?: number;
70
+ /** Host/address to bind to. Defaults to `'127.0.0.1'` (loopback-only). */
71
+ host?: string;
72
+ /** Env var names for the optional bearer-token gate. Defaults to `JINI_API_TOKEN` / `JINI_DISABLE_API_AUTH`. Ignored when `security` below selects `sidecar-strict`. */
73
+ apiToken?: {
74
+ tokenEnvVar?: string;
75
+ disableEnvVar?: string;
76
+ };
77
+ /**
78
+ * Raises this preset's default `jini-local` gate to `sidecar-strict` — see
79
+ * {@link JiniKernelSecurity}'s own doc for what that changes: every `/api` request must carry the
80
+ * bearer token, including one from `127.0.0.1`, and an unconfigured token fails closed rather than
81
+ * serving unauthenticated callers. `host` is supplied automatically from this config's own `host`
82
+ * (there is nowhere else for it to disagree with, so it is not this option's to set again).
83
+ * Omitted (the default) keeps `jini-local` exactly as every existing caller already has it.
84
+ */
85
+ security?: Omit<Extract<JiniKernelSecurity, {
86
+ readonly mode: 'sidecar-strict';
87
+ }>, 'host'>;
88
+ /** Invoked once the HTTP listener has fully closed, before the durable `EventLog` is closed. */
89
+ onShutdown?: () => Promise<void> | void;
90
+ /** Defaults to `process.env`. */
91
+ env?: NodeJS.ProcessEnv;
92
+ /** Optional detector override for a host with custom PATH/env policy or a deterministic test fixture. */
93
+ agentDetector?: () => Promise<readonly DetectedAgent[]>;
94
+ /**
95
+ * Optional host-owned driver attached immediately after `POST /api/runs` durably starts a run.
96
+ * Takes full precedence over {@link resolveRunInput} when both are supplied.
97
+ */
98
+ onRunStarted?: RunStartHandler;
99
+ /**
100
+ * Host-owned prompt/cwd/env composition seam. When supplied and `onRunStarted` is not, this
101
+ * daemon builds a default `RunStartHandler` that resolves each run's input through this seam and
102
+ * drives it straight to the zero-config `AgentExecutor`. Ignored when `onRunStarted` is supplied.
103
+ */
104
+ resolveRunInput?: ResolveRunInput;
105
+ /**
106
+ * Resolves a `resourceRef` to a filesystem working directory for `POST /api/resources/:resourceRef/open-in`.
107
+ * Defaults to `denyAllWorkspaceRoots`: the route exists and is reachable but denies every call
108
+ * with `404`, never fabricating or guessing a path.
109
+ */
110
+ resolveWorkspaceRoot?: WorkspaceRootResolver;
111
+ /**
112
+ * Optional product/capability route registrars. They run after the host's security middleware
113
+ * and locked route packs, before caller packs and daemon-status routes.
114
+ */
115
+ httpExtensions?: readonly LocalNodeHttpExtension[];
116
+ /**
117
+ * Host-contributed `{descriptor, handler, policy}` triples, registered into the same internal
118
+ * registry this preset's own gated tools (`jini.terminal.create`, `daemon.db.*`) use — and so
119
+ * executed through the same `ToolExecutor`, inheriting its authorization, confirmation, timeout,
120
+ * cancellation, output truncation, and audit trail.
121
+ *
122
+ * @throws At startup if a `descriptor.id` is already registered — by another entry here, or by
123
+ * one of the preset's own tools. The registry is append-only by design.
124
+ */
125
+ toolRegistrations?: readonly ToolRegistration[];
126
+ /**
127
+ * Resolves the `Principal` one `POST /api/delegated-tool-calls` request executes as.
128
+ *
129
+ * @default the anonymous, role-less delegated principal — inertness comes from deny-by-default
130
+ * tool policies, not from the identity.
131
+ */
132
+ resolveDelegatedPrincipal?: (request: DelegatedToolExecuteRequest) => Principal | Promise<Principal>;
133
+ /**
134
+ * Where this daemon's local discovery record (URL/host/port/pid) is written once it starts
135
+ * listening. Defaults to `<dataDir>/daemon.json`. Pass `false` to disable. Best-effort: a failure
136
+ * here never fails daemon startup or shutdown.
137
+ */
138
+ discoveryFile?: string | false;
139
+ /**
140
+ * Capability profile this daemon composes against.
141
+ *
142
+ * @default 'local-daemon-v1' — the immutable compatibility profile holding exactly the surface
143
+ * this preset has always mounted. Changing it changes which route families exist; see
144
+ * `feature.ts` for the vocabulary and why profiles are versioned rather than edited.
145
+ */
146
+ profile?: JiniProfileId;
147
+ /**
148
+ * Raises or lowers the profile's capability ceiling. A denial always wins: explicitly enabling a
149
+ * feature whose capability is denied here is a startup error naming both, never a silent
150
+ * re-opening of a coarse switch.
151
+ */
152
+ capabilities?: Readonly<Partial<Record<CapabilityId, boolean | undefined>>>;
153
+ /**
154
+ * Turns individual features on or off within whatever the capability ceiling permits — e.g.
155
+ * `{ terminal: false, daemonDb: false }`. A feature turned off here registers **neither its
156
+ * routes nor its tools**, so a disabled capability is absent from the shared registry rather than
157
+ * merely unrouted.
158
+ */
159
+ features?: Readonly<Record<string, boolean | undefined>>;
160
+ }
161
+ export interface LocalNodeDaemon {
162
+ /** The daemon's real, resolved base URL (reflects the actual bound port even when `port: 0` was requested). */
163
+ readonly url: string;
164
+ readonly server: Server;
165
+ /**
166
+ * Gracefully shuts the daemon down: closes the HTTP listener, disposes every composed feature
167
+ * (releasing e.g. an in-flight xAI OAuth loopback listener), removes the discovery record, runs
168
+ * the caller's `onShutdown` hook, then closes the durable sqlite handles. Idempotent and safe to
169
+ * call more than once, or concurrently.
170
+ */
171
+ stop(): Promise<void>;
172
+ /** Every feature actually composed, with why and what it grants. */
173
+ readonly activeFeatures: readonly string[];
174
+ }
175
+ /**
176
+ * Boots a complete, runnable `@jini-ai/core` daemon process.
177
+ *
178
+ * Preserves `createDaemon`'s compile-time "missing binding" error through this wrapper: the same
179
+ * `MissingTokenIds<Packs, BoundIds>` conditional gate `createDaemon` itself uses forces a call site
180
+ * with an unbound pack dependency to fail to typecheck with the missing token id(s) visible.
181
+ *
182
+ * **Why two overloads instead of one generic signature:** when a type parameter both has a default
183
+ * and is referenced inside a conditional type in the same parameter position where it also needs to
184
+ * be inferred from a nested callback's return type, TypeScript resolves it to the default *instead
185
+ * of* inferring from the callback, silently defeating the gate on exactly the call shape
186
+ * (`bindings` provided) where it matters most. Splitting into two overloads sidesteps the inference
187
+ * conflict entirely. Both directions are covered by `@ts-expect-error` proofs in
188
+ * `create-local-node-daemon.typecheck.ts`.
189
+ *
190
+ * @param config - See {@link CreateLocalNodeDaemonConfig}.
191
+ * @returns A promise resolving to `{url, server, stop, activeFeatures}` once the daemon is listening.
192
+ * @throws Rejects if the port is already in use (`EADDRINUSE`), the host can't be bound
193
+ * (`EACCES`/`EADDRNOTAVAIL`), the OS reports a listening socket with no resolvable port, or the
194
+ * composition itself is invalid. On any of these every sqlite handle already opened is closed
195
+ * before rejecting, so a failed boot never leaks an open file handle.
196
+ * @complexity O(1) beyond the packs' own `services()`/`http()` costs.
197
+ * @overallScore 100/100
198
+ */
199
+ export declare function createLocalNodeDaemon<const Packs extends readonly AnyPack[]>(config: CreateLocalNodeDaemonConfig<Packs, KernelBoundIds> & {
200
+ bindings?: undefined;
201
+ } & (MissingTokenIds<Packs, KernelBoundIds> extends never ? unknown : {
202
+ readonly __missingBindings: MissingTokenIds<Packs, KernelBoundIds>;
203
+ })): Promise<LocalNodeDaemon>;
204
+ export declare function createLocalNodeDaemon<const Packs extends readonly AnyPack[], BoundIds extends string>(config: CreateLocalNodeDaemonConfig<Packs, BoundIds> & {
205
+ bindings: (b: Bindings<KernelBoundIds>) => Bindings<BoundIds>;
206
+ } & (MissingTokenIds<Packs, BoundIds> extends never ? unknown : {
207
+ readonly __missingBindings: MissingTokenIds<Packs, BoundIds>;
208
+ })): Promise<LocalNodeDaemon>;
209
+ //# sourceMappingURL=create-local-node-daemon.d.ts.map