-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhypaware-plugin-kernel-types.d.ts
More file actions
3003 lines (2804 loc) · 115 KB
/
Copy pathhypaware-plugin-kernel-types.d.ts
File metadata and controls
3003 lines (2804 loc) · 115 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Draft public interfaces for the HypAware plugin-kernel design.
*
* This file is a design artifact, not an implementation contract yet. It is
* intentionally expressed as .d.ts because these shapes would become the
* reusable public API exposed to bundled and external plugins.
*
* The companion design rationale lives in the LLP corpus under `llp/`; start at
* [`llp/0000-hypaware.explainer.md`](./llp/0000-hypaware.explainer.md) and the
* per-subsystem LLPs (e.g. plugin manifest LLP 0005, capabilities LLP 0006).
* When the two disagree the LLPs win; this file is updated to follow.
*/
import type { AsyncDataSource, ScanOptions, ScanResults } from 'squirreling'
import type { CachePartitioningDeclaration } from './src/core/iceberg/types.d.ts'
import type { UsagePolicyDrop } from './src/core/usage-policy/types.d.ts'
import type { GrepSearchBackend } from './src/core/search/types.d.ts'
export type { AsyncDataSource, ScanOptions, ScanResults }
/**
* A data source that retains Squirreling's row interface. Hypaware's storage,
* union, visibility, and legacy parquet adapters all guarantee this stronger
* shape even when they also expose prepared native batches.
*/
export type ScannableDataSource = AsyncDataSource & {
columns: string[]
scan(options: ScanOptions): ScanResults
}
export type JsonPrimitive = string | number | boolean | null
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]
export interface JsonObject {
[key: string]: JsonValue
}
export type PluginName = string
/**
* Versioned capability identifier. Well-known capabilities at V1:
* - `hypaware.ai-gateway`: local HTTP/SSE AI gateway, provided by
* `@hypaware/ai-gateway`. Consumed by client adapter plugins.
* - `hypaware.blob-store`: object-store API (put/get/list/delete),
* provided by blob destination plugins (`@hypaware/local-fs`,
* `@hypaware/s3`). The capability VALUE is a `BlobStore`; consumers
* call its methods directly. Consumed by table-format plugins.
* - `hypaware.encoder`: per-batch byte encoder, provided by writer
* plugins (`@hypaware/format-parquet`, `@hypaware/format-jsonl`).
* Consumed by table-format plugins and blob destinations.
* - `hypaware.table-format`: directory layout + manifests on top of a
* blob store and encoder. Provided by `@hypaware/format-iceberg`.
* The capability VALUE is a `TableFormatProvider`.
* - `hypaware.http-endpoint`: request destination capability, provided
* by request sinks (`@hypaware/central`, future `@hypaware/webhook`).
* - `hypaware.embedder`: text embedding production, provided by
* embedder plugins (`@hypaware/embedder-openai`, future local
* embedders). The capability VALUE is an `EmbedderCapability`.
* Consumed by `@hypaware/vector-search`.
* - `hypaware.vector-search`: vector similarity search over cached
* datasets, provided by `@hypaware/vector-search`. The capability
* VALUE is a `VectorSearchCapability`.
*
* Plugins are free to define new capability names; the kernel does not
* gate registration on an enum.
*/
export type CapabilityName = string
export type SemverRange = string
export type SemverVersion = string
export type WriteStream = {
write(chunk: string): unknown
}
export type PluginPermission =
| 'read_config'
| 'write_config'
| 'read_home'
| 'write_home'
| 'read_state'
| 'write_state'
| 'network'
| 'spawn_process'
| 'read_claude_transcripts'
| 'write_claude_settings'
| 'write_codex_settings'
| 'write_openclaw_settings'
| string
export type PluginRuntime = 'node'
// =============================================================================
// Errors
// =============================================================================
/**
* Tagged error shape used across HypAware. Code that throws attaches
* `hypErrorKind` to a plain `Error`; consumers (tests, log enrichers,
* conflict detectors) read it back. Optional `code`, `status`,
* `statusCode` mirror Node/HTTP idioms when the error originated from
* a system or remote call.
*/
export interface HypError extends Error {
hypErrorKind: string
code?: string
status?: number
statusCode?: number
}
// =============================================================================
// Manifest and install metadata
// =============================================================================
export interface PluginManifest {
schema_version: 1
name: PluginName
version: SemverVersion
description?: string
/** Semver range against the HypAware kernel's plugin API. */
hypaware_api: SemverRange
/** Execution model. V1 supports `node` only (in-process from `entrypoint`). */
runtime: PluginRuntime
/** Required Node engine (e.g. ">=20"). */
node_engine?: SemverRange
/** Path to the bundled JS entrypoint. */
entrypoint: string
requires?: PluginRequirements
provides?: PluginProvides
permissions?: PluginPermission[]
contributes?: PluginContributionManifest
/**
* Plugins whose presence in a composed config pulls this one in with
* them. When the walkthrough composes every plugin named here, it
* composes this plugin too; when it composes none of them, this plugin
* is not written.
*
* This is how a **derived-data** plugin rides a pick it does not
* contribute: `@hypaware/context-graph` has no picker row of its own,
* because "project my sessions into a graph" is not a thing the user is
* asked, and it is useless without a source to project.
*
* Distinct from `requires.plugins`, which is a hard dependency governing
* activation order and presence. `requires` says "I cannot run without
* this"; `compose_with` says "write me down wherever this is written
* down". A plugin may declare either, both, or neither.
*/
compose_with?: PluginName[]
}
export interface PluginRequirements {
/** Named plugins that must be installed and activated before this plugin. */
plugins?: Record<PluginName, SemverRange>
/** Versioned capabilities that must be provided by another plugin. */
capabilities?: Record<CapabilityName, SemverRange>
}
export interface PluginProvides {
/** Capabilities implemented by this plugin, keyed by capability name. */
capabilities?: Record<CapabilityName, SemverVersion>
}
export interface PluginContributionManifest {
client?: PluginClientManifest
/**
* Picker rows this plugin contributes to the `hyp init` wizard. One
* plugin may contribute more than one row (e.g. `@hypaware/ai-gateway`
* contributes both `raw-anthropic` and `raw-openai`), so this is an
* array, sibling to the single `client` descriptor above.
*/
picker?: PluginPickerContribution[]
commands?: PluginCommandManifest[]
config_sections?: PluginConfigSectionManifest[]
sources?: PluginSourceManifest[]
sinks?: PluginSinkManifest[]
datasets?: PluginDatasetManifest[]
skills?: PluginSkillManifest[]
agents?: PluginAgentManifest[]
init_presets?: PluginInitPresetManifest[]
}
export interface PluginClientManifest {
name: string
skill_dir: string
/**
* Per-client subagent directory relative to the user's home (e.g.
* `.claude/agents`). Absent for clients without a subagent concept.
*/
agent_dir?: string
attach_probe?: PluginAttachProbeManifest
required_upstreams?: string[]
/**
* Transcript `entrypoint` values whose sessions belong to this client.
*
* Some clients write their history into another client's transcript
* tree: Claude Desktop's sessions land in `~/.claude/projects` beside
* Claude Code's, tagged `entrypoint: "claude-desktop"`. Without this
* mapping the `@hypaware/claude` backfill imports them as its own,
* so Desktop history enters the cache with no Desktop opt-in and is
* attributed to the wrong client.
*
* A client may claim several values: Desktop's transcripts say
* `claude-desktop` while its live third-party-inference route stamps
* `claude-desktop-3p` (LLP 0133#attribution).
*
* Declared here rather than in a core table so adding a client gates
* and attributes its entrypoints automatically.
*/
transcript_entrypoints?: string[]
/**
* How to start this client on a question, for the wizard's closing
* first ask and `hyp ask` (LLP 0198#split). Absent for a client that
* cannot be started on a prompt at all: Claude Desktop is a GUI app
* with no prompt argument, so it is detectable, pickable, and
* attachable but never launchable.
*/
launch?: PluginClientLaunchManifest
/**
* Where this client's own activity leaves a file trail, for the
* `hyp status` capture-health comparison (LLP 0257#status-and-health):
* the newest matching mtime under `dir` is the client's last activity,
* which status holds against the telemetry the daemon actually
* captured. Declared here rather than in a core table for the same
* reason as `attach_probe`: the path is the client's business, and
* core must be able to read it without importing plugin code.
*/
activity_probe?: PluginActivityProbeManifest
/**
* Set to `false` when this client registers no backfill provider of its
* own because another plugin's provider imports its history: Claude
* Desktop's transcripts are read by the `@hypaware/claude` provider.
* Absent means the plugin registers a provider (every other client
* adapter does).
*
* Read by `hyp status`, which cannot see the runtime backfill registry
* without activating plugins and so derives backfill-on-join targets
* from the client descriptors. Without this flag a provider-less client
* on a joined host reads as a `backfill ... [pending]` that no
* reconciler pass will ever clear; with it the line reads `n/a`, the same
* answer a probe-less client gets for attach (LLP 0229).
*
* Opt-out rather than opt-in so a client that forgets the flag shows the
* loud permanent `pending` this exists to fix, never a silently missing
* line for a provider that is real.
*/
backfill_provider?: boolean
}
/**
* A client-written directory core may stat (never parse) to answer
* "when was this client last active?". Same home-relative contract as
* `attach_probe.settings_file`: relative to `$HOME`, first segment
* relocatable by `$<CLIENT>_HOME`, absolute paths rejected.
*/
export interface PluginActivityProbeManifest {
/** Directory of activity files, RELATIVE to the user's home (e.g. `.claude/projects`). */
dir: string
/** Only files ending in this suffix count (e.g. `.jsonl`); absent means every file. */
file_suffix?: string
}
/**
* A client's launch spec: the binary to look for on `$PATH` and the
* argv to start it with. Exactly one `args` element must contain the
* `{prompt}` placeholder, which is replaced with the user's question;
* a spec without it would start the client mute, which looks like the
* feature working (LLP 0198#split).
*/
export interface PluginClientLaunchManifest {
/** Executable name resolved against `$PATH` (e.g. `claude`). */
bin: string
/** Argv template; `{prompt}` in any element is replaced. */
args: string[]
/** Display name for the launch row (e.g. `Claude Code`). */
label?: string
}
export interface PluginAttachProbeManifest {
/**
* `json_path` returns here (LLP 0173 T1), reversing LLP 0143's removal.
* LLP 0143 pulled the format because, at the time, nothing validated
* `attach_probe` at runtime (`src/core/manifest.js` treats `contributes`
* opaquely): a manifest declaring `json_path` with no runtime support
* behind it would type-check, probe as never-attached, and then slip
* past `action_attach.js`'s `!descriptor.attachProbe` orphaning guard on
* reverse, dropping the marker with the client's settings still written.
* That was exactly #212. The danger was in the *gap* between declaring
* the format and a runtime that reads/undoes it, not in the format
* itself. LLP 0173 T2 restores the undo side
* (`client_detach_disk.js`'s `detachJsonPathProviders`) and T3 restores
* the read side (`daemon/status.js`'s `json_path` branch) before
* OpenClaw's manifest (T5) declares this format again, so the gap #212
* warned about is closed by construction: no manifest may reach this
* format without both runtime sides already merged.
*
* @ref LLP 0172#lane-a-detach [implements]: json_path's runtime undo
* (client_detach_disk.js) and read (daemon/status.js) sides, which close
* the #212 gap this format's prior removal warned about.
*/
format: 'json' | 'toml' | 'json_path' | 'managed_file'
/**
* The client's settings file, RELATIVE to the user's home (e.g.
* `.codex/config.toml`). Its first path segment is the client's config
* home, which a `$<CLIENT>_HOME` env override replaces. An absolute
* path is rejected, not honored: it has no config home for the
* override to relocate, and core resolves this field for the attach
* probe and for the disk-driven detach alike, so a value core cannot
* resolve must fail rather than resolve to something else.
*/
settings_file: string
marker_key?: string
marker_header?: string
/** `managed_file` only: exact ownership marker required for probe and removal. */
marker_text?: string
/**
* `json_path` only: dotted path, relative to the parsed settings file,
* to the container object the probe/undo navigate (e.g.
* `models.providers`).
*/
container_path?: string
/**
* `json_path` only: the container's keys the probe/undo consider, in
* order (e.g. `['anthropic', 'openai']`). `marker_header` (reused, not
* duplicated) is checked against each key's own header value to decide
* ownership.
*/
provider_keys?: string[]
/**
* `json_path` only: glob, relative to the client's config home, of
* cache files the undo best-effort purges the same `provider_keys`
* entries from after the settings-file write (e.g.
* `agents/*\/agent/models.json`).
*/
cache_glob?: string
}
/**
* One row in the `hyp init` wizard's client/source picker, contributed
* declaratively by a plugin's manifest rather than a hardcoded core
* table (LLP 0130). `name` is the picker source id that keys the row
* (e.g. `claude`, `codex`, `raw-anthropic`); one plugin may contribute
* more than one row (`@hypaware/ai-gateway` contributes both
* `raw-anthropic` and `raw-openai`), so each row names its own id
* rather than inheriting the plugin's package name. The remaining
* fields drive the row's label, initial detection, and, for a
* `needs_setup` row, the command that configures it.
*/
export interface PluginPickerContribution {
/** Picker source id keying this row. */
name: string
/** Human-readable row label shown in the picker prompt. */
label: string
/** One-line description of what picking this row captures. */
summary?: string
/**
* Best-effort presence probe seeding the row's initial checkbox
* state. A probe failure means "not present," never an error.
*/
detect?: PickerDetectProbe
/**
* True when the row is kept out of the interactive picker menu while
* staying a real picker source everywhere else: `hyp init --source
* <id>` still composes it, a config that already collects it still
* reads back as collecting it, and the id keeps its identity in the
* opt-out/sync store and in the dataset-owner map the export-seam
* withholding rules key on. For a row whose audience is narrow enough
* that a first-run checkbox costs every other user more than it earns
* that one.
*/
hidden?: boolean
/**
* The `process.platform` values this row is offered on, for a client
* whose integration only exists on some of them. Absent means every
* platform, which is what almost every row wants.
*
* Like `hidden`, this gates DISPLAY only: `hyp init --source <id>`
* still composes the row, read-back still recognizes it, and the id
* keeps its dataset-owner identity. It differs from `detect` in being
* a fact about the integration rather than about this machine, so it
* can withhold a row instead of merely leaving its box unchecked.
*/
platforms?: string[]
/**
* True when picking this row is not sufficient on its own: an
* attended `configure_command` must run to place the integration
* (e.g. Claude Desktop's managed-preferences plist). Absent/false
* rows are configured entirely by the picker's config write.
*/
needs_setup?: boolean
/**
* Command name (as registered under `contributes.commands`) the
* wizard's configure phase invokes for a `needs_setup` row, run in
* process through `CommandRunContext.commands.run`.
*/
configure_command?: string
/**
* Composition contribution: the data `composePickerConfig` folds to
* build the local-layer config when this row is picked (LLP 0130). It
* carries, in manifest data, the same knowledge the retired hardcoded
* `composePickerConfig` switch held in core: which plugin instance the
* pick adds, whether it needs the local AI gateway, and which gateway
* upstream(s) it requests. Rows with no `compose` (a detection-only or
* `needs_setup` client the picker's config write handles) contribute
* nothing to the fold.
*/
compose?: PluginPickerCompose
}
/**
* A picker row's composition contribution, folded by
* `composePickerConfig` (LLP 0130#picker-block). Every field is
* optional: a row may add a plugin, request the gateway, contribute
* gateway upstreams, or any combination.
*/
export interface PluginPickerCompose {
/**
* Plugin instance added to the composed config when this row is
* picked. A gateway-requiring plugin (`requires_gateway: true`) is
* placed after the export sink plugins; a gateway-independent plugin
* is placed before them, matching the retired switch's plugin order.
*/
plugin?: PluginConfigInstance
/**
* Additional plugin instances added alongside `plugin` when this row is
* picked, in array order, under the same gateway-relative placement
* rule. A row needs this when its adapter cannot activate alone: the
* Claude Desktop row composes `@hypaware/claude-account` beside
* `@hypaware/claude-desktop` because the latter's manifest requires the
* `hypaware.anthropic-credential` capability only the former provides,
* and a row that composes half its dependency set writes a config whose
* own `configure_command` cannot resolve.
*/
plugins?: PluginConfigInstance[]
/**
* True when picking this row implies the local AI gateway
* (`@hypaware/ai-gateway`). The gateway plugin is included once when
* any picked row sets this.
*/
requires_gateway?: boolean
/**
* Gateway upstream(s) this row requests. The fold unions the requested
* upstreams across all picked rows, deduped by `name`, into the
* gateway plugin's `upstreams`. Accepts a single upstream or an array.
*/
gateway_upstream?: PluginPickerGatewayUpstream | PluginPickerGatewayUpstream[]
/**
* True when this row's client attaches through the gateway's CONNECT
* front door rather than a repointed base URL. The fold writes
* `proxy_mode: true` into the composed gateway config when any picked
* row sets this, so a fresh install boots interception and mints the
* local CA before the client's first attach. Attach reads the same
* declaration to decide whether to offer the proxy-mode migration on
* an existing install.
* @ref LLP 0243#composed-default: a proxy-attaching row declares it; composition writes the explicit key
*/
gateway_proxy_mode?: boolean
}
/**
* One upstream a picker row requests on the local AI gateway.
*/
export interface PluginPickerGatewayUpstream {
name: string
base_url: string
path_prefix: string
provider?: string
}
/**
* A picker row's presence probe. Exactly one variant key is set; the
* detector switches on which key is present.
*/
export type PickerDetectProbe =
| { settings_file: string } // reuses the `contributes.client.attach_probe` settings-file shape, home-relative likewise
| { app_bundle: string } // stat-exists check on a macOS `.app` bundle path
| { path: string } // stat-exists check on a directory (honors `$FOO_HOME`-style env overrides)
export interface PluginCommandManifest {
name: string
/** Help presentation category, mirrored onto the runtime registration. */
category?: string
audience?: 'everyday' | 'operator' | 'developer' | 'machine'
/** Compatibility spellings used for inactive-plugin ownership checks. */
aliases?: string[]
summary?: string
usage?: string
/**
* True when the command is an internal mechanism rather than CLI
* surface: it keeps dispatching, and it keeps its manifest entry, but
* it is absent from `hyp --help` and from its group's help. For a
* command whose caller is a program (a credential-helper wrapper, an
* in-process orchestration step), not a person (LLP 0268).
*
* Declaring it hidden rather than deleting the entry is deliberate,
* the same display-filter-not-catalog-deletion rule LLP 0202 set for
* hidden picker rows: the declaration is what lets the dispatch-miss
* path name the owning plugin ("unavailable", not "unknown",
* LLP 0153) and what the manifest/registration parity tests compare.
*
* The runtime registration must set `CommandRegistration.hidden` to
* match; the manifest field governs pre-boot help, the registration
* field governs post-boot group help.
*/
hidden?: boolean
}
export interface PluginConfigSectionManifest {
section: string
summary?: string
}
export interface PluginSourceManifest {
name: string
summary?: string
}
export interface PluginSinkManifest {
name: string
/**
* Capability tags this sink supports. Renamed from the older
* `capabilities` array to avoid clashing with the global capability
* registry. Recognized at V1: `"queryable"`. More tags may land
* without changing the shape.
*/
supports: SinkSupportTag[]
summary?: string
}
export type SinkSupportTag = 'queryable' | string
export interface PluginDatasetManifest {
name: string
summary?: string
source?: string
/**
* Row column carrying the picker source id a row is attributed to
* (e.g. `client_name` for `ai_gateway_messages`, where claude/codex/
* hermes rows all land). Enables source-scoped export withholding:
* a dataset with no declared `attribution_column` is never subject
* to source-scoped withholding (LLP 0132).
*/
attribution_column?: string
}
export interface PluginSkillManifest {
name: string
clients: PluginSkillClient[]
source_dir?: string
}
export interface PluginAgentManifest {
name: string
clients: PluginSkillClient[]
source_file?: string
}
export interface PluginInitPresetManifest {
name: string
summary?: string
}
export type PluginSkillClient = 'claude' | 'codex' | 'all'
// =============================================================================
// Plugin discovery, install, and lock
// =============================================================================
/**
* Short-name resolver kinds. The kernel tries first-party, then scoped
* third-party, then unscoped third-party. All three resolve down to a
* git source: the kernel fetches a prebuilt artifact from git and
* never runs `npm install` on the user's machine. npm is a naming
* authority (and metadata lookup for third-party), not an install
* source.
*/
export type PluginSourceKind =
| 'first-party'
| 'scoped-third-party'
| 'unscoped-third-party'
| 'git'
| 'local-dir'
export interface PluginSourceSpec {
kind: PluginSourceKind
raw: string
/** Resolved plugin name (e.g. `@hypaware/ai-gateway`). */
name?: PluginName
/** Resolved git URL (e.g. `github:hyperparam/hypaware-ai-gateway`). */
gitUrl?: string
/** Optional git ref (tag, branch, sha) to install from. */
ref?: string
/** Local directory source for development installs. */
path?: string
/** Optional subdirectory inside a git source. Reserved; rejected until subdir support lands. */
subdir?: string
}
export interface PluginLockFile {
schema_version: 1
plugins: Record<PluginName, PluginLockEntry>
}
export interface PluginLockEntry {
name: PluginName
version: SemverVersion
source: PluginSourceSpec
install_dir: string
/** Hash of the installed artifact tree (directory content). */
content_hash: string
/** Hash of the installed manifest, for fast drift detection. */
manifest_hash: string
installed_at: string
/** Resolved git commit the artifact was fetched from. */
resolved_ref?: string
update?: PluginUpdateState
}
export interface PluginUpdateState {
checked_at: string
latest_version?: SemverVersion
latest_ref?: string
available: boolean
error?: string
}
// =============================================================================
// Runtime module and activation context
// =============================================================================
export interface PluginModule {
activate(ctx: PluginActivationContext): void | Promise<void>
deactivate?(ctx: PluginDeactivationContext): void | Promise<void>
}
export interface PluginActivationContext {
plugin: ActivePlugin
/** Current config slice for this plugin (already validated). */
config: JsonObject
env: NodeJS.ProcessEnv
paths: PluginPaths
log: PluginLogger
permissions: PermissionContext
capabilities: CapabilityRegistry
commands: CommandRegistry
configRegistry: ConfigRegistry
sources: SourceRegistry
sinks: SinkRegistry
query: QueryRegistry
/**
* Verb registry (kernel-owned). A plugin registers a query-shaped
* operation once and the kernel projects it into both a CLI command
* and an MCP tool (LLP 0034 §verbs). `@hypaware/context-graph`
* registers `graph neighbors` here so it yields its `graph_neighbors`
* tool for free.
*/
verbs: VerbRegistry
/**
* Intrinsic storage handle for the kernel-managed query cache.
* Plugins reach the local Iceberg-backed cache through this: they
* never construct paths or open files themselves. The kernel owns
* `cacheDir`; plugins ask the storage for a `tablePath` and call
* `appendRows` / `readRows`.
*/
storage: QueryStorageService
skills: SkillRegistry
agents: AgentRegistry
initPresets: InitPresetRegistry
/**
* Backfill provider registry (kernel-owned). Plugins register
* `BackfillContribution`s during activation; `hyp backfill` selects
* providers from this registry. The shape is intentionally narrow:
* provider authors keep dataset-specific behavior in their `run`
* implementation rather than expanding the kernel surface.
*/
backfills: BackfillRegistry
/**
* Dataset materializer registry (kernel-owned). Dataset/schema owners
* register a materializer per `kind` they can convert into canonical
* rows for a target dataset. The `hyp backfill` runner asks this
* registry to materialize each `BackfillItem` yielded by a provider
* before appending to the cache.
*/
backfillMaterializers: BackfillMaterializerRegistry
/** Kernel-owned client lifecycle registry. */
clients: ClientRegistry
/**
* Narrow facade over the kernel config apply engine (LLP 0023). Only
* present when the host process runs an apply engine (the daemon);
* absent in plain CLI boots, so transport plugins must treat it as
* optional and skip their pull loops when it is missing. The facade
* is the only channel a plugin has into config application: the
* kernel owns validation, install, persistence, restart, probation,
* and rollback.
*/
configControl?: ConfigControlFacade
requireCapability<T = unknown>(name: CapabilityName, range?: SemverRange): T
provideCapability<T = unknown>(name: CapabilityName, version: SemverVersion, value: T): void
}
/**
* Plugin-facing surface of the kernel config apply engine. Handed to
* transport plugins (e.g. `@hypaware/central`) so they can deliver a
* downloaded config document and report poll liveness. Deliberately
* narrow: plugins never see probation state, slot paths, or rollback
* bookkeeping.
*/
export interface ConfigControlFacade {
/**
* Deliver a downloaded config document (parsed JSON) plus the ETag it
* was served under. The kernel validates, installs pinned plugins,
* persists, swaps, and requests a staged restart. Resolves before the
* restart happens; callers should treat `{ ok: true }` as "apply
* committed, restart pending".
*/
stage(document: unknown, etag: string): Promise<ConfigStageResult>
/**
* Report a successful authenticated config poll (200 or 304). Clears
* the post-apply probation window when one is active; a no-op
* otherwise.
*/
confirmPoll(): void
/** ETag of the *running* config, for `If-None-Match`. Undefined when the operative config was never applied from the server (e.g. seed). */
runningEtag(): string | undefined
}
export type ConfigStageResult =
| { ok: true, action: 'applied' | 'noop_same_etag' | 'skipped_bad_etag' }
| { ok: false, errorKind: ConfigApplyErrorKind, message: string }
export type ConfigApplyErrorKind =
| 'config_invalid'
| 'plugin_install_failed'
| 'artifact_hash_mismatch'
| 'bundled_version_mismatch'
| 'document_too_large'
| 'apply_engine_not_ready'
| 'restart_pending'
| 'apply_io_error'
export interface PluginDeactivationContext {
plugin: ActivePlugin
log: PluginLogger
}
export interface ActivePlugin {
name: PluginName
version: SemverVersion
manifest: PluginManifest
rootDir: string
}
export interface PluginPaths {
rootDir: string
stateDir: string
cacheDir: string
tempDir: string
}
export interface PluginLogger {
debug(message: string, fields?: Record<string, unknown>): void
info(message: string, fields?: Record<string, unknown>): void
warn(message: string, fields?: Record<string, unknown>): void
error(message: string, fields?: Record<string, unknown>): void
}
export interface PermissionContext {
has(permission: PluginPermission): boolean
require(permission: PluginPermission): void
request(permission: PluginPermission, reason: string): Promise<boolean>
}
export interface CapabilityRegistry {
provide<T = unknown>(provider: PluginName | 'core', name: CapabilityName, version: SemverVersion, value: T): void
require<T = unknown>(requester: PluginName, name: CapabilityName, range?: SemverRange): T
has(name: CapabilityName, range?: SemverRange): boolean
list(): CapabilityRegistration[]
fromProvider<T = unknown>(provider: PluginName | 'core', name: CapabilityName, range?: SemverRange): T | undefined
}
export interface CapabilityRegistration {
name: CapabilityName
version: SemverVersion
provider: PluginName | 'core'
}
// =============================================================================
// Config
// =============================================================================
/**
* Breaking v2 config shape. There is no `mode` field and no
* architectural role label: a host is described entirely by its
* plugins, sinks, and cache retention settings.
*/
export interface HypAwareV2Config {
version: 2
plugins?: PluginConfigInstance[]
sinks?: Record<string, SinkConfigInstance>
query?: QueryConfig
/**
* Explicit capability-provider pins. When two installed plugins
* provide the same capability at a compatible version, the kernel
* refuses to choose and requires the user to disambiguate by mapping
* the capability name to the chosen provider plugin name. The kernel
* walks this map during cross-plugin validation; any capability not
* listed must be unambiguously provided.
*/
disambiguate?: Record<CapabilityName, PluginName>
/**
* Kernel self-update switch. Default true when absent; the daemon
* applies new HypAware releases automatically. Central wins over
* local when both layers set it.
*/
auto_update?: boolean
}
/** Legacy alias retained only while the project rename completes. */
export type CollectivusV2Config = HypAwareV2Config
export interface PluginConfigInstance {
name: PluginName
enabled?: boolean
config?: JsonObject
/**
* Pinned plugin version. Set by centrally-served configs (LLP 0023):
* the apply engine refuses a config whose pins it cannot satisfy.
* For bundled first-party plugins the pin is checked strictly against
* the bundled version; for fetched plugins it selects the artifact.
*/
version?: SemverVersion
/**
* Pinned artifact content hash for fetched plugins. The apply engine
* verifies the fetched artifact against this before committing the
* install; a mismatch is an apply failure. Ignored (not checked) for
* plugins bundled with the running kernel.
*/
artifact_hash?: string
/**
* Optional explicit install source (raw source string accepted by the
* plugin installer). Defaults to the plugin name, which the resolver
* maps to its canonical git source.
*/
source?: string
}
/**
* A user-named sink instance. The key in `HypAwareV2Config.sinks` is
* the instance name (shown in status and logs). Sinks come in two
* shapes:
*
* - **Blob sinks** compose a `writer` and a `destination` (blob store).
* The writer plugin must require `hypaware.blob-store` and provide
* either `hypaware.encoder` (encoder writer) or
* `hypaware.table-format` (table-format writer); the destination
* plugin provides `hypaware.blob-store`. The kernel rejects
* incompatible writer/destination pairs at config-load time.
* - **Request sinks** are one-piece: a single `plugin` whose wire
* format is intrinsic (`@hypaware/central`, future
* `@hypaware/webhook`).
*
* In both shapes `config` carries the chosen plugin's settings plus a
* `schedule` cron string. Queryability for a blob sink is derived from
* the resolved writer/destination pair (e.g. parquet + local-fs is
* queryable; jsonl + local-fs is not).
*/
export type SinkConfigInstance = BlobSinkConfigInstance | RequestSinkConfigInstance
export interface BlobSinkConfigInstance {
/**
* Writer plugin. Must require `hypaware.blob-store` and provide
* either `hypaware.encoder` (per-batch byte encoder) or
* `hypaware.table-format` (directory layout + manifests on top of an
* encoder).
*/
writer: PluginName
/** Destination plugin: provides `hypaware.blob-store`. */
destination: PluginName
config?: SinkInstanceConfig
}
export interface RequestSinkConfigInstance {
/** Single plugin whose wire format is intrinsic to the destination. */
plugin: PluginName
config?: SinkInstanceConfig
}
export type SinkInstanceConfig = JsonObject & {
/** Export cadence: standard 5-field cron expression (e.g. "0 * * * *"). */
schedule?: string
/**
* For table-format writers (writer provides `hypaware.table-format`),
* the inner encoder plugin used to encode data files. Defaults to
* `@hypaware/format-parquet` when omitted. Ignored by encoder
* writers (their format is intrinsic).
*/
encoder?: PluginName
}
export interface QueryConfig {
cache?: QueryCacheConfig
/**
* Named remote MCP targets for `hyp <verb> --remote <name>` (LLP 0033
* §targets). Lives inside the **local-only** `query{}` block, so the
* central layer can never inject a remote target (LLP 0031). The URL is
* non-secret and committable; the query-scoped token is never config.
*/
remotes?: Record<string, QueryRemoteTarget>
/** Default target used by `--remote` with no argument. Must name a key in `remotes`. */
default_remote?: string
}
export interface QueryRemoteTarget {
/** The server's MCP endpoint, e.g. `https://hyp.internal/mcp`. */
url: string
}
export interface QueryCacheConfig {
/** Override the cache root (default: `~/.hyp/hypaware/`). Layout inside is fixed. */
dir?: string
retention?: QueryCacheRetentionConfig
maintenance?: QueryCacheMaintenanceConfig
}
export interface QueryCacheMaintenanceConfig {
enabled?: boolean
interval_minutes?: number
target_file_bytes?: number
min_snapshots_to_keep?: number
max_snapshot_age_hours?: number
compact_file_count?: number
compact_avg_file_bytes?: number
compact_batch_bytes?: number
max_tick_ms?: number
}
export interface QueryCacheRetentionConfig {
default_days: number
/** Per-dataset retention overrides. */
datasets?: Record<string, number>
}
export interface ConfigRegistry {
registerSection(registration: ConfigSectionRegistration): void
validatePluginConfig(pluginName: PluginName, config: unknown): ValidationResult
}
export interface ConfigSectionRegistration {
plugin: PluginName
section: string
validate(value: unknown, ctx: ConfigValidationContext): ValidationResult
defaults?(): JsonObject
}
export interface ConfigValidationContext {
pluginName: PluginName
pointer: string
}
export type ValidationResult =
| { ok: true }
| { ok: false, errors: ValidationError[] }
export interface ValidationError {
pointer: string
message: string
}
// =============================================================================
// CLI commands
// =============================================================================
export interface CommandRegistry {
/**
* Claim a command name. `command` is an input, not the registry's
* storage: the registry keeps a shallow copy and fills the semantic
* defaults (`category`, `audience`, `bootProfile`) on that copy, so a
* frozen module-level registration is accepted, a registration this
* call rejects comes back exactly as it was passed, and editing the
* object afterwards does not reach what `get`/`list` return. Function
* members (`run`) are shared with the copy, not cloned.
*
* The copy is own enumerable properties only, and the shape checks run
* on it, so a registration whose members live on a prototype (a class
* instance) is rejected here rather than stored half-formed.
*
* This declaration cannot express that rule: TypeScript has no notion of
* property ownership or enumerability, so a class whose `run()` sits on
* its prototype satisfies `CommandRegistration` under `--strict` and then
* throws at this call. Register a plain object, or assign the members onto
* the instance itself.
*/
register(command: CommandRegistration): void
/**
* Describe a command *group* (`graph`, `query`) so its `--help` can
* carry a header and a paragraph, not just a subcommand table. Core
* groups get this from the bare command `makeGroupCommand` builds; a
* plugin namespace has no bare command, so it says so here instead.
*
* Metadata only: a registered group never appears in `list()`, so it
* cannot shadow a command or show up as its own subcommand.
*/
registerGroup(group: CommandGroupRegistration): void