@vonzio/plugin-api 0.1.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/index.d.ts ADDED
@@ -0,0 +1,945 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ /**
3
+ * Current plugin-api version. Plugins encode the version they were built
4
+ * against in their package.json `vonzio.apiVersion`; the loader rejects
5
+ * plugins whose major differs or whose minor is ahead of core's (see
6
+ * `assertApiCompatible`). Bumped to 1.0.0 with the external-loader contract
7
+ * (docs/PLUGIN_LOADER_SPEC.md) — the loader surface is now a stability
8
+ * commitment.
9
+ */
10
+ export declare const PLUGIN_API_VERSION = "1.0.0";
11
+ /**
12
+ * The shape every plugin's default export must satisfy. Generic over
13
+ * the plugin's own config type so init() receives a fully-typed config.
14
+ */
15
+ export interface VonzioPlugin<TConfig = unknown> {
16
+ /**
17
+ * Stable identifier. Used for the auto-route prefix, log scope, and
18
+ * the migration namespace in the `_migrations` table. Conventionally
19
+ * matches the npm package's unscoped name (e.g. `telegram` for
20
+ * `@vonzio/plugin-telegram`).
21
+ */
22
+ name: string;
23
+ /**
24
+ * Semver of `@vonzio/plugin-api` this plugin was built against. The
25
+ * loader compares the major against core's PLUGIN_API_VERSION and
26
+ * refuses to load plugins targeting a newer major.
27
+ */
28
+ apiVersion: string;
29
+ /**
30
+ * Zod schema for the plugin's env-derived config. The loader calls
31
+ * `.parse(process.env)` on it and rejects malformed values with a
32
+ * useful error. Plugins should namespace their env vars
33
+ * (`SLACK_CLIENT_ID`, `TELLER_API_KEY`, etc.) to avoid collisions.
34
+ *
35
+ * Typed as `ConfigSchemaLike` (a structural shape with just
36
+ * `.parse()`) rather than `z.ZodType<TConfig>` for two reasons:
37
+ * 1. The workspace can have zod v3 and v4 simultaneously (different
38
+ * deps pin different majors); using zod's strong types here
39
+ * would force every plugin's schema to be the same major
40
+ * instance as plugin-api's.
41
+ * 2. ZodObject is technically a ZodType subtype, but TS variance
42
+ * rules reject ZodObject<T> as a ZodType<T> in many cases.
43
+ * The plugin's TConfig flows from `.parse()`'s return value at the
44
+ * use site, so typing stays useful where it matters.
45
+ */
46
+ configSchema: ConfigSchemaLike<TConfig>;
47
+ /**
48
+ * SQL migrations owned by this plugin. The core migration runner
49
+ * applies them in order interleaved with core's own, tagged in
50
+ * `_migrations` as `<plugin-name>_<migration-name>`. Plugins should
51
+ * keep migrations idempotent (CREATE TABLE IF NOT EXISTS, etc.) so
52
+ * partial-apply failures don't poison subsequent boots.
53
+ */
54
+ migrations?: PluginMigration[];
55
+ /**
56
+ * Where this plugin's routes live in the URL space. Default
57
+ * (`{ kind: "auto" }`) mounts everything under `/plugins/<name>/*`.
58
+ * Plugins with externally-registered URLs (Slack OAuth callback,
59
+ * Telegram webhook secret in the Telegram app config, etc.) can
60
+ * use `{ kind: "absolute", prefix }` to keep their legacy URLs and
61
+ * avoid forcing every self-hoster to update their third-party app
62
+ * configuration.
63
+ */
64
+ routePrefix?: PluginRoutePrefix;
65
+ /**
66
+ * Called once at server boot, after config parsing and after this
67
+ * plugin's migrations have been applied. The plugin registers its
68
+ * handlers via `ctx.server`, `ctx.notificationBus`,
69
+ * `ctx.mcpRegistry`, and `ctx.scheduler`. init() must not block on
70
+ * external services (e.g. an API call) -- those belong in scheduled
71
+ * jobs or lazy on-first-request initialization, so a flaky upstream
72
+ * doesn't block server startup.
73
+ */
74
+ init: (ctx: PluginContext<TConfig>) => Promise<void> | void;
75
+ /**
76
+ * Called on graceful server shutdown. Plugins MUST clear timers,
77
+ * cancel intervals, close sockets, and stop any worker threads they
78
+ * spawned. The scheduler-registered jobs are cancelled by core
79
+ * automatically, but anything the plugin spawned outside that
80
+ * channel is its own responsibility.
81
+ */
82
+ teardown?: () => Promise<void> | void;
83
+ }
84
+ /**
85
+ * Where the plugin's routes live. `auto` mounts under
86
+ * `/plugins/<plugin-name>/*` which is the recommended default --
87
+ * keeps URLs collision-free and self-documenting. `absolute` is the
88
+ * escape hatch for plugins that need a stable legacy URL (e.g. Slack
89
+ * OAuth callback is registered in the Slack app config; changing it
90
+ * would force every self-hoster to update their Slack app).
91
+ */
92
+ export type PluginRoutePrefix = {
93
+ kind: "auto";
94
+ } | {
95
+ kind: "absolute";
96
+ prefix: string;
97
+ };
98
+ /**
99
+ * One SQL migration. The core migration runner applies these in the
100
+ * order the plugin lists them, and records `<plugin-name>_<name>` in
101
+ * the `_migrations` table so a partial apply can resume cleanly.
102
+ */
103
+ export interface PluginMigration {
104
+ /**
105
+ * Migration name. Conventionally `NNNN_short_description.sql`-style
106
+ * (e.g. `0001_initial_schema`, `0002_add_thread_label`).
107
+ * Combined with the plugin name to form the key core stores.
108
+ */
109
+ name: string;
110
+ /** Idempotent SQL. Plugins should use CREATE ... IF NOT EXISTS etc. */
111
+ up: string;
112
+ }
113
+ /**
114
+ * What init() receives. Everything a plugin needs to integrate with
115
+ * core lives here -- plugins should never `import` from
116
+ * `@vonzio/core-server` directly.
117
+ */
118
+ export interface PluginContext<TConfig = unknown> {
119
+ /**
120
+ * Fastify scope. Routes registered here are auto-prefixed if the
121
+ * plugin uses the default `routePrefix`. The plugin still owns the
122
+ * relative URL space inside its prefix.
123
+ */
124
+ server: FastifyInstance;
125
+ /** The result of `configSchema.parse(process.env)`. */
126
+ config: TConfig;
127
+ /** Logger pre-tagged with `{ plugin: name }`. */
128
+ log: PluginLogger;
129
+ /**
130
+ * Versioned access to core services. At runtime this is a capability
131
+ * MEMBRANE (a revocable Proxy) — accessing a `core` surface the plugin
132
+ * did not declare + get granted throws `CapabilityViolationError` and is
133
+ * audited. The membrane is hygiene against honest mistakes via THIS
134
+ * reference; it is not a sandbox against `require('@vonzio/core-server')`.
135
+ * See docs/PLUGIN_LOADER_SPEC.md §2, §7.
136
+ */
137
+ core: PluginCore;
138
+ /**
139
+ * Per-plugin namespaced key/value store. Present only when the plugin
140
+ * declared `storage.kv` and the operator granted it; otherwise accessing
141
+ * it throws `CapabilityViolationError`. Preferred over `db.*` for new
142
+ * plugins (§5).
143
+ */
144
+ storage: PluginStorageKv;
145
+ /**
146
+ * Audited outbound HTTP. Present only when the plugin declared
147
+ * `http.outbound` (with a non-empty `outboundHosts`) and the operator
148
+ * granted it. Every call is SSRF-checked, allowlist-checked against
149
+ * manifest∩policy hosts, and logged. See §10.
150
+ */
151
+ http: PluginHttp;
152
+ /** Where the plugin claims a notification channel kind. */
153
+ notificationBus: NotificationBus;
154
+ /** Where the plugin contributes an MCP server. */
155
+ mcpRegistry: McpRegistry;
156
+ /**
157
+ * Resolve the per-task bearer token core attaches when it injects this
158
+ * plugin's MCP server into an agent container. Present only when the plugin
159
+ * declared `mcp.register` and the operator granted it; otherwise accessing it
160
+ * throws `CapabilityViolationError`. See §10.
161
+ */
162
+ mcpSessions: McpSessions;
163
+ /** Where the plugin schedules background work. */
164
+ scheduler: Scheduler;
165
+ /**
166
+ * Subscribe to session-lifecycle events emitted by the orchestrator.
167
+ * Used by integrations that relay task progress to external
168
+ * surfaces (e.g. Telegram chat, Slack thread).
169
+ */
170
+ sessionEvents: SessionEvents;
171
+ /**
172
+ * Resolve operator-provisioned secret material into opaque references.
173
+ * Present only when the plugin declared `secrets.mtls` (with a non-empty
174
+ * `manifest.mtlsSecrets`) and the operator both granted it and provisioned
175
+ * the cert/key files in policy; otherwise accessing it throws
176
+ * `CapabilityViolationError`. v1 covers mTLS client certs only. See §5, §10.
177
+ */
178
+ secrets: PluginSecrets;
179
+ }
180
+ /**
181
+ * One user's integration row, as seen by a plugin. Used by
182
+ * notification handlers to resolve `req.recipient` (an integration
183
+ * id) into the bot token / channel / chat id / etc. needed to send a
184
+ * message.
185
+ *
186
+ * `config` is type-erased on this contract -- the actual shape is
187
+ * provider-specific (Telegram: bot_token + owner_tg_user_id;
188
+ * Slack: bot_token + authed_user_id; etc.) and is the plugin's
189
+ * responsibility to assert. With `opts.decrypt: true` the loader
190
+ * runs the standard decrypt pass against config before returning.
191
+ */
192
+ export interface PluginIntegration {
193
+ id: string;
194
+ user_id: string;
195
+ type: string;
196
+ config: Record<string, unknown>;
197
+ enabled: boolean;
198
+ /**
199
+ * Last-modified timestamp (ISO-8601). Plugins use this for
200
+ * optimistic-locking writes -- see `update({...}, { expectUpdatedAt })`.
201
+ */
202
+ updated_at: string;
203
+ }
204
+ /**
205
+ * Adapter around core's IntegrationService. Plugins use this to look
206
+ * up + manage user-integration rows (Slack tokens, Telegram bots,
207
+ * Gmail OAuth grants, etc.). All eight methods mirror core's
208
+ * IntegrationService surface 1:1 -- the structural shape lets the
209
+ * loader pass the real service through without leaking the concrete
210
+ * class type into plugin-api.
211
+ */
212
+ export interface PluginIntegrationLookup {
213
+ get(id: string, opts?: {
214
+ decrypt?: boolean;
215
+ }): Promise<PluginIntegration | null>;
216
+ getByUserAndType(userId: string, type: string, opts?: {
217
+ decrypt?: boolean;
218
+ }): Promise<PluginIntegration | null>;
219
+ listByType(type: string, opts?: {
220
+ decrypt?: boolean;
221
+ }): Promise<PluginIntegration[]>;
222
+ listByUserAndType(userId: string, type: string, opts?: {
223
+ decrypt?: boolean;
224
+ }): Promise<PluginIntegration[]>;
225
+ findByTypeAndExternalId(type: string, externalId: string, opts?: {
226
+ decrypt?: boolean;
227
+ }): Promise<PluginIntegration | null>;
228
+ /**
229
+ * Multi-result variant for the case where a single external id maps
230
+ * to multiple integrations -- e.g. the platform Telegram bot has
231
+ * one bot_user_id but many user-integration rows (one per paired
232
+ * user). The relay routes by `(user_id, external_id)` to disambiguate.
233
+ */
234
+ listByTypeAndExternalId(type: string, externalId: string, opts?: {
235
+ decrypt?: boolean;
236
+ }): Promise<PluginIntegration[]>;
237
+ /**
238
+ * Lazy backfill of the indexed `external_id` column for legacy rows
239
+ * that pre-date the column. The plugin calls this on first read of
240
+ * a row that's missing the index -- avoids a one-time migration
241
+ * that would need decryption inside the migration runner.
242
+ */
243
+ backfillExternalId(id: string): Promise<void>;
244
+ create(userId: string, type: string, config: Record<string, unknown>, scopeInput?: unknown): Promise<PluginIntegration>;
245
+ /**
246
+ * Update an integration row. `opts.expectUpdatedAt` gates the write
247
+ * on the matching `updated_at` value -- used by the chat-surface
248
+ * pairing flow to make /link claims race-safe (the loser sees a
249
+ * null return and refuses without echoing "Linked.").
250
+ */
251
+ update(id: string, input: Partial<{
252
+ config: Record<string, unknown>;
253
+ enabled: boolean;
254
+ scope: string;
255
+ profile_ids: string[];
256
+ }>, opts?: {
257
+ expectUpdatedAt?: string;
258
+ }): Promise<PluginIntegration | null>;
259
+ delete(id: string): Promise<void>;
260
+ }
261
+ /**
262
+ * Narrow read-only profile lookup. Plugins use this when validating
263
+ * that a user-supplied profile_id (e.g. for binding a Telegram bot to
264
+ * a specific agent profile) actually belongs to the caller.
265
+ *
266
+ * Use `profileResolver.getResolved` (separate field on PluginCore) for
267
+ * the full ResolvedProfile shape with credentials, tools, claude_md,
268
+ * etc. This narrow surface keeps plugins that only need slug/name
269
+ * from pulling in that wider type tree.
270
+ */
271
+ export interface PluginProfileLookup {
272
+ /**
273
+ * Returns the full Profile shape (slug, name, model, default_tools,
274
+ * persistent_sessions, bound_profile_id, ...). Chat-surface pickers
275
+ * need most of these fields; mirroring a narrow subset structurally
276
+ * would silently drift as features land.
277
+ */
278
+ list(userId: string): Promise<Array<import("@vonzio/shared").Profile>>;
279
+ /**
280
+ * Single-row fetch by id. Same full Profile shape as `list`. Use
281
+ * `profileResolver.getResolved` when you need the resolved variant
282
+ * (credentials, env, setup_commands, ...).
283
+ */
284
+ get(profileId: string): Promise<import("@vonzio/shared").Profile | null>;
285
+ }
286
+ /**
287
+ * Workspace lookup + lightweight mutations. Plugins use this for:
288
+ * - ownership checks before exposing a workspace-bound resource
289
+ * (`get`)
290
+ * - chat-side `/list` commands that show the user their recent
291
+ * workspaces (`list`)
292
+ * - chat-side `/model` overrides and "set workspace title from first
293
+ * line of chat" updates (`update`)
294
+ */
295
+ export interface PluginWorkspaceLookup {
296
+ /**
297
+ * Returns the full Workspace shape from @vonzio/shared (session_id,
298
+ * user_id, profile_id, container_id, name, status, model_override,
299
+ * tags, ...). Chat surfaces use most of these for cross-resume,
300
+ * model display, and title updates.
301
+ */
302
+ get(sessionId: string): import("@vonzio/shared").Workspace | null;
303
+ list(filters: {
304
+ userId?: string;
305
+ orgId?: string;
306
+ status?: "active" | "resumable" | "idle" | "expired";
307
+ includeArchived?: boolean;
308
+ starredOnly?: boolean;
309
+ page?: number;
310
+ limit?: number;
311
+ }): Promise<{
312
+ workspaces: Array<import("@vonzio/shared").Workspace>;
313
+ total: number;
314
+ }>;
315
+ update(sessionId: string, fields: {
316
+ name?: string;
317
+ starred?: boolean;
318
+ pinned?: boolean;
319
+ archived?: boolean;
320
+ tags?: string[];
321
+ public_preview?: boolean;
322
+ model_override?: string | null;
323
+ last_run_model?: string | null;
324
+ }, opts?: {
325
+ orgId?: string;
326
+ }): Promise<import("@vonzio/shared").Workspace | null>;
327
+ }
328
+ /**
329
+ * Telegram platform-bot surface. As of Phase 3D.1d.1 the concrete
330
+ * class is plugin-internal (@vonzio/plugin-telegram/services/
331
+ * platform-bot-service.ts); this interface is the structural shape
332
+ * the plugin's own setup routes accept so they don't reach into the
333
+ * service module directly.
334
+ *
335
+ * No longer exposed on PluginCore -- other plugins should ignore it.
336
+ * Kept exported so the telegram plugin's setup-routes signature can
337
+ * reference the contract type instead of the concrete class.
338
+ */
339
+ export interface PluginTelegramPlatformBot {
340
+ getMetadata(): {
341
+ botUserId: string;
342
+ botUsername: string;
343
+ } | null;
344
+ getToken(): string | null;
345
+ getWebhookSecret(): string | null;
346
+ isConfigured(): boolean;
347
+ }
348
+ /**
349
+ * Session-lifecycle events emitted by the orchestrator. Plugins
350
+ * subscribe via `ctx.sessionEvents.on(event, handler)` to react to
351
+ * task progress without core having to know they exist -- e.g. the
352
+ * telegram plugin's relay echoes `task:token` to the user's Telegram
353
+ * chat, and `task:done` posts the final result.
354
+ *
355
+ * The signatures mirror orchestrator's existing `emit("task:*", ...)`
356
+ * calls exactly so the typed facade is a zero-overhead pass-through.
357
+ * sessionId may be `undefined` for tasks not bound to a session
358
+ * (one-off API calls); plugins typically early-return in that case.
359
+ *
360
+ * Handlers are NOT async-awaited by core -- they fire in parallel.
361
+ * Plugins that need ordering must coordinate via their own queues.
362
+ */
363
+ export interface SessionEvents {
364
+ on(event: "task:token", handler: (taskId: string, sessionId: string | undefined, text: string) => void): void;
365
+ on(event: "task:tool_use", handler: (taskId: string, sessionId: string | undefined, tool: string, input?: unknown) => void): void;
366
+ on(event: "task:ask_user", handler: (taskId: string, sessionId: string | undefined, input: unknown) => void | Promise<void>): void;
367
+ on(event: "task:done", handler: (taskId: string, sessionId: string | undefined, result?: {
368
+ text?: string;
369
+ }) => void | Promise<void>): void;
370
+ on(event: "task:failed", handler: (taskId: string, sessionId: string | undefined, error?: string) => void | Promise<void>): void;
371
+ /**
372
+ * Bulk unsubscribe -- called by core during plugin teardown so a
373
+ * reloaded plugin doesn't double up subscriptions. Plugins normally
374
+ * don't call this themselves.
375
+ */
376
+ off(event: SessionEventName, handler: (...args: never[]) => void): void;
377
+ }
378
+ export type SessionEventName = "task:token" | "task:tool_use" | "task:ask_user" | "task:done" | "task:failed";
379
+ /**
380
+ * Agent-facing description of one chat surface. Surfaced verbatim in
381
+ * the system-prompt Reachability section that tells the agent where a
382
+ * `AskUserQuestion` call will be delivered. `label` is the human
383
+ * sentence shown to the agent ("Telegram (chat bound — may take
384
+ * minutes if the user isn't near their phone)"); `slow` is true for
385
+ * surfaces with phone-typing latency, which triggers the
386
+ * "phrase as 2-4 button options" steer.
387
+ */
388
+ export interface PresenceSurfaceMetadata {
389
+ label: string;
390
+ slow?: boolean;
391
+ }
392
+ /**
393
+ * One chat-surface provider that core's presence/fallback logic walks
394
+ * to decide whether a session is reachable. Plugins register one of
395
+ * these for each surface they own (e.g. the telegram plugin
396
+ * registers `{ surface: "telegram", ... }`). Implementations may
397
+ * leave optional methods undefined when they don't apply -- the
398
+ * registry tolerates partial providers.
399
+ *
400
+ * The provider replaces the direct `db.select().from(schema.<plugin-
401
+ * table>)` calls that core used to do for each surface, breaking the
402
+ * reverse-coupling that blocks plugin schema moves.
403
+ */
404
+ export interface SessionPresenceProvider {
405
+ /**
406
+ * Stable surface key. Used for dedup (registering two providers
407
+ * for the same key throws at boot) and for logging. Conventionally
408
+ * matches the plugin name.
409
+ */
410
+ surface: string;
411
+ /** Agent-visible description; rendered verbatim. */
412
+ metadata: PresenceSurfaceMetadata;
413
+ /**
414
+ * "Is this session bound to a chat on my surface?" Used by the
415
+ * orchestrator's Reachability section and by ask-user fallback's
416
+ * in-band-suppression check. Errors are swallowed by the registry
417
+ * (treated as "no surface") so a flaky provider can't block a task.
418
+ */
419
+ hasSession(sessionId: string): Promise<boolean>;
420
+ /**
421
+ * "Will my surface deliver to this user's account-wide channel,
422
+ * regardless of session binding?" Telegram returns true if the user
423
+ * has a linked bot DM; Slack returns true if the user has a linked
424
+ * workspace DM. Used only by ask-user fallback to suppress its
425
+ * plain-text notification when the in-band relay will fire.
426
+ */
427
+ hasOwnerSurface?(userId: string): Promise<boolean>;
428
+ /**
429
+ * Session ids the user has actively engaged with via this surface
430
+ * (e.g. claimed a playbook thread). Used by the workspace list to
431
+ * keep these visible even when the standard "hide playbook
432
+ * executions" filter would drop them.
433
+ */
434
+ listEngagedSessionIds?(): Promise<Set<string>>;
435
+ /**
436
+ * Fallback user_id lookup when the session isn't in the in-process
437
+ * registry (e.g. a brand-new chat-initiated session hasn't reached
438
+ * the workspace registry yet). Walked by ask-user fallback in
439
+ * registry order; first non-null wins.
440
+ */
441
+ resolveUserIdBySession?(sessionId: string): Promise<string | null>;
442
+ }
443
+ /**
444
+ * Registration-side surface plugins program against. Plugins receive
445
+ * this via `PluginCore.sessionPresence` and call `register(provider)`
446
+ * at init() to contribute their surface. The query-side (used by
447
+ * core's orchestrator + fallback + workspace-service) is internal --
448
+ * plugins never iterate the registry themselves.
449
+ */
450
+ export interface PluginSessionPresenceRegistry {
451
+ register(provider: SessionPresenceProvider): void;
452
+ }
453
+ /**
454
+ * Subset of `SubmitTaskInput` plugins use when handing core a new
455
+ * task. Mirrors core's interface 1:1 but lives in plugin-api so
456
+ * plugins don't have to import from core-server. New optional fields
457
+ * can be added; required-field changes need an apiVersion bump.
458
+ */
459
+ export interface PluginTaskInput {
460
+ mode?: "session" | "batch" | "pooled" | "single";
461
+ prompt: string;
462
+ profile_id?: string;
463
+ session_id?: string;
464
+ allowed_tools?: string[];
465
+ egress_domains?: string[];
466
+ max_turns?: number;
467
+ max_budget_usd?: number;
468
+ model?: string;
469
+ effort?: string;
470
+ timeout_seconds?: number;
471
+ /**
472
+ * Inline file attachments (images, PDFs, text docs) the chat
473
+ * surface received with this message. Forwarded to the agent
474
+ * verbatim; the orchestrator handles MIME-typed presentation.
475
+ */
476
+ attachments?: Array<import("@vonzio/shared").TaskAttachment>;
477
+ }
478
+ /**
479
+ * Narrow facade over `TaskService.submit` for plugins that launch
480
+ * tasks from external triggers (e.g. a chat message arrives ->
481
+ * submit a session task). callerProfileIds gates which profiles the
482
+ * caller is allowed to submit against; plugins typically pass
483
+ * `[input.profile_id!]` since the trigger is bound to one profile.
484
+ */
485
+ export interface PluginTaskSubmitter {
486
+ submit(input: PluginTaskInput, callerProfileIds: string[]): Promise<{
487
+ task_id: string;
488
+ status: string;
489
+ created_at: string;
490
+ }>;
491
+ }
492
+ /**
493
+ * Session lifecycle mutations the plugin needs for chat-initiated
494
+ * sessions (e.g. /new in Telegram creates a session before any
495
+ * dashboard tab has bound to it). Read-only ownership stays on
496
+ * `core.workspaces`; this surface is for create-and-mutate.
497
+ */
498
+ export interface PluginSessionLifecycle {
499
+ /**
500
+ * Insert a new workspace row + in-memory entry. `persistent` true
501
+ * survives container teardown; `orgId` defaults to null on OSS.
502
+ */
503
+ register(sessionId: string, userId: string, profileId: string, opts?: {
504
+ persistent?: boolean;
505
+ orgId?: string | null;
506
+ }): Promise<{
507
+ session_id: string;
508
+ user_id: string;
509
+ profile_id: string;
510
+ }>;
511
+ /** Push expiry forward (ISO-8601 timestamp). */
512
+ extendExpiry(sessionId: string, expiresAtIso: string): Promise<void>;
513
+ /** Move the session between status states (e.g. "idle" -> "active"). */
514
+ setStatus(sessionId: string, status: "active" | "resumable" | "idle" | "expired"): Promise<void>;
515
+ /** Set of session_ids the dashboard currently has WS-connected. */
516
+ getConnectedSessionIds(): Set<string>;
517
+ }
518
+ /**
519
+ * Narrow orchestrator surface plugins call directly. Today this is
520
+ * just wakeWorkspaceContainer (the "boot a container for this session
521
+ * before submitting a task to it" call). More methods land here only
522
+ * with clear plugin need + a justification block on the field.
523
+ *
524
+ * `ResolvedProfile` is imported from @vonzio/shared rather than
525
+ * mirrored structurally -- the type is wide (env, tools, claude_md,
526
+ * setup_commands, persistent_sessions, ...) and the orchestrator
527
+ * reads more of it as features land. Mirroring would silently drift.
528
+ */
529
+ export interface PluginOrchestrator {
530
+ wakeWorkspaceContainer(sessionId: string, profile: import("@vonzio/shared").ResolvedProfile): Promise<string | null>;
531
+ }
532
+ /**
533
+ * Append-only session event log used by the dashboard's replay view.
534
+ * Plugins that bridge an external surface to a session (telegram /new
535
+ * inbound, slack reply) append user_message events so the dashboard
536
+ * timeline shows them. Read is used by webhook callback handlers
537
+ * that need to replay context.
538
+ */
539
+ export interface PluginEventLog {
540
+ append(sessionId: string, type: string, data: Record<string, unknown>): void;
541
+ read(sessionId: string, afterSeq?: number): Array<{
542
+ seq: number;
543
+ type: string;
544
+ data: Record<string, unknown>;
545
+ ts: number;
546
+ }>;
547
+ }
548
+ /**
549
+ * Dashboard WS push. Plugins call this to broadcast an event to the
550
+ * dashboard tab(s) watching a session (e.g. "an external chat reply
551
+ * just landed -- here it is in real time"). Same channel core uses
552
+ * for orchestrator events.
553
+ */
554
+ export interface PluginConnectionManager {
555
+ sendToSession(sessionId: string, message: Record<string, unknown>): void;
556
+ }
557
+ /**
558
+ * Image rewriter for inline `![alt](url)` markdown in agent output.
559
+ * Chat surfaces that don't render inline images (Telegram) need to
560
+ * strip the references and queue the URLs for a separate sendPhoto
561
+ * call. forSession signs the URLs so chat backends fetching them
562
+ * can authenticate against the preview gateway.
563
+ */
564
+ export interface PluginImageRewriter {
565
+ forSession(sessionId: string, text: string): Promise<{
566
+ textWithUrls: string;
567
+ textWithoutImages: string;
568
+ images: Array<{
569
+ url: string;
570
+ alt: string;
571
+ originalUrl: string;
572
+ }>;
573
+ } | null>;
574
+ }
575
+ /**
576
+ * Model picker for chat-side "switch model" interactions. Returns the
577
+ * available list + the profile's current default so the picker can
578
+ * mark a "current" pin without a second profile fetch.
579
+ */
580
+ export interface PluginModelList {
581
+ listForProfile(profileId: string): Promise<{
582
+ ok: true;
583
+ models: Array<{
584
+ id: string;
585
+ display_name: string | null;
586
+ provider: "anthropic" | "ollama";
587
+ }>;
588
+ profileDefault: string | null;
589
+ } | {
590
+ ok: false;
591
+ status: number;
592
+ error: string;
593
+ }>;
594
+ }
595
+ /**
596
+ * Extended profile lookup. The existing `PluginProfileLookup` only
597
+ * does `list(userId)`; chat surfaces also need to resolve a single
598
+ * profile's full ResolvedProfile (model defaults, allowed tools,
599
+ * setup commands, etc.) to hand to `core.orchestrator.wakeWorkspaceContainer`.
600
+ *
601
+ * Kept on a separate field rather than widening PluginProfileLookup
602
+ * so plugins that need only `.list()` aren't forced to depend on the
603
+ * full ResolvedProfile type tree.
604
+ */
605
+ export interface PluginProfileResolver {
606
+ getResolved(profileId: string): Promise<import("@vonzio/shared").ResolvedProfile | null>;
607
+ }
608
+ /**
609
+ * Core services exposed to plugins. Add fields here only with strong
610
+ * justification -- the surface is a stability commitment.
611
+ */
612
+ export interface PluginCore {
613
+ /**
614
+ * Drizzle handle. Plugins should only query their own tables; cross-
615
+ * table access requires going through a documented core call (none
616
+ * yet defined for v0.1). Typed as `unknown` in the contract -- the
617
+ * loader injects the real handle. Plugins cast to
618
+ * `NodePgDatabase<typeof pluginSchema>`.
619
+ */
620
+ db: unknown;
621
+ /**
622
+ * AES-256-GCM wrapper around the master vault key. Use this for
623
+ * any plugin-owned secret that lands in the DB (OAuth tokens, API
624
+ * keys, bot tokens). Never log decrypted values; never persist them
625
+ * outside the encryption flow.
626
+ */
627
+ encryption: {
628
+ encrypt(plaintext: string): string;
629
+ decrypt(ciphertext: string): string;
630
+ };
631
+ /**
632
+ * Integration lookup. Plugins resolve `req.recipient` against this
633
+ * to get the credentials + config for the user's connected
634
+ * provider (Slack workspace, Telegram bot, etc.).
635
+ */
636
+ integrations: PluginIntegrationLookup;
637
+ /**
638
+ * Profile lookup (read-only). Plugins use this when validating
639
+ * profile_ids in their own routes.
640
+ */
641
+ profiles: PluginProfileLookup;
642
+ /**
643
+ * Workspace lookup (read-only). Plugins use this for ownership
644
+ * checks on workspace-bound resources.
645
+ */
646
+ workspaces: PluginWorkspaceLookup;
647
+ /**
648
+ * Auth hook plugins can opt into. Plugins with routes that need
649
+ * authenticated access call
650
+ * `server.addHook("onRequest", ctx.core.authHook)` inside their
651
+ * route registration scope. v0.1 mirrors what core wires onto the
652
+ * /v1 fastify subscope -- a session-cookie-or-bearer check that
653
+ * populates request.user on success and returns 401 otherwise.
654
+ *
655
+ * Plugins whose routes are public (e.g. webhook receivers verified
656
+ * via a shared secret) skip this entirely.
657
+ */
658
+ authHook: import("fastify").onRequestHookHandler;
659
+ /**
660
+ * Where plugins contribute a chat-surface presence provider. Lets
661
+ * core's orchestrator + ask-user-fallback + workspace-service ask
662
+ * "is this session reachable on a chat surface?" without reading
663
+ * plugin-owned tables directly. The plugin's provider does the
664
+ * actual DB read.
665
+ */
666
+ sessionPresence: PluginSessionPresenceRegistry;
667
+ /**
668
+ * Submit new tasks from a plugin (e.g. a chat surface received a
669
+ * user message; submit a session task with the prompt). Same gate
670
+ * as the dashboard's submit route: caller must have access to the
671
+ * profile via callerProfileIds.
672
+ */
673
+ tasks: PluginTaskSubmitter;
674
+ /**
675
+ * Mutate session state. Used by chat surfaces that initiate
676
+ * sessions from outside the dashboard (e.g. Telegram /new).
677
+ */
678
+ sessionLifecycle: PluginSessionLifecycle;
679
+ /**
680
+ * Orchestrator surface: wake a workspace container for a session
681
+ * before submitting a task. Plugins call this from their session-
682
+ * resume codepath after pairing a chat message with an existing
683
+ * workspace.
684
+ */
685
+ orchestrator: PluginOrchestrator;
686
+ /**
687
+ * Append-only event log. Plugins write user_message events when
688
+ * relaying inbound chat messages so they show up in the dashboard
689
+ * timeline; read is used by webhook handlers that need session
690
+ * context.
691
+ */
692
+ eventLog: PluginEventLog;
693
+ /**
694
+ * Dashboard WS push surface. Plugins use it to broadcast events
695
+ * sourced from the external chat into the dashboard view of the
696
+ * session.
697
+ */
698
+ connectionManager: PluginConnectionManager;
699
+ /**
700
+ * Image rewriter for chat surfaces that don't render inline
701
+ * markdown images. Returns the stripped text + a signed-URL list
702
+ * the plugin can hand to the chat API's sendPhoto-equivalent.
703
+ */
704
+ imageRewriter: PluginImageRewriter;
705
+ /**
706
+ * Model list lookup for chat-side model pickers.
707
+ */
708
+ modelList: PluginModelList;
709
+ /**
710
+ * Resolved profile lookup (full ResolvedProfile shape). Required
711
+ * before calling `orchestrator.wakeWorkspaceContainer`. Separate
712
+ * from `profiles.list` to keep plugins that need only listing from
713
+ * pulling in the wider type tree.
714
+ */
715
+ profileResolver: PluginProfileResolver;
716
+ }
717
+ /** Minimal logger contract. Backed by core's pino logger at runtime. */
718
+ export interface PluginLogger {
719
+ info(meta: Record<string, unknown> | string, msg?: string): void;
720
+ warn(meta: Record<string, unknown> | string, msg?: string): void;
721
+ error(meta: Record<string, unknown> | string, msg?: string): void;
722
+ debug(meta: Record<string, unknown> | string, msg?: string): void;
723
+ }
724
+ /**
725
+ * One outbound notification request. Core services hand these to the
726
+ * notification bus, which dispatches to whichever plugin claimed the
727
+ * `kind`.
728
+ */
729
+ export interface NotificationRequest {
730
+ /**
731
+ * The channel family this request targets. Plugins claim a kind in
732
+ * `init()` via `notificationBus.registerHandler(kind, handler)`.
733
+ * Examples: `"telegram"`, `"slack"`, `"email"`.
734
+ */
735
+ kind: string;
736
+ /**
737
+ * Plugin-specific recipient identifier. For most plugins this is
738
+ * the user-integration id; the plugin uses it to look up the right
739
+ * bot token, channel, thread, etc. in its own DB.
740
+ */
741
+ recipient: string;
742
+ /** Human-readable body. Plugins may format-translate before sending. */
743
+ text: string;
744
+ /** Free-form per-message metadata (e.g. priority, thread anchors). */
745
+ metadata?: Record<string, unknown>;
746
+ }
747
+ /**
748
+ * Plugin-side notification handler. Returns a result so callers (and
749
+ * core's retry/circuit-breaker machinery) can react. Never throws --
750
+ * unexpected errors should be wrapped into `{ ok: false, retryable }`.
751
+ */
752
+ export type NotificationHandler = (req: NotificationRequest) => Promise<NotificationResult>;
753
+ export type NotificationResult = {
754
+ ok: true;
755
+ } | {
756
+ ok: false;
757
+ error: string;
758
+ retryable: boolean;
759
+ };
760
+ /**
761
+ * Where plugins register notification handlers. One handler per
762
+ * kind -- attempting to register a second handler for the same kind
763
+ * is an error (caught at boot).
764
+ */
765
+ export interface NotificationBus {
766
+ registerHandler(kind: string, handler: NotificationHandler): void;
767
+ }
768
+ /**
769
+ * Spec for an MCP server the plugin contributes. The loader hands
770
+ * these to the core MCP runtime, which exposes them to agents
771
+ * according to the agent's profile config.
772
+ */
773
+ export interface McpServerSpec {
774
+ /** Identifier shown in agent-side MCP listings. */
775
+ name: string;
776
+ /**
777
+ * How agents reach the server. `stdio` = spawn a process per agent
778
+ * session; `http` = a single endpoint reachable by all sessions.
779
+ *
780
+ * For `http`, `url` MUST be an absolute PATH (e.g. `/plugins/teller/mcp`) — the
781
+ * plugin serves its MCP route via `ctx.server` and core resolves the path
782
+ * against its internal server URL at injection time, so the plugin needn't
783
+ * know the internal host. External / protocol-relative / traversing urls are
784
+ * REFUSED at `registerServer`: core attaches a per-task bearer token and that
785
+ * token must never leave the deployment. Core adds the `Authorization` header
786
+ * itself; the plugin's route resolves it via {@link McpSessions.resolve}.
787
+ */
788
+ transport: {
789
+ type: "stdio";
790
+ command: string;
791
+ args?: string[];
792
+ env?: Record<string, string>;
793
+ } | {
794
+ type: "http";
795
+ url: string;
796
+ };
797
+ }
798
+ export interface McpRegistry {
799
+ registerServer(spec: McpServerSpec): void;
800
+ }
801
+ /**
802
+ * Identity behind a per-task token core minted when it injected the plugin's
803
+ * MCP server into an agent container. The plugin's MCP HTTP route reads the
804
+ * `Authorization: Bearer <token>` header and resolves it here to scope the
805
+ * call to the right user / profile / tenant. Gated by `mcp.register`.
806
+ */
807
+ export interface McpSessions {
808
+ /** Resolve a per-task MCP token, or null if unknown/expired. */
809
+ resolve(token: string): {
810
+ userId: string;
811
+ profileId: string;
812
+ orgId: string | null;
813
+ } | null;
814
+ }
815
+ /**
816
+ * Scheduled work the plugin owns. Core cancels everything registered
817
+ * here during teardown; plugins don't need to track timer handles.
818
+ */
819
+ export interface Scheduler {
820
+ /**
821
+ * Standard 5-field cron expression, evaluated in UTC. `name` is for
822
+ * logging + dedup -- registering the same name twice is an error.
823
+ */
824
+ cron(name: string, schedule: string, fn: () => Promise<void>): void;
825
+ /**
826
+ * Fixed-interval job. `ms` is the gap BETWEEN runs (not from start),
827
+ * so a slow fn won't queue up backlog.
828
+ */
829
+ interval(name: string, ms: number, fn: () => Promise<void>): void;
830
+ }
831
+ /**
832
+ * The auth-decorated user attached to a FastifyRequest by core's
833
+ * userAuthHook. Plugins receive this as `request.user` inside any
834
+ * route that runs under an auth-gated scope (e.g. /v1/*).
835
+ *
836
+ * Plugins typed against this interface should cast at the request
837
+ * site -- `const user = request.user as AuthUser` -- because the
838
+ * module augmentation that sets `user?: AuthUser` on FastifyRequest
839
+ * lives in core-server's auth/user-auth.ts; declaring it again here
840
+ * would create a conflicting declaration in plugin-api's tsconfig
841
+ * project.
842
+ *
843
+ * The shape mirrors core's AuthUser (id + email + role + minor
844
+ * fields) but is structurally typed here so plugin-api doesn't have
845
+ * to import from core-server.
846
+ */
847
+ export interface AuthUser {
848
+ id: string;
849
+ email: string;
850
+ name?: string;
851
+ role: string;
852
+ feature_flags?: string;
853
+ }
854
+ /**
855
+ * Per-plugin key/value store (`ctx.storage`). Backed by the core-owned
856
+ * `plugin_storage` table; every read/write is filtered server-side by the
857
+ * plugin's id, so one plugin cannot read another's keys via this surface.
858
+ * Gated by the `storage.kv` capability. See §5.
859
+ */
860
+ export interface PluginStorageKv {
861
+ get<T>(key: string): Promise<T | null>;
862
+ set<T>(key: string, value: T): Promise<void>;
863
+ delete(key: string): Promise<void>;
864
+ list(prefix?: string): Promise<Array<{
865
+ key: string;
866
+ value: unknown;
867
+ }>>;
868
+ }
869
+ /**
870
+ * Audited outbound HTTP/WS surface (`ctx.http`). Gated by `http.outbound`.
871
+ * Every call resolves the hostname, blocks SSRF targets (private/link-local
872
+ * IPs, DNS rebinding), and requires the host to match the manifest∩policy
873
+ * `outboundHosts` allowlist. See §10.
874
+ *
875
+ * `fetch` returns a real WHATWG `Response`, so existing `.json()` / `.ok` /
876
+ * `.text()` / `.arrayBuffer()` call sites keep working — binary downloads use
877
+ * `.arrayBuffer()` and are not corrupted (the bytes are preserved end to end).
878
+ */
879
+ export interface PluginHttpInit {
880
+ method?: string;
881
+ headers?: Record<string, string>;
882
+ body?: string | Uint8Array | FormData;
883
+ /** Per-call timeout override (ms), capped at 30s. */
884
+ timeoutMs?: number;
885
+ /** Per-call max response size override (bytes), capped at 5 MiB. */
886
+ maxResponseBytes?: number;
887
+ /**
888
+ * Present a client certificate for mutual TLS on this call. Pass an
889
+ * {@link MtlsRef} obtained from `ctx.secrets.mtls(name)` — core reads the
890
+ * operator-provisioned cert/key files server-side at request time; the cert
891
+ * material never passes through plugin code. Gated by `secrets.mtls`. See §10.
892
+ */
893
+ mtls?: MtlsRef;
894
+ }
895
+ export interface PluginHttp {
896
+ fetch(url: string, init?: PluginHttpInit): Promise<Response>;
897
+ }
898
+ /**
899
+ * Opaque handle to operator-provisioned mTLS client material, resolved from a
900
+ * logical name the plugin declared in `manifest.mtlsSecrets`. The plugin CANNOT
901
+ * read the cert/key bytes through this object — it carries only the logical
902
+ * `name`. The cert/key files are read server-side when the ref is passed to
903
+ * `ctx.http.fetch({ mtls })`. See §5, §10.
904
+ */
905
+ export interface MtlsRef {
906
+ /** Brand: lets the HTTP surface recognize a genuine ref and keeps the type
907
+ * nominal to plugin authors. */
908
+ readonly __vonzioMtls: true;
909
+ /** The logical secret name — the only field a plugin can observe. */
910
+ readonly name: string;
911
+ }
912
+ /**
913
+ * Secret-material resolution surface (`ctx.secrets`). Gated by `secrets.mtls`.
914
+ * v1 exposes only mTLS client certs as opaque {@link MtlsRef}s; the bytes are
915
+ * never readable through this surface (the operator provisions them as host
916
+ * files in policy and core loads them server-side at request time).
917
+ */
918
+ export interface PluginSecrets {
919
+ /**
920
+ * Resolve a declared mTLS secret `name` (from `manifest.mtlsSecrets`) into an
921
+ * opaque ref for `ctx.http.fetch({ mtls })`. Throws `CapabilityViolationError`
922
+ * if the name was not declared + provisioned.
923
+ */
924
+ mtls(name: string): MtlsRef;
925
+ }
926
+ export { PLUGIN_CAPABILITIES, CAPABILITY_SURFACE_MAP, ROOT_EQUIVALENT_COMBINATIONS, BUILTIN_ONLY_CAPABILITIES, isPluginCapability, } from "./capabilities.js";
927
+ export type { PluginCapability, CapabilitySurface, SurfaceKind } from "./capabilities.js";
928
+ export { MANIFEST_ALLOWED_KEYS, POLICY_ENTRY_ALLOWED_KEYS, SCHEMA_PREFIX_PATTERN, MTLS_SECRET_NAME_PATTERN, } from "./manifest.js";
929
+ export type { PluginManifest, ManifestRoutePrefix, MtlsSecretFiles, PluginSource, PolicyEntry, OperatorPolicy, } from "./manifest.js";
930
+ export { validateManifest, validatePolicy, matchOutboundHost, normalizeHostPattern, } from "./manifest-validate.js";
931
+ export type { ManifestValidationResult } from "./manifest-validate.js";
932
+ export { CapabilityViolationError, OutboundHostViolationError, DbScopeViolationError, PluginRefusedError, PolicyViolationError, REFUSAL_REASONS, } from "./errors.js";
933
+ export type { RefusalReason } from "./errors.js";
934
+ export { assertApiCompatible } from "./version.js";
935
+ /**
936
+ * Structural shape the loader needs from a plugin's `configSchema`.
937
+ * Both zod v3 and v4 satisfy this naturally -- they both expose a
938
+ * `parse(input): T` method. Plugins typically use `z.object({...})`
939
+ * which yields a `ZodObject` whose `.parse()` returns the inferred
940
+ * shape; the inference flows into TConfig.
941
+ */
942
+ export interface ConfigSchemaLike<TConfig> {
943
+ parse(input: unknown): TConfig;
944
+ }
945
+ //# sourceMappingURL=index.d.ts.map