@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.
- 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 +636 -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 +281 -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 +49 -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 +124 -3
package/dist/feature.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime half of {@link CapabilityId}.
|
|
3
|
+
*
|
|
4
|
+
* Declared as a `Record<CapabilityId, true>` rather than a plain array so drift is a compile error
|
|
5
|
+
* in **both** directions: a union member missing here fails the `Record`'s exhaustiveness, and a
|
|
6
|
+
* key here that is not in the union fails the object literal's excess-property check. A validation
|
|
7
|
+
* list that can silently fall behind the type it validates is worse than none, because it reads as
|
|
8
|
+
* closed while quietly admitting whatever was added last.
|
|
9
|
+
*/
|
|
10
|
+
const CAPABILITY_ID_SET = Object.freeze({
|
|
11
|
+
'run:transport': true,
|
|
12
|
+
'run:inject': true,
|
|
13
|
+
'agent:discovery': true,
|
|
14
|
+
'tool:delegated': true,
|
|
15
|
+
'tool:catalog': true,
|
|
16
|
+
'host:read': true,
|
|
17
|
+
'host:exec': true,
|
|
18
|
+
'db:admin': true,
|
|
19
|
+
'net:egress': true,
|
|
20
|
+
'daemon:control': true,
|
|
21
|
+
'memory:store': true,
|
|
22
|
+
'routines:schedule': true,
|
|
23
|
+
'media:generate': true,
|
|
24
|
+
'ui:session': true,
|
|
25
|
+
});
|
|
26
|
+
/** Every capability this vocabulary declares, for error messages and for exhaustive iteration. */
|
|
27
|
+
export const CAPABILITY_IDS = Object.freeze(Object.keys(CAPABILITY_ID_SET));
|
|
28
|
+
/** Whether `value` names a capability this vocabulary actually declares. */
|
|
29
|
+
export function isCapabilityId(required) {
|
|
30
|
+
return Object.prototype.hasOwnProperty.call(CAPABILITY_ID_SET, required.value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The capabilities a feature may default to *on* under the `core-only` activation policy — the
|
|
34
|
+
* run transport, installed-agent discovery and the delegated-tool gateway. These are the frozen
|
|
35
|
+
* embedding contract; adding host execution or network egress would expand the security ceiling.
|
|
36
|
+
*
|
|
37
|
+
* A feature qualifies only if **every** capability it provides is in here, so a feature that mixes
|
|
38
|
+
* a core capability with a non-core one (say `run:transport` plus `host:exec`) does not sneak in on
|
|
39
|
+
* the strength of its safe half.
|
|
40
|
+
*/
|
|
41
|
+
export const CORE_CAPABILITIES = Object.freeze([
|
|
42
|
+
'run:transport',
|
|
43
|
+
'agent:discovery',
|
|
44
|
+
'tool:delegated',
|
|
45
|
+
]);
|
|
46
|
+
/** Identity helper that pins a feature's shape at the definition site. */
|
|
47
|
+
export function defineJiniFeature(feature) {
|
|
48
|
+
return feature;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The shipped profiles. **Versioned and immutable**: a future capability must never silently join
|
|
52
|
+
* an existing profile's grant set, because a consumer pinned to `local-daemon-v1` chose the
|
|
53
|
+
* authority surface it had on the day it pinned. A new set is a new id (`-v2`), not an edit.
|
|
54
|
+
*
|
|
55
|
+
* Both name a *capability grant*, never a hand-listed feature array — so the active feature list is
|
|
56
|
+
* always computed from capability semantics and cannot drift away from them when a feature is
|
|
57
|
+
* added, renamed, or reclassified.
|
|
58
|
+
*/
|
|
59
|
+
export const JINI_PROFILES = Object.freeze({
|
|
60
|
+
/**
|
|
61
|
+
* The conservative embedding default: run transport, agent discovery and delegated tools.
|
|
62
|
+
* Everything that adds host execution, network egress, database administration or daemon control
|
|
63
|
+
* is outside the grant, so it can be
|
|
64
|
+
* neither defaulted on nor explicitly enabled without the host first raising the ceiling.
|
|
65
|
+
*/
|
|
66
|
+
'agent-core-v1': Object.freeze({
|
|
67
|
+
id: 'agent-core-v1',
|
|
68
|
+
grants: Object.freeze(['run:transport', 'agent:discovery', 'tool:delegated']),
|
|
69
|
+
activation: 'core-only',
|
|
70
|
+
}),
|
|
71
|
+
/**
|
|
72
|
+
* `createLocalNodeDaemon`'s historical surface, frozen as a compatibility profile: exactly the
|
|
73
|
+
* capabilities the standalone daemon has always mounted. `run:inject`, `memory:store`,
|
|
74
|
+
* `routines:schedule`, `media:generate` and `ui:session` are **absent by design** — those route
|
|
75
|
+
* families were never wired by that preset, and granting them here would silently expand a
|
|
76
|
+
* shipped daemon's surface.
|
|
77
|
+
*/
|
|
78
|
+
'local-daemon-v1': Object.freeze({
|
|
79
|
+
id: 'local-daemon-v1',
|
|
80
|
+
grants: Object.freeze([
|
|
81
|
+
'run:transport',
|
|
82
|
+
'agent:discovery',
|
|
83
|
+
'tool:delegated',
|
|
84
|
+
'tool:catalog',
|
|
85
|
+
'host:read',
|
|
86
|
+
'host:exec',
|
|
87
|
+
'db:admin',
|
|
88
|
+
'net:egress',
|
|
89
|
+
'daemon:control',
|
|
90
|
+
]),
|
|
91
|
+
activation: 'all-permitted',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=feature.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module host-bootstrap
|
|
3
|
+
*
|
|
4
|
+
* Generic host-lifecycle primitives ported from an origin daemon's CLI-startup module — see
|
|
5
|
+
* `archived provenance ledger`. Only the two functions with no CLI-flag-parsing or env-var-reading inside their
|
|
6
|
+
* own bodies are ported here: `parseDaemonCliStartupArgs` (argv/env parsing for a `od <cmd>`-style
|
|
7
|
+
* CLI) and the higher-level `startDaemonRuntime`/`runDaemonCliStartup` wrappers belong to a future
|
|
8
|
+
* `@jini-ai/cli` task, not this one — this module is deliberately CLI-shape-agnostic.
|
|
9
|
+
*/
|
|
10
|
+
import type { Server } from 'node:http';
|
|
11
|
+
export declare const DEFAULT_DAEMON_BIND_HOST = "127.0.0.1";
|
|
12
|
+
/**
|
|
13
|
+
* Trims `input` to a usable bind-host string, falling back to the loopback default for
|
|
14
|
+
* blank/nullish input.
|
|
15
|
+
*
|
|
16
|
+
* @param input - A caller-supplied host value of unknown shape (typically `string | undefined`
|
|
17
|
+
* from an options object, hence the deliberately loose `unknown` parameter type).
|
|
18
|
+
* @returns The trimmed stringification of `input`, or {@link DEFAULT_DAEMON_BIND_HOST} when
|
|
19
|
+
* `input` is nullish or its stringification trims to empty. Nonstrings deliberately retain the
|
|
20
|
+
* historical coercion, so a supplied number is not silently replaced with the loopback host.
|
|
21
|
+
* @complexity O(1).
|
|
22
|
+
* @overallScore 100/100
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeDaemonBindHost(input: unknown): string;
|
|
25
|
+
export interface CloseHttpServerOptions {
|
|
26
|
+
/** Hard ceiling before force-closing every remaining socket. Defaults to 5000ms. */
|
|
27
|
+
closeTimeoutMs?: number;
|
|
28
|
+
/** Delay before closing idle (non-active) connections, capped by `closeTimeoutMs`. Defaults to 1000ms. */
|
|
29
|
+
idleCloseMs?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gracefully closes `server`: after a short `idleCloseMs` grace period it closes idle
|
|
33
|
+
* connections, then force-closes any connection still open once `closeTimeoutMs` elapses. Resolves
|
|
34
|
+
* immediately, without touching any socket, if `server` is not currently listening.
|
|
35
|
+
*
|
|
36
|
+
* @param server - The `node:http` server to close.
|
|
37
|
+
* @param options.closeTimeoutMs - See {@link CloseHttpServerOptions}.
|
|
38
|
+
* @param options.idleCloseMs - See {@link CloseHttpServerOptions}.
|
|
39
|
+
* @returns Resolves once `server.close()`'s callback fires with no error, or once the hard timeout
|
|
40
|
+
* force-closes connections, whichever happens first. The deadline deliberately bounds shutdown
|
|
41
|
+
* even if the callback never arrives; any later callback is ignored after settlement.
|
|
42
|
+
* @throws Rejects with whatever error `server.close()`'s callback reports (e.g. calling `close()`
|
|
43
|
+
* on a server that was never listening in the first place — guarded against above by the early
|
|
44
|
+
* return). Errors received before settlement propagate; callbacks after the hard deadline do not
|
|
45
|
+
* replace its already-settled result.
|
|
46
|
+
* @complexity O(1) scheduling; the actual wait is bounded by `closeTimeoutMs`.
|
|
47
|
+
* @overallScore 100/100
|
|
48
|
+
*/
|
|
49
|
+
export declare function closeHttpServer(server: Server, options?: CloseHttpServerOptions): Promise<void>;
|
|
50
|
+
export interface GracefulShutdownOptions {
|
|
51
|
+
/** Host signal/exit adapter. The default preserves Node's process-global signal handling. */
|
|
52
|
+
process?: ShutdownProcessPort;
|
|
53
|
+
/** Node timer ABI, injectable so a host controls shutdown scheduling. */
|
|
54
|
+
timers?: Pick<typeof globalThis, 'setTimeout' | 'clearTimeout'>;
|
|
55
|
+
/** Node console ABI; timeout and failure messages retain their historical arguments. */
|
|
56
|
+
logger?: Pick<Console, 'error'>;
|
|
57
|
+
/** OS signals to listen for. Defaults to `['SIGTERM', 'SIGINT']` — the two a container runtime,
|
|
58
|
+
* process manager, or an operator's `kill`/Ctrl-C ordinarily send to stop a long-lived daemon. */
|
|
59
|
+
signals?: readonly NodeJS.Signals[];
|
|
60
|
+
/** Hard ceiling, from the first signal, before forcing exit even if `stop()` never settles — a
|
|
61
|
+
* wedged shutdown must not block the process from ever exiting. Defaults to 10000ms (Docker's own
|
|
62
|
+
* default grace period before it escalates to `SIGKILL`). */
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
/** Called exactly once, with `0` if `stop()` resolved or `1` if it rejected or the timeout fired
|
|
65
|
+
* first. Defaults to the supplied process port's exit (Node process.exit by default). Override to observe or customize the final step (tests always
|
|
66
|
+
* do this — calling the real `process.exit` from a unit test would kill the test runner). */
|
|
67
|
+
onExit?: (code: number) => void;
|
|
68
|
+
}
|
|
69
|
+
/** Minimal Node signal ABI; embedders can supply their own process without seizing global listeners. */
|
|
70
|
+
export interface ShutdownProcessPort {
|
|
71
|
+
on(signal: NodeJS.Signals, listener: (signal: NodeJS.Signals) => void): unknown;
|
|
72
|
+
off(signal: NodeJS.Signals, listener: (signal: NodeJS.Signals) => void): unknown;
|
|
73
|
+
exit(code: number): unknown;
|
|
74
|
+
}
|
|
75
|
+
export interface GracefulShutdownHandle {
|
|
76
|
+
/** Removes the installed signal listeners. A later signal is then a plain no-op — Node's own
|
|
77
|
+
* default behavior applies again, exactly as if this had never been called. */
|
|
78
|
+
uninstall(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Wires `stop` to the process's OS shutdown signals so a container stop, process-manager restart, or
|
|
82
|
+
* operator `kill`/Ctrl-C runs the caller's graceful teardown instead of Node's default immediate
|
|
83
|
+
* termination. Deliberately opt-in rather than something `createLocalNodeDaemon` installs on every
|
|
84
|
+
* caller's behalf: a library that unilaterally seizes process-global signal handling can surprise an
|
|
85
|
+
* embedding host that manages its own process lifecycle (an Electron shell, for one, already owns
|
|
86
|
+
* its own fatal-exception handling and — per `desktop-host`'s own sidecar shutdown path — asks a
|
|
87
|
+
* spawned daemon child to stop over HTTP and falls back to `SIGKILL`, never `SIGTERM`, so it neither
|
|
88
|
+
* needs nor should double up on this). One line at the host's own entrypoint —
|
|
89
|
+
* `installGracefulShutdown({ stop: daemon.stop })` — is the whole integration.
|
|
90
|
+
*
|
|
91
|
+
* Idempotent and re-entrant: a second signal that arrives while a shutdown from the first is still
|
|
92
|
+
* in flight is ignored outright (not queued, not a second `stop()` call) — the in-flight shutdown
|
|
93
|
+
* (or, past `timeoutMs`, the forced exit) is left to finish on its own.
|
|
94
|
+
*
|
|
95
|
+
* @param stop - The caller's own graceful-shutdown function, e.g. a `LocalNodeDaemon.stop`.
|
|
96
|
+
* @param options - See {@link GracefulShutdownOptions}.
|
|
97
|
+
* @returns A handle whose `uninstall()` removes the installed listeners.
|
|
98
|
+
* @complexity O(1) to install; the signal handler itself is O(1) plus whatever `stop` costs.
|
|
99
|
+
*/
|
|
100
|
+
export declare function installGracefulShutdown({ stop }: {
|
|
101
|
+
stop: () => Promise<void>;
|
|
102
|
+
}, options?: GracefulShutdownOptions): GracefulShutdownHandle;
|
|
103
|
+
//# sourceMappingURL=host-bootstrap.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const DEFAULT_DAEMON_BIND_HOST = '127.0.0.1';
|
|
2
|
+
/**
|
|
3
|
+
* Trims `input` to a usable bind-host string, falling back to the loopback default for
|
|
4
|
+
* blank/nullish input.
|
|
5
|
+
*
|
|
6
|
+
* @param input - A caller-supplied host value of unknown shape (typically `string | undefined`
|
|
7
|
+
* from an options object, hence the deliberately loose `unknown` parameter type).
|
|
8
|
+
* @returns The trimmed stringification of `input`, or {@link DEFAULT_DAEMON_BIND_HOST} when
|
|
9
|
+
* `input` is nullish or its stringification trims to empty. Nonstrings deliberately retain the
|
|
10
|
+
* historical coercion, so a supplied number is not silently replaced with the loopback host.
|
|
11
|
+
* @complexity O(1).
|
|
12
|
+
* @overallScore 100/100
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeDaemonBindHost(input) {
|
|
15
|
+
const host = String(input ?? '').trim();
|
|
16
|
+
return host || DEFAULT_DAEMON_BIND_HOST;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gracefully closes `server`: after a short `idleCloseMs` grace period it closes idle
|
|
20
|
+
* connections, then force-closes any connection still open once `closeTimeoutMs` elapses. Resolves
|
|
21
|
+
* immediately, without touching any socket, if `server` is not currently listening.
|
|
22
|
+
*
|
|
23
|
+
* @param server - The `node:http` server to close.
|
|
24
|
+
* @param options.closeTimeoutMs - See {@link CloseHttpServerOptions}.
|
|
25
|
+
* @param options.idleCloseMs - See {@link CloseHttpServerOptions}.
|
|
26
|
+
* @returns Resolves once `server.close()`'s callback fires with no error, or once the hard timeout
|
|
27
|
+
* force-closes connections, whichever happens first. The deadline deliberately bounds shutdown
|
|
28
|
+
* even if the callback never arrives; any later callback is ignored after settlement.
|
|
29
|
+
* @throws Rejects with whatever error `server.close()`'s callback reports (e.g. calling `close()`
|
|
30
|
+
* on a server that was never listening in the first place — guarded against above by the early
|
|
31
|
+
* return). Errors received before settlement propagate; callbacks after the hard deadline do not
|
|
32
|
+
* replace its already-settled result.
|
|
33
|
+
* @complexity O(1) scheduling; the actual wait is bounded by `closeTimeoutMs`.
|
|
34
|
+
* @overallScore 100/100
|
|
35
|
+
*/
|
|
36
|
+
export async function closeHttpServer(server, options = {}) {
|
|
37
|
+
const { closeTimeoutMs = 5_000, idleCloseMs = 1_000 } = options;
|
|
38
|
+
if (!server.listening)
|
|
39
|
+
return;
|
|
40
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
41
|
+
let resolved = false;
|
|
42
|
+
const resolveOnce = () => {
|
|
43
|
+
if (resolved)
|
|
44
|
+
return;
|
|
45
|
+
resolved = true;
|
|
46
|
+
clearTimeout(idleTimer);
|
|
47
|
+
clearTimeout(hardTimer);
|
|
48
|
+
resolveClose();
|
|
49
|
+
};
|
|
50
|
+
const rejectOnce = (error) => {
|
|
51
|
+
if (resolved)
|
|
52
|
+
return;
|
|
53
|
+
resolved = true;
|
|
54
|
+
clearTimeout(idleTimer);
|
|
55
|
+
clearTimeout(hardTimer);
|
|
56
|
+
rejectClose(error);
|
|
57
|
+
};
|
|
58
|
+
const idleTimer = setTimeout(() => {
|
|
59
|
+
server.closeIdleConnections?.();
|
|
60
|
+
}, Math.min(idleCloseMs, closeTimeoutMs));
|
|
61
|
+
const hardTimer = setTimeout(() => {
|
|
62
|
+
server.closeAllConnections?.();
|
|
63
|
+
resolveOnce();
|
|
64
|
+
}, closeTimeoutMs);
|
|
65
|
+
idleTimer.unref?.();
|
|
66
|
+
hardTimer.unref?.();
|
|
67
|
+
server.close((error) => (error == null ? resolveOnce() : rejectOnce(error)));
|
|
68
|
+
}).finally(() => {
|
|
69
|
+
server.closeIdleConnections?.();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Wires `stop` to the process's OS shutdown signals so a container stop, process-manager restart, or
|
|
74
|
+
* operator `kill`/Ctrl-C runs the caller's graceful teardown instead of Node's default immediate
|
|
75
|
+
* termination. Deliberately opt-in rather than something `createLocalNodeDaemon` installs on every
|
|
76
|
+
* caller's behalf: a library that unilaterally seizes process-global signal handling can surprise an
|
|
77
|
+
* embedding host that manages its own process lifecycle (an Electron shell, for one, already owns
|
|
78
|
+
* its own fatal-exception handling and — per `desktop-host`'s own sidecar shutdown path — asks a
|
|
79
|
+
* spawned daemon child to stop over HTTP and falls back to `SIGKILL`, never `SIGTERM`, so it neither
|
|
80
|
+
* needs nor should double up on this). One line at the host's own entrypoint —
|
|
81
|
+
* `installGracefulShutdown({ stop: daemon.stop })` — is the whole integration.
|
|
82
|
+
*
|
|
83
|
+
* Idempotent and re-entrant: a second signal that arrives while a shutdown from the first is still
|
|
84
|
+
* in flight is ignored outright (not queued, not a second `stop()` call) — the in-flight shutdown
|
|
85
|
+
* (or, past `timeoutMs`, the forced exit) is left to finish on its own.
|
|
86
|
+
*
|
|
87
|
+
* @param stop - The caller's own graceful-shutdown function, e.g. a `LocalNodeDaemon.stop`.
|
|
88
|
+
* @param options - See {@link GracefulShutdownOptions}.
|
|
89
|
+
* @returns A handle whose `uninstall()` removes the installed listeners.
|
|
90
|
+
* @complexity O(1) to install; the signal handler itself is O(1) plus whatever `stop` costs.
|
|
91
|
+
*/
|
|
92
|
+
export function installGracefulShutdown({ stop }, options = {}) {
|
|
93
|
+
const signalProcess = options.process ?? process;
|
|
94
|
+
const timers = options.timers ?? globalThis;
|
|
95
|
+
const logger = options.logger ?? console;
|
|
96
|
+
const signals = options.signals ?? ['SIGTERM', 'SIGINT'];
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
98
|
+
const onExit = options.onExit ?? ((code) => { signalProcess.exit(code); });
|
|
99
|
+
let shuttingDown = false;
|
|
100
|
+
const handleSignal = (signal) => {
|
|
101
|
+
// A second signal while a shutdown is already in flight must not re-run `stop()` or restart the
|
|
102
|
+
// timeout — the first attempt (or its forced-exit fallback below) already owns getting the
|
|
103
|
+
// process to exit.
|
|
104
|
+
if (shuttingDown)
|
|
105
|
+
return;
|
|
106
|
+
shuttingDown = true;
|
|
107
|
+
let timedOut = false;
|
|
108
|
+
const timer = timers.setTimeout(() => {
|
|
109
|
+
timedOut = true;
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
logger.error(`[@jini-ai/server] graceful shutdown did not complete within ${timeoutMs}ms after ${signal}; forcing exit`);
|
|
112
|
+
onExit(1);
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
timer.unref?.();
|
|
115
|
+
void stop()
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
logger.error(`[@jini-ai/server] graceful shutdown failed after ${signal}`, error);
|
|
119
|
+
// Re-thrown so the `.finally` below still distinguishes success from failure via rejection
|
|
120
|
+
// state rather than a second flag.
|
|
121
|
+
throw error;
|
|
122
|
+
})
|
|
123
|
+
.then(() => {
|
|
124
|
+
if (timedOut)
|
|
125
|
+
return;
|
|
126
|
+
timers.clearTimeout(timer);
|
|
127
|
+
onExit(0);
|
|
128
|
+
}, () => {
|
|
129
|
+
if (timedOut)
|
|
130
|
+
return;
|
|
131
|
+
timers.clearTimeout(timer);
|
|
132
|
+
onExit(1);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
for (const signal of signals) {
|
|
136
|
+
signalProcess.on(signal, handleSignal);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
uninstall() {
|
|
140
|
+
for (const signal of signals) {
|
|
141
|
+
signalProcess.off(signal, handleSignal);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=host-bootstrap.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @jini-ai/server
|
|
3
|
+
*
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
export type { CreateLocalNodeDaemonConfig, KernelBoundIds, LocalNodeDaemon, LocalNodeHttpExtension, LocalNodeHttpExtensionContext, } from './create-local-node-daemon.js';
|
|
34
|
+
export { buildDaemonDbOperations, classifyRunFailureForRetry, createLocalNodeDaemon, projectDetectedAgent, resolveBoundPort, resolveReportHost, } from './create-local-node-daemon.js';
|
|
35
|
+
export type { ComposeJiniKernelConfig, JiniKernel, JiniKernelSecurity } from './compose-jini-kernel.js';
|
|
36
|
+
export { composeJiniKernel, defaultServerMessages } from './compose-jini-kernel.js';
|
|
37
|
+
export type { AnyPack, CapabilityId, FeatureBuildContext, FeatureComposition, FeaturePhase, JiniFeature, JiniProfile, JiniProfileId, ProfileActivation, } from './feature.js';
|
|
38
|
+
export { CAPABILITY_IDS, CORE_CAPABILITIES, defineJiniFeature, isCapabilityId, JINI_PROFILES } from './feature.js';
|
|
39
|
+
export type { ActivationReason, ActiveFeatureRecord, DeactivationReason, FeatureActivationInput, FeatureActivationPlan, InactiveFeatureRecord, } from './feature-activation.js';
|
|
40
|
+
export { resolveFeatureActivation } from './feature-activation.js';
|
|
41
|
+
export type { BuiltInFeatureOptions } from './builtin-features.js';
|
|
42
|
+
export { ANONYMOUS_DELEGATED_PRINCIPAL, createBuiltInFeatures, LOCAL_DAEMON_PRINCIPAL, } from './builtin-features.js';
|
|
43
|
+
export type { CreateJiniKernelBaseOptions, JiniKernelBase, JiniKernelStorage, KernelSqliteAccess, } from './kernel-base.js';
|
|
44
|
+
export { createJiniKernelBase } from './kernel-base.js';
|
|
45
|
+
export { createFrontendControl } from '@jini-ai/daemon/http';
|
|
46
|
+
export type { CreateFrontendControlOptions, FrontendBindErrorContext, FrontendControl, FrontendHttpExtension } from '@jini-ai/daemon/http';
|
|
47
|
+
export type { CloseHttpServerOptions, GracefulShutdownHandle, GracefulShutdownOptions } from './host-bootstrap.js';
|
|
48
|
+
export { DEFAULT_DAEMON_BIND_HOST, closeHttpServer, installGracefulShutdown, normalizeDaemonBindHost, } from './host-bootstrap.js';
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { buildDaemonDbOperations, classifyRunFailureForRetry, createLocalNodeDaemon, projectDetectedAgent, resolveBoundPort, resolveReportHost, } from './create-local-node-daemon.js';
|
|
2
|
+
export { composeJiniKernel, defaultServerMessages } from './compose-jini-kernel.js';
|
|
3
|
+
export { CAPABILITY_IDS, CORE_CAPABILITIES, defineJiniFeature, isCapabilityId, JINI_PROFILES } from './feature.js';
|
|
4
|
+
export { resolveFeatureActivation } from './feature-activation.js';
|
|
5
|
+
export { ANONYMOUS_DELEGATED_PRINCIPAL, createBuiltInFeatures, LOCAL_DAEMON_PRINCIPAL, } from './builtin-features.js';
|
|
6
|
+
export { createJiniKernelBase } from './kernel-base.js';
|
|
7
|
+
// Frontend control belongs to the daemon HTTP surface: hosts that build their own Express app
|
|
8
|
+
// can compose it without the server kernel. This server entry keeps its existing public export.
|
|
9
|
+
export { createFrontendControl } from '@jini-ai/daemon/http';
|
|
10
|
+
export { DEFAULT_DAEMON_BIND_HOST, closeHttpServer, installGracefulShutdown, normalizeDaemonBindHost, } from './host-bootstrap.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module kernel-base
|
|
3
|
+
*
|
|
4
|
+
* The infrastructure every composition owns regardless of which features it activates: the durable
|
|
5
|
+
* (or in-memory) event log, the run lifecycle, the byte journal, the agent executor, the one shared
|
|
6
|
+
* `ToolRegistry`/`ToolExecutor` pair, and — for sqlite storage — the raw database connection.
|
|
7
|
+
*
|
|
8
|
+
* **The database connection belongs here, not to a feature.** It is borrowed by the readiness probe
|
|
9
|
+
* (`health`), by the raw db-operations tools (`daemonDb`), and by the durable tool catalog
|
|
10
|
+
* (`toolCatalog`) — three independently-selectable features. Had any one of them owned it, disabling
|
|
11
|
+
* that feature would have silently broken the others: turning off `daemonDb` would leave `health`'s
|
|
12
|
+
* readiness probe with nothing to check, which is a much worse failure than the one it was meant to
|
|
13
|
+
* simplify, because a readiness probe that stops probing still answers `200`. Ownership by the base,
|
|
14
|
+
* borrowing by features, is what makes the feature set genuinely independent.
|
|
15
|
+
*/
|
|
16
|
+
import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
|
|
17
|
+
import { type ToolRegistry } from '@jini-ai/core';
|
|
18
|
+
import { createAgentExecutor, type AgentExecutor, type EventLog, type RunLifecycle, type RunRetrySideEffectState, type ToolExecutor } from '@jini-ai/daemon';
|
|
19
|
+
/** Where a composition's durable state lives. */
|
|
20
|
+
export type JiniKernelStorage =
|
|
21
|
+
/** No files touched. Runs and their events vanish with the process — right for tests and for an
|
|
22
|
+
* embedded host that treats runs as ephemeral. Features needing a real database file
|
|
23
|
+
* (`daemonDb`, `toolCatalog`, `xai`) cannot be activated against it. */
|
|
24
|
+
{
|
|
25
|
+
readonly kind: 'memory';
|
|
26
|
+
}
|
|
27
|
+
/** `<dataDir>/events.db` + `<dataDir>/journal.db`. The directory must already exist. */
|
|
28
|
+
| {
|
|
29
|
+
readonly kind: 'sqlite';
|
|
30
|
+
readonly dataDir: string;
|
|
31
|
+
readonly open: SqliteSyncOpener;
|
|
32
|
+
};
|
|
33
|
+
/** The raw sqlite handle and its path, borrowed by `health`/`daemonDb`/`toolCatalog`. */
|
|
34
|
+
export interface KernelSqliteAccess {
|
|
35
|
+
readonly connection: SqliteDb;
|
|
36
|
+
readonly eventsDbPath: string;
|
|
37
|
+
readonly dataDir: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* An `EventLog` that may or may not hold an OS resource. `@jini-ai/daemon/store/event-log/sqlite`'s `SqliteEventLog` adds
|
|
41
|
+
* `close({})`; `@jini-ai/daemon`'s in-memory reference adapter has nothing to release and so declares
|
|
42
|
+
* none. Modelling `close` as optional lets the base treat both storage kinds uniformly instead of
|
|
43
|
+
* branching on which one it happens to hold.
|
|
44
|
+
*/
|
|
45
|
+
export type ClosableEventLog = EventLog & {
|
|
46
|
+
close?: (requiredArgs: Record<string, never>) => Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
export interface JiniKernelBase {
|
|
49
|
+
readonly eventLog: ClosableEventLog;
|
|
50
|
+
readonly journalEventLog: ClosableEventLog;
|
|
51
|
+
readonly lifecycle: RunLifecycle;
|
|
52
|
+
readonly agentExecutor: AgentExecutor;
|
|
53
|
+
/** The single registry every active feature's tools land in, and the only one `toolExecutor` reads. */
|
|
54
|
+
readonly registry: ToolRegistry;
|
|
55
|
+
readonly toolExecutor: ToolExecutor;
|
|
56
|
+
/** `null` under memory storage. Owned here; features borrow and must never close it. */
|
|
57
|
+
readonly sqlite: KernelSqliteAccess | null;
|
|
58
|
+
/** Closes every handle this base opened, in the same order the standalone daemon always has. */
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export interface CreateJiniKernelBaseOptions {
|
|
62
|
+
readonly storage: JiniKernelStorage;
|
|
63
|
+
/** Extra `createAgentExecutor` options (e.g. `mcpJsonInjection`). `lifecycle`/`journal`/`classifyFailure` are kernel-owned. */
|
|
64
|
+
readonly agentExecutor?: Omit<NonNullable<Parameters<typeof createAgentExecutor>[1]>, 'journal' | 'classifyFailure'>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The zero-config retry classifier every composition gets. Delegates entirely to `@jini-ai/daemon`'s
|
|
68
|
+
* `resumableFromProcessExit` — see that function's own doc for the classification policy.
|
|
69
|
+
*/
|
|
70
|
+
export declare function classifyRunFailureForRetry(context: {
|
|
71
|
+
code: number | null;
|
|
72
|
+
signal: string | null;
|
|
73
|
+
sideEffects?: Pick<RunRetrySideEffectState, 'userVisibleOutputSeen' | 'toolCallSeen'>;
|
|
74
|
+
}): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Opens every kernel-owned resource and rehydrates durable run history.
|
|
77
|
+
*
|
|
78
|
+
* @throws If rehydration fails against a corrupt or unreadable durable history. Every handle this
|
|
79
|
+
* call had already opened is closed before it rethrows, so a failed boot never leaks a sqlite file
|
|
80
|
+
* handle — the invariant the standalone daemon has always maintained.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createJiniKernelBase(options: CreateJiniKernelBaseOptions): Promise<JiniKernelBase>;
|
|
83
|
+
//# sourceMappingURL=kernel-base.d.ts.map
|