-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.js
More file actions
203 lines (196 loc) · 8.18 KB
/
Copy pathconfig.js
File metadata and controls
203 lines (196 loc) · 8.18 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
// @ts-check
/**
* Config validation for the `@hypaware/claude` plugin's own `config`
* block. It validates the optional `backfill` sub-object that drives
* backfill-on-join and the daemon transcript sweep
* (`{ on_join, window_days, sweep_cron }`), the optional `attach`
* sub-object that drives attach-on-join (`{ on_join }`), and the
* optional `telemetry` sub-object that places the Claude telemetry
* listener (`{ listen_host, listen_port }`). Every
* other key (e.g. `proxy`) passes through untouched so existing configs
* keep working; there is no top-level `backfill`/`attach` section and
* nothing new for core to validate.
*
* Pure and dependency-free: it returns a `ValidationResult` so it plugs
* straight into `ctx.configRegistry.registerSection` and is callable from
* tests without spinning up observability.
*
* @import { ValidationError, ValidationResult } from '../../../../hypaware-plugin-kernel-types.js'
*/
import { isCronExpression } from '../../../../src/core/config/validate.js'
/** Manifest `config_sections[].section` name this validator backs. */
export const CLAUDE_CONFIG_SECTION = 'claude'
/**
* Validate the `@hypaware/claude` plugin config slice. Only the optional
* `backfill` and `attach` policy blocks are checked; unknown sibling keys
* are ignored so the validator stays additive over the existing config
* surface.
*
* @ref LLP 0037#per-plugin-config-kernel-generic-reconciler [implements]:
* backfill policy ({ on_join, window_days, sweep_cron }) lives in and is validated
* by the source plugin's own config section; the kernel reconciler adds
* no top-level schema.
*
* @param {unknown} value
* @returns {ValidationResult}
*/
export function validateClaudeConfig(value) {
if (value === undefined || value === null) return { ok: true }
if (typeof value !== 'object' || Array.isArray(value)) {
return { ok: false, errors: [{ pointer: '', message: 'claude config must be an object' }] }
}
const raw = /** @type {Record<string, unknown>} */ (value)
const errors = [
...validateBackfillSection(raw.backfill, '/backfill'),
...validateAttachSection(raw.attach, '/attach'),
...validateTelemetrySection(raw.telemetry, '/telemetry'),
]
if (errors.length > 0) return { ok: false, errors }
return { ok: true }
}
/**
* Validate the optional `backfill` policy block shared by every
* backfill-capable source plugin: `on_join` (whether to import on join,
* boolean), `window_days` (how far back, positive integer), and `sweep_cron`
* (the scheduled transcript cadence, a five-field cron expression). All are
* optional; unknown keys are rejected so a typo (`window_day`) surfaces
* instead of being silently ignored. Pure: the caller chooses where the
* returned pointers mount.
*
* @ref LLP 0358#scheduled-sweep [implements]: Claude owns and validates its
* scheduled backfill cadence using the kernel's existing cron grammar
*
* @param {unknown} value
* @param {string} pointer JSON-pointer prefix for the `backfill` object
* @returns {ValidationError[]}
*/
export function validateBackfillSection(value, pointer) {
/** @type {ValidationError[]} */
const errors = []
if (value === undefined) return errors
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
errors.push({ pointer, message: 'backfill must be an object' })
return errors
}
const raw = /** @type {Record<string, unknown>} */ (value)
if (raw.on_join !== undefined && typeof raw.on_join !== 'boolean') {
errors.push({ pointer: `${pointer}/on_join`, message: 'backfill.on_join must be a boolean' })
}
if (raw.window_days !== undefined) {
const days = raw.window_days
if (typeof days !== 'number' || !Number.isInteger(days) || days <= 0) {
errors.push({
pointer: `${pointer}/window_days`,
message: 'backfill.window_days must be a positive integer',
})
}
}
if (raw.sweep_cron !== undefined) {
const cron = raw.sweep_cron
if (typeof cron !== 'string' || !isCronExpression(cron)) {
errors.push({
pointer: `${pointer}/sweep_cron`,
message: 'backfill.sweep_cron must be a valid 5-field cron expression',
})
}
}
for (const key of Object.keys(raw)) {
if (key !== 'on_join' && key !== 'window_days' && key !== 'sweep_cron') {
errors.push({ pointer: `${pointer}/${key}`, message: `unknown backfill key '${key}'` })
}
}
return errors
}
/**
* Validate the optional `telemetry` block: where the Claude telemetry
* listener binds (`listen_host`, string; `listen_port`, integer in
* `0..65535` where `0` asks for a dynamic port) and how large the raw
* body spool may grow (`spool_max_bytes`, positive integer, default
* 512 MB). All optional; unknown keys are rejected so a typo
* (`listen_ports`) surfaces instead of silently leaving the listener on
* its default port while attach writes the address the operator meant.
*
* @ref LLP 0257#registration [implements]: the listener's port is config with a
* default, and `0` requests a dynamic port
* @ref LLP 0253#byte-cap [implements]: the spool cap is a configured byte value
*
* @param {unknown} value
* @param {string} pointer JSON-pointer prefix for the `telemetry` object
* @returns {ValidationError[]}
*/
export function validateTelemetrySection(value, pointer) {
/** @type {ValidationError[]} */
const errors = []
if (value === undefined) return errors
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
errors.push({ pointer, message: 'telemetry must be an object' })
return errors
}
const raw = /** @type {Record<string, unknown>} */ (value)
if (raw.listen_host !== undefined && (typeof raw.listen_host !== 'string' || raw.listen_host.length === 0)) {
errors.push({
pointer: `${pointer}/listen_host`,
message: 'telemetry.listen_host must be a non-empty string',
})
}
if (raw.listen_port !== undefined) {
const port = raw.listen_port
if (typeof port !== 'number' || !Number.isInteger(port) || port < 0 || port > 65535) {
errors.push({
pointer: `${pointer}/listen_port`,
message: 'telemetry.listen_port must be an integer between 0 and 65535',
})
}
}
if (raw.spool_max_bytes !== undefined) {
const cap = raw.spool_max_bytes
if (typeof cap !== 'number' || !Number.isInteger(cap) || cap < 1) {
errors.push({
pointer: `${pointer}/spool_max_bytes`,
message: 'telemetry.spool_max_bytes must be a positive integer',
})
}
}
for (const key of Object.keys(raw)) {
if (key !== 'listen_host' && key !== 'listen_port' && key !== 'spool_max_bytes') {
errors.push({ pointer: `${pointer}/${key}`, message: `unknown telemetry key '${key}'` })
}
}
return errors
}
/**
* Validate the optional `attach` policy block on a client-adapter plugin's
* config: `on_join` (whether the daemon auto-attaches this client when a
* joined host confirms a central config that enables it, boolean,
* default true). Optional; unknown keys are rejected so a typo
* (`on_joins`) surfaces instead of being silently ignored. Pure: the
* caller chooses where the returned pointers mount.
*
* @ref LLP 0045#part-4-per-plugin-attach-config--status-surface [implements]:
* attach.on_join rides the client adapter's own config block, validated
* by this plugin's config-section validator beside validateBackfillSection;
* no top-level/core schema.
*
* @param {unknown} value
* @param {string} pointer JSON-pointer prefix for the `attach` object
* @returns {ValidationError[]}
*/
export function validateAttachSection(value, pointer) {
/** @type {ValidationError[]} */
const errors = []
if (value === undefined) return errors
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
errors.push({ pointer, message: 'attach must be an object' })
return errors
}
const raw = /** @type {Record<string, unknown>} */ (value)
if (raw.on_join !== undefined && typeof raw.on_join !== 'boolean') {
errors.push({ pointer: `${pointer}/on_join`, message: 'attach.on_join must be a boolean' })
}
for (const key of Object.keys(raw)) {
if (key !== 'on_join') {
errors.push({ pointer: `${pointer}/${key}`, message: `unknown attach key '${key}'` })
}
}
return errors
}