night-orch 0.3.2 → 0.3.3
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/LICENSE +21 -0
- package/README.md +47 -108
- package/dist/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/cli/commands/monitoring.js +7 -3
- package/dist/cli/commands/monitoring.js.map +1 -1
- package/dist/cli/commands/settings.d.ts.map +1 -1
- package/dist/cli/commands/settings.js +40 -4
- package/dist/cli/commands/settings.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/tui/app.d.ts.map +1 -1
- package/dist/cli/tui/app.js +52 -3
- package/dist/cli/tui/app.js.map +1 -1
- package/dist/cli/tui/settings-view.d.ts.map +1 -1
- package/dist/cli/tui/settings-view.js +22 -5
- package/dist/cli/tui/settings-view.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +141 -13
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +266 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +38 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/mcp/tools/index.js +23 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/settings/registry.d.ts +23 -7
- package/dist/settings/registry.d.ts.map +1 -1
- package/dist/settings/registry.js +802 -172
- package/dist/settings/registry.js.map +1 -1
- package/dist/settings/runtime.d.ts +5 -1
- package/dist/settings/runtime.d.ts.map +1 -1
- package/dist/settings/runtime.js +46 -9
- package/dist/settings/runtime.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +244 -9
- package/dist/web/server.js.map +1 -1
- package/dist/web/shell-session.d.ts +74 -0
- package/dist/web/shell-session.d.ts.map +1 -0
- package/dist/web/shell-session.js +278 -0
- package/dist/web/shell-session.js.map +1 -0
- package/package.json +8 -2
- package/web/dist/assets/index-BIrXUwFe.css +1 -0
- package/web/dist/assets/index-BzM2M-8S.js +26 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-k6kgdnzy.js +0 -9
- package/web/dist/assets/index-xm9qPlYB.css +0 -1
|
@@ -1,207 +1,671 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AppMentionSchema, CostPricingModelSchema, NotificationChannelSchema, WorkerProfileSchema, WorkflowSchema, } from '../config/schema.js';
|
|
3
|
+
const SETTING_DEFINITIONS = buildSettingDefinitions();
|
|
4
|
+
const SETTING_KEYS = Object.keys(SETTING_DEFINITIONS);
|
|
5
|
+
export function listSettingDefinitions() {
|
|
6
|
+
return SETTING_KEYS.map((key) => SETTING_DEFINITIONS[key]);
|
|
7
|
+
}
|
|
8
|
+
export function getSettingDefinition(key) {
|
|
9
|
+
return SETTING_DEFINITIONS[key] ?? null;
|
|
10
|
+
}
|
|
11
|
+
export function sanitizeSettingValueForDisplay(definition, value) {
|
|
12
|
+
if (value === null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const sanitize = definition.sanitizeForDisplay;
|
|
16
|
+
return sanitize(value);
|
|
17
|
+
}
|
|
18
|
+
export function resolveSettingYamlValue(definition, rawConfig, baseConfig) {
|
|
19
|
+
if (!hasValueAtPath(rawConfig, definition.yamlPath)) {
|
|
20
|
+
return {
|
|
21
|
+
hasYamlValue: false,
|
|
22
|
+
yamlValue: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
hasYamlValue: true,
|
|
27
|
+
yamlValue: definition.read(baseConfig),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const GithubAppMentionsOverrideSchema = z.record(AppMentionSchema);
|
|
31
|
+
const NotificationsChannelsOverrideSchema = z.array(NotificationChannelSchema);
|
|
32
|
+
const CostPricingModelsOverrideSchema = z.record(CostPricingModelSchema);
|
|
33
|
+
const WorkerProfilesOverrideSchema = z.record(WorkerProfileSchema);
|
|
34
|
+
const WorkflowsOverrideSchema = z.record(WorkflowSchema);
|
|
35
|
+
function buildSettingDefinitions() {
|
|
36
|
+
const definitions = [
|
|
37
|
+
stringSetting({
|
|
38
|
+
key: 'github.tokenEnv',
|
|
39
|
+
label: 'GitHub Token Env Var',
|
|
40
|
+
description: 'Environment variable name used for GitHub authentication.',
|
|
41
|
+
details: 'Set the env var name that stores the GitHub token. This must be a variable name, not a literal token value.',
|
|
42
|
+
defaultValue: null,
|
|
43
|
+
yamlPath: ['github', 'tokenEnv'],
|
|
44
|
+
minLength: 1,
|
|
45
|
+
validate: (value) => {
|
|
46
|
+
if (value.startsWith('ghp_') || value.startsWith('ghs_') || value.startsWith('github_pat_')) {
|
|
47
|
+
return 'github.tokenEnv should be an environment variable name, not a literal token';
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
19
50
|
},
|
|
20
51
|
}),
|
|
21
|
-
|
|
52
|
+
stringSetting({
|
|
53
|
+
key: 'github.apiBaseUrl',
|
|
54
|
+
label: 'GitHub API Base URL',
|
|
55
|
+
description: 'Base URL for GitHub API requests.',
|
|
56
|
+
details: 'Override the API base URL used for GitHub repos. Useful for GitHub Enterprise deployments.',
|
|
57
|
+
defaultValue: 'https://api.github.com',
|
|
58
|
+
yamlPath: ['github', 'apiBaseUrl'],
|
|
59
|
+
minLength: 1,
|
|
60
|
+
url: true,
|
|
61
|
+
}),
|
|
62
|
+
numberSetting({
|
|
22
63
|
key: 'github.pollIntervalSeconds',
|
|
64
|
+
label: 'Poll Interval (seconds)',
|
|
65
|
+
description: 'Delay between automatic poll cycles.',
|
|
66
|
+
details: 'Controls how often night-orch checks configured repos for work. Lower values react faster but increase API traffic.',
|
|
67
|
+
defaultValue: 300,
|
|
68
|
+
yamlPath: ['github', 'pollIntervalSeconds'],
|
|
23
69
|
integer: true,
|
|
24
70
|
min: 5,
|
|
25
71
|
max: 3600,
|
|
72
|
+
step: 5,
|
|
26
73
|
}),
|
|
27
|
-
|
|
28
|
-
key: 'github.
|
|
74
|
+
jsonSetting({
|
|
75
|
+
key: 'github.appMentions',
|
|
76
|
+
label: 'GitHub App Mentions',
|
|
77
|
+
description: 'Mention template map used for app-trigger comments.',
|
|
78
|
+
details: 'Record keyed by mention alias with enabled flag and comment template payloads.',
|
|
79
|
+
defaultValue: {},
|
|
80
|
+
yamlPath: ['github', 'appMentions'],
|
|
81
|
+
normalize: (value) => validateJsonSettingShape(value, GithubAppMentionsOverrideSchema, 'github.appMentions'),
|
|
82
|
+
}),
|
|
83
|
+
stringSetting({
|
|
84
|
+
key: 'storage.dbPath',
|
|
85
|
+
label: 'State DB Path',
|
|
86
|
+
description: 'SQLite database file path.',
|
|
87
|
+
details: 'Location of the runtime state database file. Read-only at runtime because DB opens before overrides are loaded.',
|
|
88
|
+
defaultValue: '~/.config/night-orch/state.db',
|
|
89
|
+
yamlPath: ['storage', 'dbPath'],
|
|
90
|
+
minLength: 1,
|
|
91
|
+
mutable: false,
|
|
92
|
+
}),
|
|
93
|
+
stringSetting({
|
|
94
|
+
key: 'storage.worktreeRoot',
|
|
95
|
+
label: 'Worktree Root',
|
|
96
|
+
description: 'Root directory used for issue worktrees.',
|
|
97
|
+
details: 'Base path where dedicated issue worktrees are created.',
|
|
98
|
+
defaultValue: '~/code/.night-orch/worktrees',
|
|
99
|
+
yamlPath: ['storage', 'worktreeRoot'],
|
|
100
|
+
minLength: 1,
|
|
101
|
+
}),
|
|
102
|
+
stringSetting({
|
|
103
|
+
key: 'storage.logsRoot',
|
|
104
|
+
label: 'Logs Root',
|
|
105
|
+
description: 'Root directory used for run/session logs.',
|
|
106
|
+
details: 'Base path where runtime log files are written.',
|
|
107
|
+
defaultValue: '~/code/.night-orch/logs',
|
|
108
|
+
yamlPath: ['storage', 'logsRoot'],
|
|
109
|
+
minLength: 1,
|
|
110
|
+
}),
|
|
111
|
+
booleanSetting({
|
|
112
|
+
key: 'storage.autoCleanup.enabled',
|
|
113
|
+
label: 'Auto Cleanup Enabled',
|
|
114
|
+
description: 'Enable periodic cleanup jobs.',
|
|
115
|
+
details: 'When enabled, stale worktrees and old logs are cleaned up automatically on schedule.',
|
|
116
|
+
defaultValue: true,
|
|
117
|
+
yamlPath: ['storage', 'autoCleanup', 'enabled'],
|
|
118
|
+
}),
|
|
119
|
+
numberSetting({
|
|
120
|
+
key: 'storage.autoCleanup.intervalMinutes',
|
|
121
|
+
label: 'Auto Cleanup Interval (minutes)',
|
|
122
|
+
description: 'How often automatic cleanup runs.',
|
|
123
|
+
details: 'Interval between automatic cleanup runs.',
|
|
124
|
+
defaultValue: 60,
|
|
125
|
+
yamlPath: ['storage', 'autoCleanup', 'intervalMinutes'],
|
|
29
126
|
integer: true,
|
|
30
|
-
min:
|
|
31
|
-
|
|
127
|
+
min: 1,
|
|
128
|
+
step: 5,
|
|
32
129
|
}),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
min: 1,
|
|
44
|
-
max: 10000,
|
|
45
|
-
step: 1,
|
|
46
|
-
read: (config) => config.security.maxDailyCostUsd,
|
|
47
|
-
apply: (config, value) => ({
|
|
48
|
-
...config,
|
|
49
|
-
security: {
|
|
50
|
-
...config.security,
|
|
51
|
-
maxDailyCostUsd: value,
|
|
52
|
-
},
|
|
130
|
+
numberSetting({
|
|
131
|
+
key: 'storage.retention.worktreeAgeDays',
|
|
132
|
+
label: 'Worktree Retention (days)',
|
|
133
|
+
description: 'Retention window for stale worktrees.',
|
|
134
|
+
details: 'Completed/error worktrees older than this threshold may be removed during cleanup.',
|
|
135
|
+
defaultValue: 7,
|
|
136
|
+
yamlPath: ['storage', 'retention', 'worktreeAgeDays'],
|
|
137
|
+
integer: true,
|
|
138
|
+
min: 1,
|
|
139
|
+
step: 1,
|
|
53
140
|
}),
|
|
54
|
-
|
|
55
|
-
key: '
|
|
56
|
-
|
|
141
|
+
numberSetting({
|
|
142
|
+
key: 'storage.retention.detailDays',
|
|
143
|
+
label: 'Detail Retention (days)',
|
|
144
|
+
description: 'Retention window for detailed run data.',
|
|
145
|
+
details: 'Detailed run artifacts are retained for this many days before archival/cleanup.',
|
|
146
|
+
defaultValue: 30,
|
|
147
|
+
yamlPath: ['storage', 'retention', 'detailDays'],
|
|
148
|
+
integer: true,
|
|
57
149
|
min: 1,
|
|
58
|
-
|
|
150
|
+
step: 1,
|
|
59
151
|
}),
|
|
60
|
-
|
|
61
|
-
key: '
|
|
62
|
-
|
|
152
|
+
numberSetting({
|
|
153
|
+
key: 'storage.retention.archiveDays',
|
|
154
|
+
label: 'Archive Retention (days)',
|
|
155
|
+
description: 'Retention window for archived run data.',
|
|
156
|
+
details: 'Archived records older than this threshold can be removed.',
|
|
157
|
+
defaultValue: 90,
|
|
158
|
+
yamlPath: ['storage', 'retention', 'archiveDays'],
|
|
159
|
+
integer: true,
|
|
63
160
|
min: 1,
|
|
64
|
-
|
|
161
|
+
step: 1,
|
|
65
162
|
}),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
defaultValue: 10,
|
|
75
|
-
yamlPath: ['security', 'maxCostPerRunUsd'],
|
|
76
|
-
min: 0.1,
|
|
77
|
-
max: 1000,
|
|
78
|
-
step: 0.5,
|
|
79
|
-
read: (config) => config.security.maxCostPerRunUsd,
|
|
80
|
-
apply: (config, value) => ({
|
|
81
|
-
...config,
|
|
82
|
-
security: {
|
|
83
|
-
...config.security,
|
|
84
|
-
maxCostPerRunUsd: value,
|
|
85
|
-
},
|
|
163
|
+
jsonSetting({
|
|
164
|
+
key: 'notifications.channels',
|
|
165
|
+
label: 'Notification Channels',
|
|
166
|
+
description: 'Configured notification channel list.',
|
|
167
|
+
details: 'Array of notification channel definitions (console/webhook/discord/smtp).',
|
|
168
|
+
defaultValue: [{ type: 'console' }],
|
|
169
|
+
yamlPath: ['notifications', 'channels'],
|
|
170
|
+
normalize: (value) => validateJsonSettingShape(value, NotificationsChannelsOverrideSchema, 'notifications.channels'),
|
|
86
171
|
}),
|
|
87
|
-
|
|
88
|
-
key: '
|
|
172
|
+
booleanSetting({
|
|
173
|
+
key: 'notifications.events.onRunStarted',
|
|
174
|
+
label: 'Notify: Run Started',
|
|
175
|
+
description: 'Send notifications when runs start.',
|
|
176
|
+
details: 'Controls whether run-started notifications are dispatched.',
|
|
177
|
+
defaultValue: false,
|
|
178
|
+
yamlPath: ['notifications', 'events', 'onRunStarted'],
|
|
179
|
+
}),
|
|
180
|
+
booleanSetting({
|
|
181
|
+
key: 'notifications.events.onBlocked',
|
|
182
|
+
label: 'Notify: Blocked',
|
|
183
|
+
description: 'Send notifications when runs become blocked.',
|
|
184
|
+
details: 'Controls whether blocked-run notifications are dispatched.',
|
|
185
|
+
defaultValue: true,
|
|
186
|
+
yamlPath: ['notifications', 'events', 'onBlocked'],
|
|
187
|
+
}),
|
|
188
|
+
booleanSetting({
|
|
189
|
+
key: 'notifications.events.onPrReady',
|
|
190
|
+
label: 'Notify: PR Ready',
|
|
191
|
+
description: 'Send notifications when PRs are ready.',
|
|
192
|
+
details: 'Controls whether ready-for-review PR notifications are dispatched.',
|
|
193
|
+
defaultValue: true,
|
|
194
|
+
yamlPath: ['notifications', 'events', 'onPrReady'],
|
|
195
|
+
}),
|
|
196
|
+
booleanSetting({
|
|
197
|
+
key: 'notifications.events.onPrUpdated',
|
|
198
|
+
label: 'Notify: PR Updated',
|
|
199
|
+
description: 'Send notifications when PRs are updated.',
|
|
200
|
+
details: 'Controls whether PR update notifications are dispatched.',
|
|
201
|
+
defaultValue: true,
|
|
202
|
+
yamlPath: ['notifications', 'events', 'onPrUpdated'],
|
|
203
|
+
}),
|
|
204
|
+
booleanSetting({
|
|
205
|
+
key: 'notifications.events.onError',
|
|
206
|
+
label: 'Notify: Error',
|
|
207
|
+
description: 'Send notifications on orchestration errors.',
|
|
208
|
+
details: 'Controls whether error notifications are dispatched.',
|
|
209
|
+
defaultValue: true,
|
|
210
|
+
yamlPath: ['notifications', 'events', 'onError'],
|
|
211
|
+
}),
|
|
212
|
+
booleanSetting({
|
|
213
|
+
key: 'notifications.events.onRetryExhausted',
|
|
214
|
+
label: 'Notify: Retry Exhausted',
|
|
215
|
+
description: 'Send notifications when retries are exhausted.',
|
|
216
|
+
details: 'Controls whether retry-exhausted notifications are dispatched.',
|
|
217
|
+
defaultValue: true,
|
|
218
|
+
yamlPath: ['notifications', 'events', 'onRetryExhausted'],
|
|
219
|
+
}),
|
|
220
|
+
numberSetting({
|
|
221
|
+
key: 'loop.maxReviewIterations',
|
|
222
|
+
label: 'Max Review Iterations',
|
|
223
|
+
description: 'Maximum review correction loops per run.',
|
|
224
|
+
details: 'Limits how many review-fix-review cycles a run can execute before stopping.',
|
|
225
|
+
defaultValue: 4,
|
|
226
|
+
yamlPath: ['loop', 'maxReviewIterations'],
|
|
227
|
+
integer: true,
|
|
228
|
+
min: 1,
|
|
229
|
+
step: 1,
|
|
230
|
+
}),
|
|
231
|
+
numberSetting({
|
|
232
|
+
key: 'loop.maxTotalAgentPasses',
|
|
233
|
+
label: 'Max Total Agent Passes',
|
|
234
|
+
description: 'Hard cap on planner/coder/reviewer passes in one run.',
|
|
235
|
+
details: 'Caps total planner/coder/reviewer passes across the full run.',
|
|
236
|
+
defaultValue: 10,
|
|
237
|
+
yamlPath: ['loop', 'maxTotalAgentPasses'],
|
|
238
|
+
integer: true,
|
|
239
|
+
min: 1,
|
|
240
|
+
step: 1,
|
|
241
|
+
}),
|
|
242
|
+
booleanSetting({
|
|
243
|
+
key: 'loop.stopOnPlannerFailure',
|
|
244
|
+
label: 'Stop On Planner Failure',
|
|
245
|
+
description: 'Stop run when planner output fails validation.',
|
|
246
|
+
details: 'If enabled, planner failure ends the run early instead of proceeding.',
|
|
247
|
+
defaultValue: true,
|
|
248
|
+
yamlPath: ['loop', 'stopOnPlannerFailure'],
|
|
249
|
+
}),
|
|
250
|
+
booleanSetting({
|
|
251
|
+
key: 'loop.requireVerificationPass',
|
|
252
|
+
label: 'Require Verification Pass',
|
|
253
|
+
description: 'Require verify commands to pass before completion.',
|
|
254
|
+
details: 'If enabled, failed verification blocks completion.',
|
|
255
|
+
defaultValue: true,
|
|
256
|
+
yamlPath: ['loop', 'requireVerificationPass'],
|
|
257
|
+
}),
|
|
258
|
+
stringSetting({
|
|
259
|
+
key: 'loop.reviewApprovalKeyword',
|
|
260
|
+
label: 'Review Approval Keyword',
|
|
261
|
+
description: 'Reviewer keyword interpreted as approval.',
|
|
262
|
+
details: 'Expected keyword emitted by reviewer agent for approval.',
|
|
263
|
+
defaultValue: 'APPROVED',
|
|
264
|
+
yamlPath: ['loop', 'reviewApprovalKeyword'],
|
|
265
|
+
minLength: 1,
|
|
266
|
+
}),
|
|
267
|
+
stringSetting({
|
|
268
|
+
key: 'loop.reviewNeedsChangesKeyword',
|
|
269
|
+
label: 'Review Needs-Changes Keyword',
|
|
270
|
+
description: 'Reviewer keyword interpreted as changes required.',
|
|
271
|
+
details: 'Expected keyword emitted by reviewer agent for change requests.',
|
|
272
|
+
defaultValue: 'CHANGES_REQUIRED',
|
|
273
|
+
yamlPath: ['loop', 'reviewNeedsChangesKeyword'],
|
|
274
|
+
minLength: 1,
|
|
275
|
+
}),
|
|
276
|
+
booleanSetting({
|
|
277
|
+
key: 'loop.blockOnAmbiguousReview',
|
|
278
|
+
label: 'Block On Ambiguous Review',
|
|
279
|
+
description: 'Block run if review output is ambiguous.',
|
|
280
|
+
details: 'If enabled, unparseable reviewer output results in blocked status.',
|
|
281
|
+
defaultValue: true,
|
|
282
|
+
yamlPath: ['loop', 'blockOnAmbiguousReview'],
|
|
283
|
+
}),
|
|
284
|
+
numberSetting({
|
|
285
|
+
key: 'loop.maxAutoRetries',
|
|
286
|
+
label: 'Max Auto Retries',
|
|
287
|
+
description: 'Automatic retry attempts for infrastructure failures.',
|
|
288
|
+
details: 'Maximum number of automatic retries after transient failures.',
|
|
289
|
+
defaultValue: 3,
|
|
290
|
+
yamlPath: ['loop', 'maxAutoRetries'],
|
|
291
|
+
integer: true,
|
|
292
|
+
min: 0,
|
|
293
|
+
step: 1,
|
|
294
|
+
}),
|
|
295
|
+
booleanSetting({
|
|
296
|
+
key: 'loop.decompose',
|
|
297
|
+
label: 'Enable Decomposition',
|
|
298
|
+
description: 'Allow issue decomposition into sub-tasks.',
|
|
299
|
+
details: 'If enabled, eligible issues can be split into dependent sub-tasks.',
|
|
300
|
+
defaultValue: false,
|
|
301
|
+
yamlPath: ['loop', 'decompose'],
|
|
302
|
+
}),
|
|
303
|
+
numberSetting({
|
|
304
|
+
key: 'loop.maxSubtasks',
|
|
305
|
+
label: 'Max Subtasks',
|
|
306
|
+
description: 'Maximum sub-tasks per decomposition.',
|
|
307
|
+
details: 'Upper bound for generated decomposition sub-tasks.',
|
|
308
|
+
defaultValue: 5,
|
|
309
|
+
yamlPath: ['loop', 'maxSubtasks'],
|
|
310
|
+
integer: true,
|
|
311
|
+
min: 1,
|
|
312
|
+
max: 10,
|
|
313
|
+
step: 1,
|
|
314
|
+
}),
|
|
315
|
+
numberSetting({
|
|
316
|
+
key: 'loop.maxConcurrentSubtasks',
|
|
317
|
+
label: 'Max Concurrent Subtasks',
|
|
318
|
+
description: 'Maximum parallel sub-task executions.',
|
|
319
|
+
details: 'Upper bound for concurrently running sub-task worktrees.',
|
|
320
|
+
defaultValue: 3,
|
|
321
|
+
yamlPath: ['loop', 'maxConcurrentSubtasks'],
|
|
322
|
+
integer: true,
|
|
323
|
+
min: 1,
|
|
324
|
+
max: 10,
|
|
325
|
+
step: 1,
|
|
326
|
+
}),
|
|
327
|
+
numberSetting({
|
|
328
|
+
key: 'security.maxChangedFiles',
|
|
329
|
+
label: 'Max Changed Files',
|
|
330
|
+
description: 'Diff guard threshold for changed files.',
|
|
331
|
+
details: 'Runs are blocked when changed-file count exceeds this threshold.',
|
|
332
|
+
defaultValue: 50,
|
|
333
|
+
yamlPath: ['security', 'maxChangedFiles'],
|
|
334
|
+
integer: true,
|
|
335
|
+
min: 1,
|
|
336
|
+
step: 1,
|
|
337
|
+
}),
|
|
338
|
+
numberSetting({
|
|
339
|
+
key: 'security.maxChangedLines',
|
|
340
|
+
label: 'Max Changed Lines',
|
|
341
|
+
description: 'Diff guard threshold for changed lines.',
|
|
342
|
+
details: 'Runs are blocked when changed-line count exceeds this threshold.',
|
|
343
|
+
defaultValue: 5000,
|
|
344
|
+
yamlPath: ['security', 'maxChangedLines'],
|
|
345
|
+
integer: true,
|
|
346
|
+
min: 1,
|
|
347
|
+
step: 50,
|
|
348
|
+
}),
|
|
349
|
+
numberSetting({
|
|
350
|
+
key: 'security.maxDailyCostUsd',
|
|
351
|
+
label: 'Daily Cost Budget (USD)',
|
|
352
|
+
description: 'Maximum allowed spend per UTC day before new runs are blocked.',
|
|
353
|
+
details: 'Sets the global daily spend cap for pay-per-use mode.',
|
|
354
|
+
defaultValue: 50,
|
|
355
|
+
yamlPath: ['security', 'maxDailyCostUsd'],
|
|
89
356
|
integer: false,
|
|
90
|
-
min: 0.
|
|
91
|
-
|
|
357
|
+
min: 0.01,
|
|
358
|
+
step: 1,
|
|
92
359
|
}),
|
|
93
|
-
|
|
360
|
+
numberSetting({
|
|
94
361
|
key: 'security.maxCostPerRunUsd',
|
|
362
|
+
label: 'Per-Run Cost Budget (USD)',
|
|
363
|
+
description: 'Maximum allowed spend for a single run before it is blocked.',
|
|
364
|
+
details: 'Sets the per-issue run cost ceiling for pay-per-use mode.',
|
|
365
|
+
defaultValue: 10,
|
|
366
|
+
yamlPath: ['security', 'maxCostPerRunUsd'],
|
|
95
367
|
integer: false,
|
|
96
|
-
min: 0.
|
|
97
|
-
|
|
368
|
+
min: 0.01,
|
|
369
|
+
step: 0.5,
|
|
98
370
|
}),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
defaultValue: 4,
|
|
108
|
-
yamlPath: ['loop', 'maxReviewIterations'],
|
|
109
|
-
min: 1,
|
|
110
|
-
max: 20,
|
|
111
|
-
step: 1,
|
|
112
|
-
read: (config) => config.loop.maxReviewIterations,
|
|
113
|
-
apply: (config, value) => ({
|
|
114
|
-
...config,
|
|
115
|
-
loop: {
|
|
116
|
-
...config.loop,
|
|
117
|
-
maxReviewIterations: value,
|
|
118
|
-
},
|
|
371
|
+
stringSetting({
|
|
372
|
+
key: 'cost.model',
|
|
373
|
+
label: 'Cost Model',
|
|
374
|
+
description: 'Cost enforcement model.',
|
|
375
|
+
details: 'pay-per-use enforces spend caps; subscription treats USD as advisory.',
|
|
376
|
+
defaultValue: 'pay-per-use',
|
|
377
|
+
yamlPath: ['cost', 'model'],
|
|
378
|
+
options: ['pay-per-use', 'subscription'],
|
|
119
379
|
}),
|
|
120
|
-
|
|
121
|
-
key: '
|
|
380
|
+
stringSetting({
|
|
381
|
+
key: 'cost.pricing.defaultModel',
|
|
382
|
+
label: 'Default Pricing Model',
|
|
383
|
+
description: 'Fallback model key for pricing lookups.',
|
|
384
|
+
details: 'Used when a worker profile does not specify a pricing model.',
|
|
385
|
+
defaultValue: 'default',
|
|
386
|
+
yamlPath: ['cost', 'pricing', 'defaultModel'],
|
|
387
|
+
minLength: 1,
|
|
388
|
+
}),
|
|
389
|
+
jsonSetting({
|
|
390
|
+
key: 'cost.pricing.models',
|
|
391
|
+
label: 'Pricing Models Map',
|
|
392
|
+
description: 'Model-specific pricing table.',
|
|
393
|
+
details: 'Record keyed by model name with token/minute pricing values.',
|
|
394
|
+
defaultValue: {},
|
|
395
|
+
yamlPath: ['cost', 'pricing', 'models'],
|
|
396
|
+
normalize: (value) => validateJsonSettingShape(value, CostPricingModelsOverrideSchema, 'cost.pricing.models'),
|
|
397
|
+
}),
|
|
398
|
+
jsonSetting({
|
|
399
|
+
key: 'workerProfiles',
|
|
400
|
+
label: 'Worker Profiles',
|
|
401
|
+
description: 'Global worker profile definitions.',
|
|
402
|
+
details: 'Record of worker profile definitions keyed by profile name.',
|
|
403
|
+
defaultValue: {},
|
|
404
|
+
yamlPath: ['workerProfiles'],
|
|
405
|
+
sensitive: true,
|
|
406
|
+
normalize: (value) => validateJsonSettingShape(value, WorkerProfilesOverrideSchema, 'workerProfiles'),
|
|
407
|
+
sanitizeForDisplay: (value) => redactWorkerProfiles(value),
|
|
408
|
+
}),
|
|
409
|
+
booleanSetting({
|
|
410
|
+
key: 'metrics.enabled',
|
|
411
|
+
label: 'Metrics Enabled',
|
|
412
|
+
description: 'Enable Prometheus metrics export.',
|
|
413
|
+
details: 'Turns `/metrics` export on or off.',
|
|
414
|
+
defaultValue: true,
|
|
415
|
+
yamlPath: ['metrics', 'enabled'],
|
|
416
|
+
}),
|
|
417
|
+
numberSetting({
|
|
418
|
+
key: 'metrics.port',
|
|
419
|
+
label: 'Metrics Port',
|
|
420
|
+
description: 'TCP port for Prometheus metrics endpoint.',
|
|
421
|
+
details: 'Port used when metrics exporter is enabled.',
|
|
422
|
+
defaultValue: 9090,
|
|
423
|
+
yamlPath: ['metrics', 'port'],
|
|
122
424
|
integer: true,
|
|
123
425
|
min: 1,
|
|
124
|
-
max:
|
|
426
|
+
max: 65535,
|
|
427
|
+
step: 1,
|
|
125
428
|
}),
|
|
126
|
-
|
|
127
|
-
key: '
|
|
429
|
+
stringSetting({
|
|
430
|
+
key: 'metrics.host',
|
|
431
|
+
label: 'Metrics Host',
|
|
432
|
+
description: 'Bind address for Prometheus metrics endpoint.',
|
|
433
|
+
details: 'Network interface/address used by metrics exporter.',
|
|
434
|
+
defaultValue: '0.0.0.0',
|
|
435
|
+
yamlPath: ['metrics', 'host'],
|
|
436
|
+
minLength: 1,
|
|
437
|
+
}),
|
|
438
|
+
booleanSetting({
|
|
439
|
+
key: 'observability.agentStreaming',
|
|
440
|
+
label: 'Agent Streaming',
|
|
441
|
+
description: 'Enable in-flight agent event streaming to TUI/Web.',
|
|
442
|
+
details: 'Turns live agent event streaming on or off for terminal and web views.',
|
|
443
|
+
defaultValue: true,
|
|
444
|
+
yamlPath: ['observability', 'agentStreaming'],
|
|
445
|
+
}),
|
|
446
|
+
numberSetting({
|
|
447
|
+
key: 'observability.eventRetention',
|
|
448
|
+
label: 'Event Retention',
|
|
449
|
+
description: 'Maximum in-memory event backlog per run/session.',
|
|
450
|
+
details: 'Controls event retention window for stream history.',
|
|
451
|
+
defaultValue: 1000,
|
|
452
|
+
yamlPath: ['observability', 'eventRetention'],
|
|
128
453
|
integer: true,
|
|
129
|
-
min:
|
|
130
|
-
max:
|
|
454
|
+
min: 100,
|
|
455
|
+
max: 10000,
|
|
456
|
+
step: 100,
|
|
131
457
|
}),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
type: 'number',
|
|
140
|
-
defaultValue: 10,
|
|
141
|
-
yamlPath: ['loop', 'maxTotalAgentPasses'],
|
|
142
|
-
min: 1,
|
|
143
|
-
max: 50,
|
|
144
|
-
step: 1,
|
|
145
|
-
read: (config) => config.loop.maxTotalAgentPasses,
|
|
146
|
-
apply: (config, value) => ({
|
|
147
|
-
...config,
|
|
148
|
-
loop: {
|
|
149
|
-
...config.loop,
|
|
150
|
-
maxTotalAgentPasses: value,
|
|
151
|
-
},
|
|
458
|
+
booleanSetting({
|
|
459
|
+
key: 'observability.sessionLogs',
|
|
460
|
+
label: 'Session Logs Enabled',
|
|
461
|
+
description: 'Persist interactive agent session logs to disk.',
|
|
462
|
+
details: 'If enabled, interactive session logs are written and retained.',
|
|
463
|
+
defaultValue: true,
|
|
464
|
+
yamlPath: ['observability', 'sessionLogs'],
|
|
152
465
|
}),
|
|
153
|
-
|
|
154
|
-
key: '
|
|
466
|
+
numberSetting({
|
|
467
|
+
key: 'observability.sessionLogRetention',
|
|
468
|
+
label: 'Session Log Retention (days)',
|
|
469
|
+
description: 'Retention period for interactive session logs.',
|
|
470
|
+
details: 'Session logs older than this many days may be deleted by cleanup.',
|
|
471
|
+
defaultValue: 7,
|
|
472
|
+
yamlPath: ['observability', 'sessionLogRetention'],
|
|
155
473
|
integer: true,
|
|
156
474
|
min: 1,
|
|
157
|
-
|
|
475
|
+
step: 1,
|
|
158
476
|
}),
|
|
159
|
-
|
|
160
|
-
key: '
|
|
477
|
+
booleanSetting({
|
|
478
|
+
key: 'mcp.enabled',
|
|
479
|
+
label: 'MCP Enabled',
|
|
480
|
+
description: 'Enable MCP server exposure.',
|
|
481
|
+
details: 'Turns MCP server availability on or off for applicable commands/modes.',
|
|
482
|
+
defaultValue: false,
|
|
483
|
+
yamlPath: ['mcp', 'enabled'],
|
|
484
|
+
}),
|
|
485
|
+
stringSetting({
|
|
486
|
+
key: 'mcp.transport',
|
|
487
|
+
label: 'MCP Transport',
|
|
488
|
+
description: 'Transport protocol used by MCP server.',
|
|
489
|
+
details: 'Currently only stdio transport is supported.',
|
|
490
|
+
defaultValue: 'stdio',
|
|
491
|
+
yamlPath: ['mcp', 'transport'],
|
|
492
|
+
options: ['stdio'],
|
|
493
|
+
}),
|
|
494
|
+
stringSetting({
|
|
495
|
+
key: 'mcp.authTokenEnv',
|
|
496
|
+
label: 'MCP Auth Token Env Var',
|
|
497
|
+
description: 'Optional env var name containing MCP mutation auth token.',
|
|
498
|
+
details: 'Set to null (or clear in YAML) to disable MCP mutation token checks.',
|
|
499
|
+
defaultValue: null,
|
|
500
|
+
yamlPath: ['mcp', 'authTokenEnv'],
|
|
501
|
+
allowNull: true,
|
|
502
|
+
minLength: 1,
|
|
503
|
+
}),
|
|
504
|
+
numberSetting({
|
|
505
|
+
key: 'mcp.httpPort',
|
|
506
|
+
label: 'MCP HTTP Port',
|
|
507
|
+
description: 'TCP port for embedded MCP HTTP transport.',
|
|
508
|
+
details: 'Port used when running embedded MCP HTTP/SSE mode.',
|
|
509
|
+
defaultValue: 3100,
|
|
510
|
+
yamlPath: ['mcp', 'httpPort'],
|
|
161
511
|
integer: true,
|
|
162
512
|
min: 1,
|
|
163
|
-
max:
|
|
513
|
+
max: 65535,
|
|
514
|
+
step: 1,
|
|
515
|
+
}),
|
|
516
|
+
stringSetting({
|
|
517
|
+
key: 'mcp.httpHost',
|
|
518
|
+
label: 'MCP HTTP Host',
|
|
519
|
+
description: 'Bind address for embedded MCP HTTP transport.',
|
|
520
|
+
details: 'Network interface/address used for MCP HTTP endpoint binding.',
|
|
521
|
+
defaultValue: '127.0.0.1',
|
|
522
|
+
yamlPath: ['mcp', 'httpHost'],
|
|
523
|
+
minLength: 1,
|
|
524
|
+
}),
|
|
525
|
+
booleanSetting({
|
|
526
|
+
key: 'commentCommands.enabled',
|
|
527
|
+
label: 'Comment Commands Enabled',
|
|
528
|
+
description: 'Enable issue comment command processing.',
|
|
529
|
+
details: 'If enabled, `/orch` comment commands are parsed and processed.',
|
|
530
|
+
defaultValue: true,
|
|
531
|
+
yamlPath: ['commentCommands', 'enabled'],
|
|
532
|
+
}),
|
|
533
|
+
booleanSetting({
|
|
534
|
+
key: 'commentCommands.requireCollaborator',
|
|
535
|
+
label: 'Require Collaborator For Commands',
|
|
536
|
+
description: 'Require collaborator permission for comment commands.',
|
|
537
|
+
details: 'When enabled, only collaborators can run issue comment commands.',
|
|
538
|
+
defaultValue: true,
|
|
539
|
+
yamlPath: ['commentCommands', 'requireCollaborator'],
|
|
540
|
+
}),
|
|
541
|
+
jsonSetting({
|
|
542
|
+
key: 'workflows',
|
|
543
|
+
label: 'Workflow Definitions',
|
|
544
|
+
description: 'Named workflow definitions.',
|
|
545
|
+
details: 'Record of named workflow graphs (steps/roles/agents) used by workflow selection.',
|
|
546
|
+
defaultValue: {},
|
|
547
|
+
yamlPath: ['workflows'],
|
|
548
|
+
normalize: (value) => validateJsonSettingShape(value, WorkflowsOverrideSchema, 'workflows'),
|
|
549
|
+
}),
|
|
550
|
+
];
|
|
551
|
+
const entries = new Map();
|
|
552
|
+
for (const definition of definitions) {
|
|
553
|
+
if (entries.has(definition.key)) {
|
|
554
|
+
throw new Error(`Duplicate runtime setting key: ${definition.key}`);
|
|
555
|
+
}
|
|
556
|
+
entries.set(definition.key, definition);
|
|
557
|
+
}
|
|
558
|
+
return Object.fromEntries(entries);
|
|
559
|
+
}
|
|
560
|
+
function numberSetting(options) {
|
|
561
|
+
return {
|
|
562
|
+
key: options.key,
|
|
563
|
+
label: options.label,
|
|
564
|
+
description: options.description,
|
|
565
|
+
details: options.details,
|
|
566
|
+
type: 'number',
|
|
567
|
+
mutable: options.mutable ?? true,
|
|
568
|
+
sensitive: options.sensitive ?? false,
|
|
569
|
+
defaultValue: options.defaultValue,
|
|
570
|
+
yamlPath: options.yamlPath,
|
|
571
|
+
...(options.min !== undefined ? { min: options.min } : {}),
|
|
572
|
+
...(options.max !== undefined ? { max: options.max } : {}),
|
|
573
|
+
...(options.step !== undefined ? { step: options.step } : {}),
|
|
574
|
+
read: (config) => readNumberValue(config, options.yamlPath, options.defaultValue),
|
|
575
|
+
apply: (config, value) => setConfigValue(config, options.yamlPath, value),
|
|
576
|
+
parseInput: (raw) => parseNumberInput(raw, {
|
|
577
|
+
key: options.key,
|
|
578
|
+
integer: options.integer,
|
|
579
|
+
min: options.min,
|
|
580
|
+
max: options.max,
|
|
581
|
+
}),
|
|
582
|
+
parseStored: (raw) => parseStoredNumber(raw, {
|
|
583
|
+
key: options.key,
|
|
584
|
+
integer: options.integer,
|
|
585
|
+
min: options.min,
|
|
586
|
+
max: options.max,
|
|
164
587
|
}),
|
|
165
588
|
serialize: (value) => JSON.stringify(value),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
589
|
+
sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function booleanSetting(options) {
|
|
593
|
+
return {
|
|
594
|
+
key: options.key,
|
|
595
|
+
label: options.label,
|
|
596
|
+
description: options.description,
|
|
597
|
+
details: options.details,
|
|
172
598
|
type: 'boolean',
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
},
|
|
182
|
-
}),
|
|
183
|
-
parseInput: (raw) => parseBooleanInput(raw, 'observability.agentStreaming'),
|
|
184
|
-
parseStored: (raw) => parseStoredBoolean(raw, 'observability.agentStreaming'),
|
|
599
|
+
mutable: options.mutable ?? true,
|
|
600
|
+
sensitive: options.sensitive ?? false,
|
|
601
|
+
defaultValue: options.defaultValue,
|
|
602
|
+
yamlPath: options.yamlPath,
|
|
603
|
+
read: (config) => readBooleanValue(config, options.yamlPath, options.defaultValue),
|
|
604
|
+
apply: (config, value) => setConfigValue(config, options.yamlPath, value),
|
|
605
|
+
parseInput: (raw) => parseBooleanInput(raw, options.key),
|
|
606
|
+
parseStored: (raw) => parseStoredBoolean(raw, options.key),
|
|
185
607
|
serialize: (value) => JSON.stringify(value),
|
|
186
|
-
|
|
187
|
-
};
|
|
188
|
-
const SETTING_KEYS = Object.keys(SETTING_DEFINITIONS);
|
|
189
|
-
export function listSettingDefinitions() {
|
|
190
|
-
return SETTING_KEYS.map((key) => SETTING_DEFINITIONS[key]);
|
|
608
|
+
sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
|
|
609
|
+
};
|
|
191
610
|
}
|
|
192
|
-
|
|
193
|
-
return
|
|
611
|
+
function stringSetting(options) {
|
|
612
|
+
return {
|
|
613
|
+
key: options.key,
|
|
614
|
+
label: options.label,
|
|
615
|
+
description: options.description,
|
|
616
|
+
details: options.details,
|
|
617
|
+
type: 'string',
|
|
618
|
+
mutable: options.mutable ?? true,
|
|
619
|
+
sensitive: options.sensitive ?? false,
|
|
620
|
+
defaultValue: options.defaultValue,
|
|
621
|
+
yamlPath: options.yamlPath,
|
|
622
|
+
...(options.options ? { options: options.options } : {}),
|
|
623
|
+
...(options.allowNull ? { allowNull: true } : {}),
|
|
624
|
+
read: (config) => readStringValue(config, options.yamlPath, options.defaultValue, options.allowNull ?? false),
|
|
625
|
+
apply: (config, value) => setConfigValue(config, options.yamlPath, value),
|
|
626
|
+
parseInput: (raw) => parseStringInput(raw, {
|
|
627
|
+
key: options.key,
|
|
628
|
+
allowNull: options.allowNull,
|
|
629
|
+
options: options.options,
|
|
630
|
+
minLength: options.minLength,
|
|
631
|
+
url: options.url,
|
|
632
|
+
validate: options.validate,
|
|
633
|
+
}),
|
|
634
|
+
parseStored: (raw) => parseStoredString(raw, {
|
|
635
|
+
key: options.key,
|
|
636
|
+
allowNull: options.allowNull,
|
|
637
|
+
options: options.options,
|
|
638
|
+
minLength: options.minLength,
|
|
639
|
+
url: options.url,
|
|
640
|
+
validate: options.validate,
|
|
641
|
+
}),
|
|
642
|
+
serialize: (value) => JSON.stringify(value),
|
|
643
|
+
sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
|
|
644
|
+
};
|
|
194
645
|
}
|
|
195
|
-
|
|
196
|
-
if (!hasValueAtPath(rawConfig, definition.yamlPath)) {
|
|
197
|
-
return {
|
|
198
|
-
hasYamlValue: false,
|
|
199
|
-
yamlValue: null,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
646
|
+
function jsonSetting(options) {
|
|
202
647
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
648
|
+
key: options.key,
|
|
649
|
+
label: options.label,
|
|
650
|
+
description: options.description,
|
|
651
|
+
details: options.details,
|
|
652
|
+
type: 'json',
|
|
653
|
+
mutable: options.mutable ?? true,
|
|
654
|
+
sensitive: options.sensitive ?? false,
|
|
655
|
+
defaultValue: options.defaultValue,
|
|
656
|
+
yamlPath: options.yamlPath,
|
|
657
|
+
read: (config) => readJsonValue(config, options.yamlPath, options.defaultValue),
|
|
658
|
+
apply: (config, value) => setConfigValue(config, options.yamlPath, value),
|
|
659
|
+
parseInput: (raw) => {
|
|
660
|
+
const parsed = parseJsonInput(raw, options.key);
|
|
661
|
+
return options.normalize ? options.normalize(parsed) : parsed;
|
|
662
|
+
},
|
|
663
|
+
parseStored: (raw) => {
|
|
664
|
+
const parsed = parseStoredJson(raw, options.key);
|
|
665
|
+
return options.normalize ? options.normalize(parsed) : parsed;
|
|
666
|
+
},
|
|
667
|
+
serialize: (value) => JSON.stringify(value),
|
|
668
|
+
sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
|
|
205
669
|
};
|
|
206
670
|
}
|
|
207
671
|
function parseNumberInput(raw, options) {
|
|
@@ -231,8 +695,11 @@ function validateNumber(value, options) {
|
|
|
231
695
|
if (options.integer && !Number.isInteger(value)) {
|
|
232
696
|
throw new Error(`${options.key} must be an integer`);
|
|
233
697
|
}
|
|
234
|
-
if (
|
|
235
|
-
throw new Error(`${options.key} must be
|
|
698
|
+
if (options.min !== undefined && value < options.min) {
|
|
699
|
+
throw new Error(`${options.key} must be >= ${options.min}`);
|
|
700
|
+
}
|
|
701
|
+
if (options.max !== undefined && value > options.max) {
|
|
702
|
+
throw new Error(`${options.key} must be <= ${options.max}`);
|
|
236
703
|
}
|
|
237
704
|
return value;
|
|
238
705
|
}
|
|
@@ -275,12 +742,160 @@ function parseStoredBoolean(raw, key) {
|
|
|
275
742
|
throw new Error(`Invalid stored value for ${key}: ${err.message}`);
|
|
276
743
|
}
|
|
277
744
|
}
|
|
278
|
-
function
|
|
745
|
+
function parseStringInput(raw, options) {
|
|
746
|
+
if (raw === null) {
|
|
747
|
+
if (options.allowNull) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
throw new Error(`${options.key} must be a string`);
|
|
751
|
+
}
|
|
752
|
+
if (typeof raw !== 'string') {
|
|
753
|
+
throw new Error(`${options.key} must be a string`);
|
|
754
|
+
}
|
|
755
|
+
if (options.allowNull && raw.trim().toLowerCase() === 'null') {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
const value = raw;
|
|
759
|
+
if (options.minLength !== undefined && value.trim().length < options.minLength) {
|
|
760
|
+
throw new Error(`${options.key} must be at least ${options.minLength} character(s)`);
|
|
761
|
+
}
|
|
762
|
+
if (options.options && !options.options.includes(value)) {
|
|
763
|
+
throw new Error(`${options.key} must be one of: ${options.options.join(', ')}`);
|
|
764
|
+
}
|
|
765
|
+
if (options.url) {
|
|
766
|
+
try {
|
|
767
|
+
void new URL(value);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
throw new Error(`${options.key} must be a valid URL`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const validationError = options.validate?.(value);
|
|
774
|
+
if (validationError) {
|
|
775
|
+
throw new Error(validationError);
|
|
776
|
+
}
|
|
777
|
+
return value;
|
|
778
|
+
}
|
|
779
|
+
function parseStoredString(raw, options) {
|
|
780
|
+
try {
|
|
781
|
+
const parsed = JSON.parse(raw);
|
|
782
|
+
if (typeof parsed !== 'string' && parsed !== null) {
|
|
783
|
+
throw new Error(`Stored value for ${options.key} is not string/null`);
|
|
784
|
+
}
|
|
785
|
+
return parseStringInput(parsed, options);
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
throw new Error(`Invalid stored value for ${options.key}: ${err.message}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function parseJsonInput(raw, key) {
|
|
792
|
+
if (typeof raw === 'string') {
|
|
793
|
+
try {
|
|
794
|
+
const parsed = JSON.parse(raw);
|
|
795
|
+
if (!isJsonValue(parsed)) {
|
|
796
|
+
throw new Error('must be valid JSON');
|
|
797
|
+
}
|
|
798
|
+
return parsed;
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
throw new Error(`${key} must be valid JSON: ${err.message}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (!isJsonValue(raw)) {
|
|
805
|
+
throw new Error(`${key} must be valid JSON`);
|
|
806
|
+
}
|
|
807
|
+
return raw;
|
|
808
|
+
}
|
|
809
|
+
function parseStoredJson(raw, key) {
|
|
810
|
+
try {
|
|
811
|
+
const parsed = JSON.parse(raw);
|
|
812
|
+
if (!isJsonValue(parsed)) {
|
|
813
|
+
throw new Error(`Stored value for ${key} is not valid JSON`);
|
|
814
|
+
}
|
|
815
|
+
return parsed;
|
|
816
|
+
}
|
|
817
|
+
catch (err) {
|
|
818
|
+
throw new Error(`Invalid stored value for ${key}: ${err.message}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function validateJsonSettingShape(value, schema, key) {
|
|
822
|
+
const result = schema.safeParse(value);
|
|
823
|
+
if (result.success) {
|
|
824
|
+
return result.data;
|
|
825
|
+
}
|
|
826
|
+
const issue = result.error.issues[0];
|
|
827
|
+
const path = issue?.path.length ? issue.path.join('.') : key;
|
|
828
|
+
const message = issue?.message ?? 'invalid structure';
|
|
829
|
+
throw new Error(`${key} has invalid structure (${path}): ${message}`);
|
|
830
|
+
}
|
|
831
|
+
function identitySanitize(value) {
|
|
832
|
+
return value;
|
|
833
|
+
}
|
|
834
|
+
function redactWorkerProfiles(value) {
|
|
835
|
+
if (!isRecord(value)) {
|
|
836
|
+
return value;
|
|
837
|
+
}
|
|
838
|
+
return Object.fromEntries(Object.entries(value).map(([profileName, profile]) => {
|
|
839
|
+
if (!isRecord(profile) || !isRecord(profile['env'])) {
|
|
840
|
+
return [profileName, profile];
|
|
841
|
+
}
|
|
842
|
+
const redactedEnv = Object.fromEntries(Object.keys(profile['env']).map((envKey) => [envKey, '[redacted]']));
|
|
843
|
+
return [profileName, { ...profile, env: redactedEnv }];
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
function readNumberValue(config, path, fallback) {
|
|
847
|
+
const value = readPathValue(config, path);
|
|
848
|
+
return typeof value === 'number' ? value : fallback;
|
|
849
|
+
}
|
|
850
|
+
function readBooleanValue(config, path, fallback) {
|
|
851
|
+
const value = readPathValue(config, path);
|
|
852
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
853
|
+
}
|
|
854
|
+
function readStringValue(config, path, fallback, allowNull) {
|
|
855
|
+
const value = readPathValue(config, path);
|
|
856
|
+
if (typeof value === 'string') {
|
|
857
|
+
return value;
|
|
858
|
+
}
|
|
859
|
+
if (allowNull && value === null) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return fallback;
|
|
863
|
+
}
|
|
864
|
+
function readJsonValue(config, path, fallback) {
|
|
865
|
+
const value = readPathValue(config, path);
|
|
866
|
+
return isJsonValue(value) ? value : fallback;
|
|
867
|
+
}
|
|
868
|
+
function setConfigValue(config, path, value) {
|
|
869
|
+
return setPathValue(config, path, value);
|
|
870
|
+
}
|
|
871
|
+
function readPathValue(source, path) {
|
|
872
|
+
let current = source;
|
|
873
|
+
for (const segment of path) {
|
|
874
|
+
if (!isRecord(current)) {
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
current = current[segment];
|
|
878
|
+
}
|
|
879
|
+
return current;
|
|
880
|
+
}
|
|
881
|
+
function setPathValue(source, path, value) {
|
|
882
|
+
if (path.length === 0) {
|
|
883
|
+
return isRecord(source) ? { ...source } : {};
|
|
884
|
+
}
|
|
885
|
+
const [segment, ...rest] = path;
|
|
886
|
+
if (segment === undefined) {
|
|
887
|
+
return isRecord(source) ? { ...source } : {};
|
|
888
|
+
}
|
|
889
|
+
const current = isRecord(source) ? source : {};
|
|
890
|
+
if (rest.length === 0) {
|
|
891
|
+
return {
|
|
892
|
+
...current,
|
|
893
|
+
[segment]: value,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
279
896
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
sessionLogs: config.observability?.sessionLogs ?? true,
|
|
283
|
-
sessionLogRetention: config.observability?.sessionLogRetention ?? 7,
|
|
897
|
+
...current,
|
|
898
|
+
[segment]: setPathValue(current[segment], rest, value),
|
|
284
899
|
};
|
|
285
900
|
}
|
|
286
901
|
function hasValueAtPath(source, path) {
|
|
@@ -296,4 +911,19 @@ function hasValueAtPath(source, path) {
|
|
|
296
911
|
function isRecord(value) {
|
|
297
912
|
return typeof value === 'object' && value !== null;
|
|
298
913
|
}
|
|
914
|
+
function isJsonValue(value) {
|
|
915
|
+
if (value === null
|
|
916
|
+
|| typeof value === 'string'
|
|
917
|
+
|| typeof value === 'number'
|
|
918
|
+
|| typeof value === 'boolean') {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
if (Array.isArray(value)) {
|
|
922
|
+
return value.every((entry) => isJsonValue(entry));
|
|
923
|
+
}
|
|
924
|
+
if (!isRecord(value)) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
return Object.values(value).every((entry) => isJsonValue(entry));
|
|
928
|
+
}
|
|
299
929
|
//# sourceMappingURL=registry.js.map
|