@graphorin/server 0.6.1 → 0.7.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 +71 -0
- package/README.md +3 -3
- package/dist/app-audit-binding.d.ts +15 -0
- package/dist/app-audit-binding.d.ts.map +1 -0
- package/dist/app-audit-binding.js +136 -0
- package/dist/app-audit-binding.js.map +1 -0
- package/dist/app-daemons.d.ts +29 -0
- package/dist/app-daemons.d.ts.map +1 -0
- package/dist/app-daemons.js +27 -0
- package/dist/app-daemons.js.map +1 -0
- package/dist/app-lifecycle.js +272 -0
- package/dist/app-lifecycle.js.map +1 -0
- package/dist/app-metrics.js +79 -0
- package/dist/app-metrics.js.map +1 -0
- package/dist/app-middleware.js +92 -0
- package/dist/app-middleware.js.map +1 -0
- package/dist/app-routes.js +131 -0
- package/dist/app-routes.js.map +1 -0
- package/dist/app-ws.js +65 -0
- package/dist/app-ws.js.map +1 -0
- package/dist/app.d.ts +16 -16
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +35 -607
- package/dist/app.js.map +1 -1
- package/dist/commentary/built-in-patterns.d.ts +1 -1
- package/dist/commentary/built-in-patterns.js +1 -1
- package/dist/commentary/built-in-patterns.js.map +1 -1
- package/dist/config.d.ts +100 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -2
- package/dist/config.js.map +1 -1
- package/dist/health/checks.d.ts +15 -1
- package/dist/health/checks.d.ts.map +1 -1
- package/dist/health/checks.js +21 -0
- package/dist/health/checks.js.map +1 -1
- package/dist/health/routes.js +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -24
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +2 -2
- package/dist/internal/wire-error.js +20 -0
- package/dist/internal/wire-error.js.map +1 -0
- package/dist/lifecycle/pre-bind.d.ts +1 -1
- package/dist/metrics/catalog.d.ts.map +1 -1
- package/dist/metrics/catalog.js.map +1 -1
- package/dist/metrics/tools-bridge.d.ts +15 -0
- package/dist/metrics/tools-bridge.d.ts.map +1 -0
- package/dist/metrics/tools-bridge.js +103 -0
- package/dist/metrics/tools-bridge.js.map +1 -0
- package/dist/middleware/audit.d.ts +1 -1
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/csrf.js +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/scope.d.ts.map +1 -1
- package/dist/middleware/scope.js +44 -4
- package/dist/middleware/scope.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/index.d.ts +33 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/replay/routes.d.ts +1 -1
- package/dist/replay/routes.js +2 -2
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +64 -18
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.js +5 -5
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/tokens.d.ts +1 -1
- package/dist/routes/tokens.d.ts.map +1 -1
- package/dist/routes/tokens.js +21 -1
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +165 -19
- package/dist/routes/workflows.js.map +1 -1
- package/dist/runtime/retention.d.ts +105 -0
- package/dist/runtime/retention.d.ts.map +1 -0
- package/dist/runtime/retention.js +154 -0
- package/dist/runtime/retention.js.map +1 -0
- package/dist/runtime/run-state.d.ts +2 -0
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +34 -2
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/sse/events.js +3 -4
- package/dist/sse/events.js.map +1 -1
- package/dist/tools-audit-bridge.d.ts +29 -0
- package/dist/tools-audit-bridge.d.ts.map +1 -0
- package/dist/tools-audit-bridge.js +103 -0
- package/dist/tools-audit-bridge.js.map +1 -0
- package/dist/workflows/timer-daemon.d.ts +69 -0
- package/dist/workflows/timer-daemon.d.ts.map +1 -0
- package/dist/workflows/timer-daemon.js +37 -0
- package/dist/workflows/timer-daemon.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +1 -1
- package/dist/ws/dispatcher.js +19 -12
- package/dist/ws/dispatcher.js.map +1 -1
- package/dist/ws/index.d.ts +2 -2
- package/dist/ws/index.js +3 -3
- package/dist/ws/replay-buffer.d.ts +35 -1
- package/dist/ws/replay-buffer.d.ts.map +1 -1
- package/dist/ws/replay-buffer.js +35 -3
- package/dist/ws/replay-buffer.js.map +1 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/dist/ws/upgrade.js +24 -18
- package/dist/ws/upgrade.js.map +1 -1
- package/package.json +30 -29
- package/src/app-audit-binding.ts +227 -0
- package/src/app-daemons.ts +73 -0
- package/src/app-lifecycle.ts +476 -0
- package/src/app-metrics.ts +144 -0
- package/src/app-middleware.ts +149 -0
- package/src/app-routes.ts +305 -0
- package/src/app-ws.ts +111 -0
- package/src/app.ts +317 -0
- package/src/commentary/audit-bridge.ts +135 -0
- package/src/commentary/built-in-patterns.ts +79 -0
- package/src/commentary/index.ts +32 -0
- package/src/commentary/sanitizer.ts +238 -0
- package/src/commentary/types.ts +130 -0
- package/src/config.ts +472 -0
- package/src/consolidator/daemon.ts +141 -0
- package/src/consolidator/index.ts +14 -0
- package/src/errors/index.ts +247 -0
- package/src/health/checks.ts +322 -0
- package/src/health/index.ts +34 -0
- package/src/health/routes.ts +154 -0
- package/src/index.ts +243 -0
- package/src/internal/context.ts +77 -0
- package/src/internal/ids.ts +17 -0
- package/src/internal/json.ts +47 -0
- package/src/internal/wire-error.ts +44 -0
- package/src/lifecycle/hooks.ts +66 -0
- package/src/lifecycle/index.ts +16 -0
- package/src/lifecycle/pre-bind.ts +138 -0
- package/src/metrics/catalog.ts +112 -0
- package/src/metrics/index.ts +12 -0
- package/src/metrics/registry.ts +337 -0
- package/src/metrics/tools-bridge.ts +124 -0
- package/src/middleware/audit.ts +114 -0
- package/src/middleware/auth.ts +195 -0
- package/src/middleware/cors.ts +72 -0
- package/src/middleware/csrf.ts +98 -0
- package/src/middleware/idempotency.ts +336 -0
- package/src/middleware/index.ts +38 -0
- package/src/middleware/rate-limit.ts +71 -0
- package/src/middleware/request-state.ts +79 -0
- package/src/middleware/scope.ts +161 -0
- package/src/registry/index.ts +278 -0
- package/src/replay/index.ts +14 -0
- package/src/replay/routes.ts +255 -0
- package/src/routes/agents.ts +363 -0
- package/src/routes/audit.ts +207 -0
- package/src/routes/auth.ts +80 -0
- package/src/routes/health.ts +42 -0
- package/src/routes/index.ts +16 -0
- package/src/routes/mcp.ts +97 -0
- package/src/routes/memory.ts +152 -0
- package/src/routes/sessions.ts +250 -0
- package/src/routes/skills.ts +91 -0
- package/src/routes/tokens.ts +166 -0
- package/src/routes/workflows.ts +616 -0
- package/src/runtime/retention.ts +284 -0
- package/src/runtime/run-state.ts +422 -0
- package/src/sse/events.ts +303 -0
- package/src/sse/index.ts +7 -0
- package/src/tools-audit-bridge.ts +120 -0
- package/src/triggers/daemon.ts +198 -0
- package/src/triggers/index.ts +16 -0
- package/src/triggers/routes.ts +139 -0
- package/src/workflows/timer-daemon.ts +102 -0
- package/src/ws/dispatcher.ts +537 -0
- package/src/ws/index.ts +45 -0
- package/src/ws/replay-buffer.ts +209 -0
- package/src/ws/subjects.ts +162 -0
- package/src/ws/ticket.ts +174 -0
- package/src/ws/upgrade.ts +507 -0
- package/dist/lifecycle/index.js +0 -3
- package/dist/routes/index.js +0 -12
package/src/app.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createServer({...})` - the single programmatic entry point.
|
|
3
|
+
*
|
|
4
|
+
* The factory returns a {@link GraphorinServer} handle the operator
|
|
5
|
+
* controls via `start()` / `stop()`. The same handle is consumed by
|
|
6
|
+
* the `graphorin start` CLI binary in `@graphorin/cli`.
|
|
7
|
+
*
|
|
8
|
+
* Phase 14a covers the full HTTP surface, the lifecycle plumbing,
|
|
9
|
+
* the Hono app composition, and the auth / scope / idempotency /
|
|
10
|
+
* audit middleware stack. Phase 14b/c attach the WebSocket layer +
|
|
11
|
+
* triggers daemon + observability endpoints to the same handle.
|
|
12
|
+
*
|
|
13
|
+
* This file is the composition root: the cohesive builders live in
|
|
14
|
+
* focused sibling modules - `app-audit-binding.ts` (default audit-db
|
|
15
|
+
* binding), `app-middleware.ts` (global + authenticated-subtree
|
|
16
|
+
* middleware), `app-ws.ts` (WebSocket layer), `app-daemons.ts`
|
|
17
|
+
* (triggers / consolidator daemon construction), `app-routes.ts`
|
|
18
|
+
* (route mounting), `app-metrics.ts` (live scrape refresh), and
|
|
19
|
+
* `app-lifecycle.ts` (`start()` / `stop()` sequencing).
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import process from 'node:process';
|
|
25
|
+
import { createSqliteStore, type GraphorinSqliteStore } from '@graphorin/store-sqlite';
|
|
26
|
+
import { Hono } from 'hono';
|
|
27
|
+
import { buildDaemons, type TriggersDaemonInput, type WorkflowTimersInput } from './app-daemons.js';
|
|
28
|
+
import { createLifecycle } from './app-lifecycle.js';
|
|
29
|
+
import { attachGlobalMiddleware } from './app-middleware.js';
|
|
30
|
+
import { buildWsLayer } from './app-ws.js';
|
|
31
|
+
import { parseServerConfig, type ServerConfigInput, type ServerConfigSpec } from './config.js';
|
|
32
|
+
import type { ConsolidatorDaemon, ConsolidatorLike } from './consolidator/daemon.js';
|
|
33
|
+
import { ConfigInvalidError } from './errors/index.js';
|
|
34
|
+
import type { HealthCheckOptions } from './health/index.js';
|
|
35
|
+
import type { ServerVariables } from './internal/context.js';
|
|
36
|
+
import type { LifecycleHooks } from './lifecycle/index.js';
|
|
37
|
+
import { createServerMetricRegistry, SERVER_METRIC_NAMES } from './metrics/catalog.js';
|
|
38
|
+
import type { MetricRegistry } from './metrics/registry.js';
|
|
39
|
+
import { AgentRegistry, WorkflowRegistry } from './registry/index.js';
|
|
40
|
+
import type { ReplayApi } from './replay/index.js';
|
|
41
|
+
import type { AuditApi, McpApi, MemoryApi, SessionApi, SkillsApi } from './routes/index.js';
|
|
42
|
+
import { RunStateTracker } from './runtime/run-state.js';
|
|
43
|
+
import type { TriggersDaemon } from './triggers/daemon.js';
|
|
44
|
+
import type { WorkflowTimerDaemon } from './workflows/timer-daemon.js';
|
|
45
|
+
import type { WsDispatcher, WsTicketStore } from './ws/index.js';
|
|
46
|
+
|
|
47
|
+
// Stable re-exports - both were born in this module and stay
|
|
48
|
+
// importable from `./app.js` (and the package barrel) unchanged.
|
|
49
|
+
export { ensureStoreAuditBinding } from './app-audit-binding.js';
|
|
50
|
+
export type { TriggersDaemonInput, WorkflowTimersInput } from './app-daemons.js';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Public surface returned by {@link createServer}.
|
|
54
|
+
*
|
|
55
|
+
* @stable
|
|
56
|
+
*/
|
|
57
|
+
export interface GraphorinServer {
|
|
58
|
+
readonly version: string;
|
|
59
|
+
readonly config: ServerConfigSpec;
|
|
60
|
+
readonly app: Hono<{ Variables: ServerVariables }>;
|
|
61
|
+
readonly agents: AgentRegistry;
|
|
62
|
+
readonly workflows: WorkflowRegistry;
|
|
63
|
+
readonly runs: RunStateTracker;
|
|
64
|
+
readonly listeningOn: { readonly host: string; readonly port: number } | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Optional WebSocket dispatcher exposed when `server.ws.enabled =
|
|
67
|
+
* true`. Route handlers + the agent / workflow runtimes call
|
|
68
|
+
* `dispatcher.emit(subject, event)` to fan out events to subscribed
|
|
69
|
+
* clients. Returns `undefined` when WS is disabled.
|
|
70
|
+
*/
|
|
71
|
+
readonly wsDispatcher: WsDispatcher | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Optional WebSocket ticket store exposed when `server.ws.enabled
|
|
74
|
+
* = true`. Surfaced primarily for tests; the
|
|
75
|
+
* `POST /v1/session/ws-ticket` route uses it transparently.
|
|
76
|
+
*/
|
|
77
|
+
readonly wsTickets: WsTicketStore | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Optional triggers daemon - populated when the operator wired a
|
|
80
|
+
* scheduler (or an in-process trigger surface) at construction
|
|
81
|
+
* time. Phase 14c integration.
|
|
82
|
+
*/
|
|
83
|
+
readonly triggers: TriggersDaemon | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Optional consolidator daemon - populated when the operator
|
|
86
|
+
* supplied a `Consolidator` instance via `createServer({
|
|
87
|
+
* consolidator })`. Phase 14c integration.
|
|
88
|
+
*/
|
|
89
|
+
readonly consolidator: ConsolidatorDaemon | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* W-032: optional workflow durable-timer daemon - populated when the
|
|
92
|
+
* operator wired a `createTimerDriver(...)` at construction time.
|
|
93
|
+
*/
|
|
94
|
+
readonly workflowTimers: WorkflowTimerDaemon | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Phase 14c Prometheus registry. Always present; sample updates
|
|
97
|
+
* are observable via `metrics.snapshot()`.
|
|
98
|
+
*/
|
|
99
|
+
readonly metrics: MetricRegistry;
|
|
100
|
+
start(): Promise<{ readonly host: string; readonly port: number }>;
|
|
101
|
+
stop(options?: { readonly force?: boolean }): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @stable
|
|
106
|
+
*/
|
|
107
|
+
export interface CreateServerOptions {
|
|
108
|
+
/** Loaded `graphorin.config.ts` payload - see `defineConfig({...})`. */
|
|
109
|
+
readonly config?: ServerConfigInput;
|
|
110
|
+
/**
|
|
111
|
+
* Optional pre-validated config. When supplied, `config` is ignored
|
|
112
|
+
* and the schema validation step is skipped. Useful for tests + the
|
|
113
|
+
* `graphorin migrate` CLI command which bypasses the listener.
|
|
114
|
+
*/
|
|
115
|
+
readonly validatedConfig?: ServerConfigSpec;
|
|
116
|
+
/** Pre-built SQLite store. Tests inject an in-memory store. */
|
|
117
|
+
readonly store?: GraphorinSqliteStore;
|
|
118
|
+
/** Optional pre-built tracker. Tests inject deterministic timing. */
|
|
119
|
+
readonly runs?: RunStateTracker;
|
|
120
|
+
/** Optional pre-built registries. */
|
|
121
|
+
readonly agents?: AgentRegistry;
|
|
122
|
+
readonly workflows?: WorkflowRegistry;
|
|
123
|
+
/** Optional in-process domain adapters wired into REST routes. */
|
|
124
|
+
readonly sessions?: SessionApi;
|
|
125
|
+
readonly memory?: MemoryApi;
|
|
126
|
+
readonly skills?: SkillsApi;
|
|
127
|
+
readonly mcp?: McpApi;
|
|
128
|
+
readonly audit?: AuditApi;
|
|
129
|
+
/**
|
|
130
|
+
* Optional consolidator surface (`@graphorin/memory`). Phase 14c
|
|
131
|
+
* starts/stops the runtime alongside the server lifecycle and
|
|
132
|
+
* surfaces its status through `/v1/health`.
|
|
133
|
+
*/
|
|
134
|
+
readonly consolidator?: ConsolidatorLike;
|
|
135
|
+
/**
|
|
136
|
+
* Optional triggers daemon - pass an existing one (e.g. built
|
|
137
|
+
* from `createScheduler`) or a triggers configuration the server
|
|
138
|
+
* should wrap with the daemon adapter.
|
|
139
|
+
*/
|
|
140
|
+
readonly triggers?: TriggersDaemonInput;
|
|
141
|
+
/**
|
|
142
|
+
* W-032: optional workflow durable-timer surface - pass a
|
|
143
|
+
* `createTimerDriver(...)` built over your workflows + checkpoint
|
|
144
|
+
* stores (`{ driver }`), or a pre-built daemon. The server starts
|
|
145
|
+
* and stops it with the lifecycle and reports it on `/v1/health`.
|
|
146
|
+
*/
|
|
147
|
+
readonly workflowTimers?: WorkflowTimersInput;
|
|
148
|
+
/**
|
|
149
|
+
* Optional replay API consumed by the scope-enforced replay
|
|
150
|
+
* endpoints. Phase 14c.
|
|
151
|
+
*/
|
|
152
|
+
readonly replay?: ReplayApi;
|
|
153
|
+
/**
|
|
154
|
+
* Optional probes that augment `/v1/health`. Provided by consumer
|
|
155
|
+
* code (e.g. `embedder` provides `embedderLoaded`).
|
|
156
|
+
*/
|
|
157
|
+
readonly healthProbes?: () => HealthCheckOptions | Promise<HealthCheckOptions>;
|
|
158
|
+
/**
|
|
159
|
+
* Optional Prometheus metric registry override. When omitted, the
|
|
160
|
+
* server constructs the canonical registry from
|
|
161
|
+
* {@link createServerMetricRegistry}.
|
|
162
|
+
*/
|
|
163
|
+
readonly metricRegistry?: MetricRegistry;
|
|
164
|
+
/** Lifecycle hook overrides. */
|
|
165
|
+
readonly hooks?: LifecycleHooks;
|
|
166
|
+
/** Wall-clock provider for tests. */
|
|
167
|
+
readonly now?: () => number;
|
|
168
|
+
/** Override the cipher peer probe. Tests inject a stub. */
|
|
169
|
+
readonly probeCipherPeer?: () => Promise<void>;
|
|
170
|
+
/** Override the package version reported on `/v1/health`. */
|
|
171
|
+
readonly version?: string;
|
|
172
|
+
/** Skip `applyProcessHardening` (tests). */
|
|
173
|
+
readonly skipHardening?: boolean;
|
|
174
|
+
/** Skip starting the actual listener (tests). */
|
|
175
|
+
readonly skipListen?: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @stable
|
|
180
|
+
*/
|
|
181
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
182
|
+
|
|
183
|
+
export const VERSION: string = pkg.version;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build a fully-wired Graphorin server. The returned handle is
|
|
187
|
+
* inert until `start()` is awaited.
|
|
188
|
+
*
|
|
189
|
+
* @stable
|
|
190
|
+
*/
|
|
191
|
+
export async function createServer(options: CreateServerOptions = {}): Promise<GraphorinServer> {
|
|
192
|
+
const config: ServerConfigSpec =
|
|
193
|
+
options.validatedConfig ?? parseServerConfig(options.config ?? {});
|
|
194
|
+
const now = options.now ?? Date.now;
|
|
195
|
+
const startedAt = now();
|
|
196
|
+
const version = options.version ?? VERSION;
|
|
197
|
+
|
|
198
|
+
// IP-1: when encryption is configured, resolve the passphrase ref
|
|
199
|
+
// BEFORE constructing the store and thread the encryption config
|
|
200
|
+
// through - `graphorin init --encrypted` produced a config nothing
|
|
201
|
+
// honoured, and a database encrypted via `storage encrypt` could not
|
|
202
|
+
// be opened by the server at all.
|
|
203
|
+
let storeEncryption:
|
|
204
|
+
| { enabled: true; cipher?: never; passphraseResolver: () => Promise<string> }
|
|
205
|
+
| undefined;
|
|
206
|
+
if (options.store === undefined && config.storage.encryption.enabled) {
|
|
207
|
+
if (config.storage.encryption.passphraseRef === undefined) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'[graphorin/server] storage.encryption.enabled is true but no passphraseRef is configured.',
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const { resolveSecret } = await import('@graphorin/security/secrets');
|
|
213
|
+
const passphrase = await resolveSecret(config.storage.encryption.passphraseRef);
|
|
214
|
+
storeEncryption = {
|
|
215
|
+
enabled: true,
|
|
216
|
+
passphraseResolver: async () => passphrase.use((v) => v),
|
|
217
|
+
} as never;
|
|
218
|
+
}
|
|
219
|
+
const store =
|
|
220
|
+
options.store ??
|
|
221
|
+
(await createSqliteStore({
|
|
222
|
+
path: config.storage.path,
|
|
223
|
+
mode: config.storage.mode,
|
|
224
|
+
...(storeEncryption !== undefined
|
|
225
|
+
? {
|
|
226
|
+
encryption: {
|
|
227
|
+
enabled: true,
|
|
228
|
+
...(config.storage.encryption.cipher !== undefined
|
|
229
|
+
? { cipher: config.storage.encryption.cipher as never }
|
|
230
|
+
: {}),
|
|
231
|
+
passphraseResolver: storeEncryption.passphraseResolver,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
const metricRegistry = options.metricRegistry ?? createServerMetricRegistry();
|
|
238
|
+
metricRegistry.set(SERVER_METRIC_NAMES.buildInfo, 1, { version });
|
|
239
|
+
|
|
240
|
+
// IP-15: agent / workflow run completions move graphorin_agent_runs_total +
|
|
241
|
+
// graphorin_agent_run_duration_seconds. The tracker stays metric-agnostic -
|
|
242
|
+
// it fires a terminal callback the server turns into samples.
|
|
243
|
+
const runs =
|
|
244
|
+
options.runs ??
|
|
245
|
+
new RunStateTracker({
|
|
246
|
+
now,
|
|
247
|
+
onTerminal: (info) => {
|
|
248
|
+
metricRegistry.inc(SERVER_METRIC_NAMES.agentRunsTotal, { status: info.status });
|
|
249
|
+
if (info.durationMs !== undefined) {
|
|
250
|
+
metricRegistry.observe(SERVER_METRIC_NAMES.agentRunDuration, info.durationMs / 1000);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const agents = options.agents ?? new AgentRegistry();
|
|
255
|
+
const workflows = options.workflows ?? new WorkflowRegistry();
|
|
256
|
+
|
|
257
|
+
const app = new Hono<{ Variables: ServerVariables }>();
|
|
258
|
+
|
|
259
|
+
// Request-state / CORS / CSRF / rate-limit - see `app-middleware.ts`.
|
|
260
|
+
attachGlobalMiddleware(app, config, now);
|
|
261
|
+
|
|
262
|
+
const { triggersDaemon, consolidatorDaemon, workflowTimerDaemon } = buildDaemons(options);
|
|
263
|
+
|
|
264
|
+
const ws = buildWsLayer(app, config, now);
|
|
265
|
+
|
|
266
|
+
const lifecycle = createLifecycle({
|
|
267
|
+
options,
|
|
268
|
+
config,
|
|
269
|
+
store,
|
|
270
|
+
app,
|
|
271
|
+
metricRegistry,
|
|
272
|
+
runs,
|
|
273
|
+
agents,
|
|
274
|
+
workflows,
|
|
275
|
+
version,
|
|
276
|
+
startedAt,
|
|
277
|
+
now,
|
|
278
|
+
ws,
|
|
279
|
+
triggersDaemon,
|
|
280
|
+
consolidatorDaemon,
|
|
281
|
+
workflowTimerDaemon,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const handle: GraphorinServer = {
|
|
285
|
+
version,
|
|
286
|
+
config,
|
|
287
|
+
app,
|
|
288
|
+
agents,
|
|
289
|
+
workflows,
|
|
290
|
+
runs,
|
|
291
|
+
metrics: metricRegistry,
|
|
292
|
+
get wsDispatcher() {
|
|
293
|
+
return lifecycle.wsDispatcher;
|
|
294
|
+
},
|
|
295
|
+
get wsTickets() {
|
|
296
|
+
return lifecycle.wsTickets;
|
|
297
|
+
},
|
|
298
|
+
get triggers() {
|
|
299
|
+
return triggersDaemon;
|
|
300
|
+
},
|
|
301
|
+
get consolidator() {
|
|
302
|
+
return consolidatorDaemon;
|
|
303
|
+
},
|
|
304
|
+
get workflowTimers() {
|
|
305
|
+
return workflowTimerDaemon;
|
|
306
|
+
},
|
|
307
|
+
get listeningOn() {
|
|
308
|
+
return lifecycle.listeningOn;
|
|
309
|
+
},
|
|
310
|
+
start: lifecycle.start,
|
|
311
|
+
stop: lifecycle.stop,
|
|
312
|
+
};
|
|
313
|
+
return handle;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
void process;
|
|
317
|
+
void ConfigInvalidError;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP-21: bridge the delivery-commentary sanitizer's audit decisions into the
|
|
3
|
+
* tamper-evident audit log. `createWsDispatcher` ran the sanitizer without a
|
|
4
|
+
* `sink`, so every documented sanitization decision - what was wrapped or
|
|
5
|
+
* stripped on the wire, with before/after digests and the matched-pattern
|
|
6
|
+
* bucket - was silently dropped instead of landing in the audit chain.
|
|
7
|
+
*
|
|
8
|
+
* The WS dispatcher is constructed before the audit DB is unsealed (the DB
|
|
9
|
+
* opens during `start()`), so the server hands the dispatcher a
|
|
10
|
+
* {@link LateBoundCommentarySink} and installs the real audit-writing target
|
|
11
|
+
* via {@link bridgeCommentaryToAudit} once the DB exists. Mirrors the
|
|
12
|
+
* `bridgeAuthToAudit` / `bridgeSecretsToAudit` pattern in `@graphorin/security`.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type AuditDb, type AuditEntryInput, appendAudit } from '@graphorin/security/audit';
|
|
18
|
+
|
|
19
|
+
import type { DeliveryCommentaryDecision, DeliveryCommentarySink } from './types.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Audit action recorded for a delivery-commentary sanitization decision.
|
|
23
|
+
*
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
export const COMMENTARY_AUDIT_ACTION = 'delivery:commentary:sanitized';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Translate a sanitizer decision into an audit entry. The digests + matched
|
|
30
|
+
* pattern bucket land in `metadata`; raw payloads never do (the sanitizer only
|
|
31
|
+
* ever exposes SHA-256s of the before/after bodies).
|
|
32
|
+
*/
|
|
33
|
+
export function commentaryDecisionToAuditInput(
|
|
34
|
+
decision: DeliveryCommentaryDecision,
|
|
35
|
+
): AuditEntryInput {
|
|
36
|
+
return {
|
|
37
|
+
actor: { kind: 'system', id: 'graphorin/server' },
|
|
38
|
+
action: COMMENTARY_AUDIT_ACTION,
|
|
39
|
+
target: `${decision.transport}:${decision.eventType}`,
|
|
40
|
+
decision: 'success',
|
|
41
|
+
metadata: {
|
|
42
|
+
policy: decision.policy,
|
|
43
|
+
applied: decision.applied,
|
|
44
|
+
boundary: decision.boundary,
|
|
45
|
+
reasons: decision.reasons,
|
|
46
|
+
...(decision.matchedPattern !== undefined ? { matchedPattern: decision.matchedPattern } : {}),
|
|
47
|
+
sha256OfBefore: decision.sha256OfBefore,
|
|
48
|
+
sha256OfAfter: decision.sha256OfAfter,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A commentary sink that also exposes a `drain()` so callers (and tests) can
|
|
55
|
+
* await any in-flight audit writes.
|
|
56
|
+
*
|
|
57
|
+
* @stable
|
|
58
|
+
*/
|
|
59
|
+
export interface CommentaryAuditSink extends DeliveryCommentarySink {
|
|
60
|
+
/** Resolve once every queued audit write has settled. */
|
|
61
|
+
readonly drain: () => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a commentary sink that appends each sanitization decision to the audit
|
|
66
|
+
* log. Writes serialise through `appendAudit` so concurrent decisions never
|
|
67
|
+
* race on `seq`; a failed write is isolated from the wire - `onWriteError`
|
|
68
|
+
* (default: a console warning) runs instead of throwing.
|
|
69
|
+
*
|
|
70
|
+
* @stable
|
|
71
|
+
*/
|
|
72
|
+
export function bridgeCommentaryToAudit(
|
|
73
|
+
db: AuditDb,
|
|
74
|
+
onWriteError: (
|
|
75
|
+
decision: DeliveryCommentaryDecision,
|
|
76
|
+
error: unknown,
|
|
77
|
+
) => void = defaultOnWriteError,
|
|
78
|
+
): CommentaryAuditSink {
|
|
79
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
80
|
+
return {
|
|
81
|
+
onDecision(decision) {
|
|
82
|
+
const input = commentaryDecisionToAuditInput(decision);
|
|
83
|
+
tail = tail
|
|
84
|
+
.then(() => appendAudit(db, input))
|
|
85
|
+
.catch((error) => onWriteError(decision, error));
|
|
86
|
+
},
|
|
87
|
+
drain: async () => {
|
|
88
|
+
await tail;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function defaultOnWriteError(_decision: DeliveryCommentaryDecision, error: unknown): void {
|
|
94
|
+
console.warn(
|
|
95
|
+
`[graphorin/server] WARN: failed to write a delivery-commentary audit entry: ${
|
|
96
|
+
error instanceof Error ? error.message : String(error)
|
|
97
|
+
}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A {@link DeliveryCommentarySink} whose real target is installed later. The WS
|
|
103
|
+
* dispatcher is created before the audit DB opens (IP-21); the server hands it
|
|
104
|
+
* this forwarding sink and calls {@link LateBoundCommentarySink.bind} once the
|
|
105
|
+
* audit-writing sink exists. Decisions emitted before binding are dropped - the
|
|
106
|
+
* dispatcher only sanitizes once it is live (after `start()`, by which point
|
|
107
|
+
* the audit DB, if configured, has opened and bound).
|
|
108
|
+
*
|
|
109
|
+
* @stable
|
|
110
|
+
*/
|
|
111
|
+
export interface LateBoundCommentarySink extends DeliveryCommentarySink {
|
|
112
|
+
/** Install the real sink. Replaces any previously-bound target. */
|
|
113
|
+
bind(target: DeliveryCommentarySink): void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @stable
|
|
118
|
+
*/
|
|
119
|
+
export function createLateBoundCommentarySink(): LateBoundCommentarySink {
|
|
120
|
+
let target: DeliveryCommentarySink | undefined;
|
|
121
|
+
return {
|
|
122
|
+
bind(next) {
|
|
123
|
+
target = next;
|
|
124
|
+
},
|
|
125
|
+
onDecision(decision) {
|
|
126
|
+
// Contract: a sink never throws into the wire. The sanitizer also guards,
|
|
127
|
+
// but be defensive in case a bound target misbehaves.
|
|
128
|
+
try {
|
|
129
|
+
target?.onDecision(decision);
|
|
130
|
+
} catch {
|
|
131
|
+
// swallow
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default pattern catalogue for the delivery-layer commentary-phase
|
|
3
|
+
* trace sanitization applied by the WebSocket dispatcher (`@graphorin/server/ws`)
|
|
4
|
+
* and the SSE event-emission boundary (`@graphorin/server/sse`).
|
|
5
|
+
*
|
|
6
|
+
* The catalogue is structurally identical to the session-output
|
|
7
|
+
* sanitization catalogue exposed by `@graphorin/sessions/commentary`;
|
|
8
|
+
* they live in two packages so the server's delivery layer does not
|
|
9
|
+
* pull a hard dependency on the session module while still providing
|
|
10
|
+
* defense-in-depth across the storage-write boundary (Phase 11) and
|
|
11
|
+
* the wire-emission boundary (this module). Deployments that want a
|
|
12
|
+
* single source of truth across both layers can pass the sessions
|
|
13
|
+
* catalogue through {@link import('./types.js').DeliveryCommentaryConfig}.patterns.
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { DeliveryCommentaryPattern } from './types.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The framework-shipped catalogue. Snapshot bytes-equal across the
|
|
22
|
+
* `ws` / `sse` / `rest` transports; idempotent on a single payload
|
|
23
|
+
* (the wrap envelope itself is not matched by any pattern, so a
|
|
24
|
+
* second pass over a previously-sanitized payload is a no-op).
|
|
25
|
+
*
|
|
26
|
+
* @stable
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_DELIVERY_COMMENTARY_PATTERNS: ReadonlyArray<DeliveryCommentaryPattern> =
|
|
29
|
+
Object.freeze([
|
|
30
|
+
Object.freeze({
|
|
31
|
+
reason: 'tool.call.start-payload-signature',
|
|
32
|
+
regex: /\{\s*"type"\s*:\s*"tool\.call\.start"[\s\S]*?"toolName"\s*:\s*"[^"]+"[\s\S]*?\}/g,
|
|
33
|
+
description:
|
|
34
|
+
'JSON-encoded `tool.call.start` event with a `toolName` field - leaks the internal tool dispatch.',
|
|
35
|
+
}),
|
|
36
|
+
Object.freeze({
|
|
37
|
+
reason: 'tool.call.delta-payload-signature',
|
|
38
|
+
regex: /\{\s*"type"\s*:\s*"tool\.call\.delta"[\s\S]*?"argsDelta"[\s\S]*?\}/g,
|
|
39
|
+
description: 'JSON-encoded `tool.call.delta` event with streaming `argsDelta`.',
|
|
40
|
+
}),
|
|
41
|
+
Object.freeze({
|
|
42
|
+
reason: 'tool.call.end-payload-signature',
|
|
43
|
+
regex: /\{\s*"type"\s*:\s*"tool\.call\.end"[\s\S]*?"finalArgs"[\s\S]*?\}/g,
|
|
44
|
+
description: 'JSON-encoded `tool.call.end` event carrying the final args payload.',
|
|
45
|
+
}),
|
|
46
|
+
Object.freeze({
|
|
47
|
+
reason: 'tool.execute.end-payload-signature',
|
|
48
|
+
regex: /\{\s*"type"\s*:\s*"tool\.execute\.end"[\s\S]*?"result"\s*:\s*[\s\S]*?\}/g,
|
|
49
|
+
description: 'JSON-encoded `tool.execute.end` event with the raw `result` payload.',
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
reason: 'agent.fanout-event-signature',
|
|
53
|
+
regex: /\{\s*"type"\s*:\s*"agent\.fanout\.(?:spawned|merged)"[\s\S]*?\}/g,
|
|
54
|
+
description: 'JSON-encoded `agent.fanout.spawned` / `agent.fanout.merged` event.',
|
|
55
|
+
}),
|
|
56
|
+
Object.freeze({
|
|
57
|
+
reason: 'context.compacted-event-signature',
|
|
58
|
+
regex: /\{\s*"type"\s*:\s*"context\.compacted"[\s\S]*?"originalTokens"[\s\S]*?\}/g,
|
|
59
|
+
description: 'JSON-encoded `context.compacted` event with token deltas.',
|
|
60
|
+
}),
|
|
61
|
+
Object.freeze({
|
|
62
|
+
reason: 'agent.model.fellback-event-signature',
|
|
63
|
+
regex: /\{\s*"type"\s*:\s*"agent\.model\.fellback"[\s\S]*?"fromModel"[\s\S]*?\}/g,
|
|
64
|
+
description: 'JSON-encoded `agent.model.fellback` event with fallback chain detail.',
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default whitelist of `event.type` strings the dispatcher walks
|
|
70
|
+
* through the sanitizer. Extension is opt-in via
|
|
71
|
+
* `DeliveryCommentaryConfig.applyToEvents`.
|
|
72
|
+
*
|
|
73
|
+
* @stable
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_APPLY_TO_EVENTS: ReadonlyArray<string> = Object.freeze([
|
|
76
|
+
'tool.execute.end',
|
|
77
|
+
'tool.execute.error',
|
|
78
|
+
'text.delta',
|
|
79
|
+
]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery-layer commentary-phase trace sanitization for
|
|
3
|
+
* `@graphorin/server`.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
bridgeCommentaryToAudit,
|
|
10
|
+
COMMENTARY_AUDIT_ACTION,
|
|
11
|
+
type CommentaryAuditSink,
|
|
12
|
+
commentaryDecisionToAuditInput,
|
|
13
|
+
createLateBoundCommentarySink,
|
|
14
|
+
type LateBoundCommentarySink,
|
|
15
|
+
} from './audit-bridge.js';
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_APPLY_TO_EVENTS,
|
|
18
|
+
DEFAULT_DELIVERY_COMMENTARY_PATTERNS,
|
|
19
|
+
} from './built-in-patterns.js';
|
|
20
|
+
export {
|
|
21
|
+
createDeliveryCommentarySanitizer,
|
|
22
|
+
type DeliveryCommentarySanitizer,
|
|
23
|
+
} from './sanitizer.js';
|
|
24
|
+
export type {
|
|
25
|
+
DeliveryCommentaryConfig,
|
|
26
|
+
DeliveryCommentaryDecision,
|
|
27
|
+
DeliveryCommentaryPattern,
|
|
28
|
+
DeliveryCommentaryPolicy,
|
|
29
|
+
DeliveryCommentaryReason,
|
|
30
|
+
DeliveryCommentarySink,
|
|
31
|
+
DeliveryCommentaryTransport,
|
|
32
|
+
} from './types.js';
|