neuralos 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ /**
2
+ * patch-manager plugin — autonomous patch management for RTerm.
3
+ *
4
+ * Discovers patch status across hosts (yum/apt/Windows Update), plans patch
5
+ * deployments, executes via playbooks with MOP approval gates, and alerts on
6
+ * completion/failure. Combines RTerm's fleet orchestration, MOP change
7
+ * management, triggers, and audit trail into an autonomous patch pipeline.
8
+ */
9
+
10
+ // --- Pure: build the command to query patch status on a host ---
11
+ export function buildPatchStatusCommand(os) {
12
+ const o = String(os).toLowerCase()
13
+ if (o === 'windows' || o === 'win32') {
14
+ return 'powershell -Command "Get-WindowsUpdate -Verbose 2>&1 | Select-Object -First 50"'
15
+ }
16
+ if (o === 'linux' || o === 'darwin') {
17
+ // Try yum first, then apt
18
+ return '(yum check-update --quiet 2>/dev/null || apt list --upgradable 2>/dev/null) | head -50'
19
+ }
20
+ return 'echo "unsupported os: ' + o + '"'
21
+ }
22
+
23
+ // --- Pure: build the command to apply patches on a host ---
24
+ export function buildPatchApplyCommand(os, opts = {}) {
25
+ const o = String(os).toLowerCase()
26
+ const severity = opts.severity ?? 'all' // all, critical, security, recommended
27
+ const dryRun = opts.dryRun ?? false
28
+
29
+ if (o === 'windows' || o === 'win32') {
30
+ const kbFilter = severity === 'critical' ? '-KBArticleID "KB*"' : ''
31
+ const dryRunFlag = dryRun ? '-WhatIf' : ''
32
+ return `powershell -Command "Install-WindowsUpdate ${kbFilter} ${dryRunFlag} -AcceptAll -AutoReboot:$false 2>&1"`
33
+ }
34
+ if (o === 'linux') {
35
+ if (severity === 'security') {
36
+ return dryRun
37
+ ? '(yum update --security --assumeno 2>/dev/null || apt-get -s upgrade --with-new-pkgs 2>/dev/null | grep -i security | head -20)'
38
+ : '(yum update --security -y 2>/dev/null || apt-get upgrade -y --with-new-pkgs 2>/dev/null) | tail -20'
39
+ }
40
+ return dryRun
41
+ ? '(yum update --assumeno 2>/dev/null || apt-get -s upgrade 2>/dev/null) | tail -20'
42
+ : '(yum update -y 2>/dev/null || apt-get upgrade -y 2>/dev/null) | tail -20'
43
+ }
44
+ return 'echo "unsupported os: ' + o + '"'
45
+ }
46
+
47
+ // --- Pure: build the pre-patch verification command ---
48
+ export function buildPrePatchCheckCommand(os) {
49
+ const o = String(os).toLowerCase()
50
+ if (o === 'windows' || o === 'win32') {
51
+ return 'powershell -Command "Get-PSDrive -Name C | Select-Object Used,Free; Get-Service -Name wuauserv | Select-Object Status"'
52
+ }
53
+ if (o === 'linux') {
54
+ return 'df -h / | tail -1; systemctl is-system-running 2>/dev/null || true; uptime'
55
+ }
56
+ return 'echo "unsupported os: ' + o + '"'
57
+ }
58
+
59
+ // --- Pure: build the post-patch verification command ---
60
+ export function buildPostPatchCheckCommand(os) {
61
+ const o = String(os).toLowerCase()
62
+ if (o === 'windows' || o === 'win32') {
63
+ return 'powershell -Command "Get-WindowsUpdate -Verbose 2>&1 | Select-Object -First 10; Get-Service -Name wuauserv | Select-Object Status"'
64
+ }
65
+ if (o === 'linux') {
66
+ return 'echo "post-patch check: system running"; uptime; df -h / | tail -1'
67
+ }
68
+ return 'echo "unsupported os: ' + o + '"'
69
+ }
70
+
71
+ // --- Pure: parse patch status output into structured results ---
72
+ export function parsePatchStatus(output, os) {
73
+ const o = String(os ?? '').toLowerCase()
74
+ const lines = String(output ?? '').split(/\r?\n/).filter((l) => l.trim().length > 0)
75
+ const patches = []
76
+ let summary = { total: 0, critical: 0, security: 0, recommended: 0 }
77
+
78
+ for (const line of lines) {
79
+ const l = line.trim()
80
+ // Windows: KB article lines
81
+ const kbMatch = l.match(/(KB\d{7})/i)
82
+ if (kbMatch) {
83
+ patches.push({
84
+ id: kbMatch[1],
85
+ title: l.replace(/KB\d{7}/i, '').trim().slice(0, 80),
86
+ severity: l.match(/critical/i) ? 'critical' : l.match(/security/i) ? 'security' : 'recommended',
87
+ os: 'windows',
88
+ })
89
+ summary.total++
90
+ if (l.match(/critical/i)) summary.critical++
91
+ if (l.match(/security/i)) summary.security++
92
+ continue
93
+ }
94
+ // Linux yum: package.arch version repo
95
+ const yumMatch = l.match(/^(\S+)\.(\S+)\s+(\S+)\s+(\S+)/)
96
+ if (yumMatch && !l.startsWith('Loaded') && !l.startsWith('Last') && !l.startsWith('No')) {
97
+ patches.push({
98
+ id: yumMatch[1],
99
+ title: `${yumMatch[1]}.${yumMatch[2]} ${yumMatch[3]} (${yumMatch[4]})`,
100
+ severity: l.match(/security/i) ? 'security' : l.match(/critical/i) ? 'critical' : 'recommended',
101
+ os: 'linux',
102
+ })
103
+ summary.total++
104
+ if (l.match(/security/i)) summary.security++
105
+ if (l.match(/critical/i)) summary.critical++
106
+ continue
107
+ }
108
+ // Linux apt: package/version
109
+ const aptMatch = l.match(/^(\S+)\/(\S+)\s+(\S+)\s+(\S+)/)
110
+ if (aptMatch && !l.startsWith('Listing')) {
111
+ patches.push({
112
+ id: aptMatch[1],
113
+ title: `${aptMatch[1]}/${aptMatch[2]} ${aptMatch[3]} (${aptMatch[4]})`,
114
+ severity: l.match(/security/i) ? 'security' : l.match(/critical/i) ? 'critical' : 'recommended',
115
+ os: 'linux',
116
+ })
117
+ summary.total++
118
+ if (l.match(/security/i)) summary.security++
119
+ if (l.match(/critical/i)) summary.critical++
120
+ }
121
+ }
122
+
123
+ summary.recommended = summary.total - summary.critical - summary.security
124
+ return { patches, summary }
125
+ }
126
+
127
+ // --- Pure: build a patch plan from patch status ---
128
+ export function buildPatchPlan(host, os, patchStatus, opts = {}) {
129
+ const severity = opts.severity ?? 'all'
130
+ const toApply = severity === 'all'
131
+ ? patchStatus.patches
132
+ : patchStatus.patches.filter((p) => p.severity === severity)
133
+
134
+ return {
135
+ host,
136
+ os,
137
+ patchesToApply: toApply.length,
138
+ patchIds: toApply.map((p) => p.id),
139
+ severity,
140
+ estimatedDowntimeMin: toApply.length * 2, // rough estimate
141
+ requiresReboot: os === 'windows' || toApply.some((p) => p.severity === 'critical'),
142
+ plan: [
143
+ { step: 1, name: 'pre-check', command: buildPrePatchCheckCommand(os), description: 'Verify system health before patching' },
144
+ { step: 2, name: 'backup', command: 'echo "snapshot/checkpoint before patch"', description: 'Create restore point' },
145
+ { step: 3, name: 'apply', command: buildPatchApplyCommand(os, { severity }), description: `Apply ${toApply.length} patches (${severity})` },
146
+ { step: 4, name: 'post-check', command: buildPostPatchCheckCommand(os), description: 'Verify patches applied successfully' },
147
+ { step: 5, name: 'rollback', command: 'echo "rollback if post-check fails"', description: 'Rollback on failure', onError: 'continue' },
148
+ ],
149
+ }
150
+ }
151
+
152
+ // --- Pure: build a compliance report for a fleet ---
153
+ export function buildComplianceReport(hostStatuses) {
154
+ const hosts = Object.entries(hostStatuses ?? {}).map(([host, status]) => ({
155
+ host,
156
+ os: status.os ?? 'unknown',
157
+ totalPatches: status.summary?.total ?? 0,
158
+ criticalPatches: status.summary?.critical ?? 0,
159
+ securityPatches: status.summary?.security ?? 0,
160
+ compliant: (status.summary?.critical ?? 0) === 0 && (status.summary?.security ?? 0) === 0,
161
+ }))
162
+
163
+ const totalHosts = hosts.length
164
+ const compliantHosts = hosts.filter((h) => h.compliant).length
165
+ const nonCompliantHosts = totalHosts - compliantHosts
166
+ const totalCritical = hosts.reduce((s, h) => s + h.criticalPatches, 0)
167
+ const totalSecurity = hosts.reduce((s, h) => s + h.securityPatches, 0)
168
+
169
+ return {
170
+ hosts,
171
+ summary: {
172
+ totalHosts,
173
+ compliantHosts,
174
+ nonCompliantHosts,
175
+ complianceRate: totalHosts > 0 ? Math.round((compliantHosts / totalHosts) * 100) : 100,
176
+ totalCriticalPatches: totalCritical,
177
+ totalSecurityPatches: totalSecurity,
178
+ },
179
+ }
180
+ }
181
+
182
+ // --- Plugin entry ---
183
+ export function register(ctx) {
184
+ const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
185
+
186
+ // Tool: patch_status — query patch status of a host
187
+ registerTool({
188
+ name: 'patch_status',
189
+ description: 'Query the current patch status of a host (available updates). Returns patch list with severity classification.',
190
+ params: {
191
+ host: { type: 'string', description: 'Host to query' },
192
+ os: { type: 'string', description: 'OS type: linux, windows, darwin' },
193
+ },
194
+ handler: async (params) => {
195
+ const { host, os } = params ?? {}
196
+ if (!host || !os) return { error: 'host and os are required' }
197
+ const cmd = buildPatchStatusCommand(os)
198
+ log(`[patch-manager] querying patch status on ${host} (${os})`)
199
+ const output = await exec(cmd, { host })
200
+ const result = parsePatchStatus(output, os)
201
+ return { host, os, ...result }
202
+ },
203
+ })
204
+
205
+ // Tool: patch_plan — build a patch deployment plan for a host
206
+ registerTool({
207
+ name: 'patch_plan',
208
+ description: 'Build a patch deployment plan for a host (pre-check → backup → apply → post-check → rollback). Returns the plan for MOP approval.',
209
+ params: {
210
+ host: { type: 'string', description: 'Host to plan for' },
211
+ os: { type: 'string', description: 'OS type' },
212
+ severity: { type: 'string', description: 'Patch severity filter: all, critical, security, recommended' },
213
+ },
214
+ handler: async (params) => {
215
+ const { host, os, severity = 'all' } = params ?? {}
216
+ if (!host || !os) return { error: 'host and os are required' }
217
+ // First get current status
218
+ const statusCmd = buildPatchStatusCommand(os)
219
+ const statusOutput = await exec(statusCmd, { host })
220
+ const patchStatus = parsePatchStatus(statusOutput, os)
221
+ const plan = buildPatchPlan(host, os, patchStatus, { severity })
222
+ log(`[patch-manager] built patch plan for ${host}: ${plan.patchesToApply} patches`)
223
+ return plan
224
+ },
225
+ })
226
+
227
+ // Tool: patch_apply — execute a patch plan on a host (runs as MOP change)
228
+ registerTool({
229
+ name: 'patch_apply',
230
+ description: 'Execute a patch deployment on a host. This is a destructive operation — requires MOP approval. Returns the execution result.',
231
+ params: {
232
+ host: { type: 'string', description: 'Host to patch' },
233
+ os: { type: 'string', description: 'OS type' },
234
+ severity: { type: 'string', description: 'Patch severity filter' },
235
+ dryRun: { type: 'boolean', description: 'If true, simulate only (no changes)' },
236
+ },
237
+ handler: async (params) => {
238
+ const { host, os, severity = 'all', dryRun = false } = params ?? {}
239
+ if (!host || !os) return { error: 'host and os are required' }
240
+ const applyCmd = buildPatchApplyCommand(os, { severity, dryRun })
241
+ log(`[patch-manager] ${dryRun ? 'simulating' : 'applying'} patches on ${host} (${severity})`)
242
+ const output = await exec(applyCmd, { host })
243
+ const postCheck = await exec(buildPostPatchCheckCommand(os), { host })
244
+ return { host, os, severity, dryRun, output: output.slice(0, 2000), postCheck: postCheck.slice(0, 500) }
245
+ },
246
+ })
247
+
248
+ // Trigger: patch_failure — fires when a patch execution fails
249
+ registerTrigger({
250
+ name: 'patch_failure',
251
+ description: 'Fires when a patch execution output contains error/failure indicators. Use for auto-remediation or investigation.',
252
+ match: (event) => {
253
+ if (event?.source !== 'playbook') return false
254
+ const detail = String(event?.detail ?? '').toLowerCase()
255
+ return detail.includes('error') || detail.includes('failed') || detail.includes('rollback')
256
+ },
257
+ action: 'propose-change',
258
+ })
259
+
260
+ // Trigger: patch_completion — fires when a patch execution completes successfully
261
+ registerTrigger({
262
+ name: 'patch_completion',
263
+ description: 'Fires when a patch execution completes without errors. Use for compliance reporting.',
264
+ match: (event) => {
265
+ if (event?.source !== 'playbook') return false
266
+ const detail = String(event?.detail ?? '').toLowerCase()
267
+ return detail.includes('success') || detail.includes('completed') || detail.includes('no packages marked for update')
268
+ },
269
+ action: 'run-playbook',
270
+ })
271
+
272
+ // Panel: patch-compliance — patch compliance dashboard
273
+ registerPanel({
274
+ name: 'patch-compliance',
275
+ title: 'Patch Compliance',
276
+ render: (data) => {
277
+ const report = buildComplianceReport(data ?? {})
278
+ const rows = report.hosts.map((h) =>
279
+ `<tr><td>${h.host}</td><td>${h.os}</td><td>${h.totalPatches}</td><td>${h.criticalPatches}</td><td>${h.securityPatches}</td><td>${h.compliant ? '✅' : '❌'}</td></tr>`
280
+ ).join('')
281
+ return `<div class="patch-compliance"><h3>Patch Compliance</h3><p>Compliance rate: ${report.summary.complianceRate}% (${report.summary.compliantHosts}/${report.summary.totalHosts} hosts)</p><p>Critical: ${report.summary.totalCriticalPatches} | Security: ${report.summary.totalSecurityPatches}</p><table><thead><tr><th>Host</th><th>OS</th><th>Total</th><th>Critical</th><th>Security</th><th>Compliant</th></tr></thead><tbody>${rows}</tbody></table></div>`
282
+ },
283
+ })
284
+
285
+ log('[patch-manager] registered: 3 tools, 2 triggers, 1 panel')
286
+ }
287
+
288
+ export default { register, buildPatchStatusCommand, buildPatchApplyCommand, buildPrePatchCheckCommand, buildPostPatchCheckCommand, parsePatchStatus, buildPatchPlan, buildComplianceReport }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "patch-manager",
3
+ "version": "1.0.0",
4
+ "description": "Autonomous patch management for RTerm — discover patch status across hosts, plan patch deployments, execute via playbooks with MOP approval gates, and alert on completion/failure. Supports Linux (yum/apt) and Windows (Get-WindowsUpdate).",
5
+ "entry": "index.mjs",
6
+ "tools": ["patch_status", "patch_plan", "patch_apply"],
7
+ "triggers": ["patch_failure", "patch_completion"],
8
+ "panels": ["patch-compliance"],
9
+ "permissions": ["exec", "readLedger:metrics", "readLedger:incidents"]
10
+ }
@@ -0,0 +1,9 @@
1
+ // request-router plugin type declarations
2
+ export function register(ctx: any): void
3
+ export function classifyRequest(request: any): string
4
+ export function routeRequest(request: any): { route: string; risk: string; reason: string }
5
+ export function buildRequestId(): string
6
+ export function buildApprovalRecord(requestId: string, approvedBy: string, rationale: string, decision: string): any
7
+ export function buildQueueEntry(request: any, requestId: string): any
8
+ export function filterQueue(queue: any[] | null | undefined, filter?: any): any[]
9
+ export default any
@@ -0,0 +1,239 @@
1
+ /**
2
+ * request-router plugin — automated request handling and approval workflow.
3
+ *
4
+ * Receives operational requests (access, restart, deploy, patch, custom),
5
+ * classifies risk, routes for approval (auto-approve/queue/MOP), executes
6
+ * end-to-end, and audits every step. Formalizes RTerm's existing command
7
+ * policy + MOP approval into a structured request pipeline.
8
+ */
9
+
10
+ // --- Pure: classify a request by risk ---
11
+ export function classifyRequest(request) {
12
+ const req = request ?? {}
13
+ const type = String(req.type ?? '').toLowerCase()
14
+ const urgency = String(req.urgency ?? 'low').toLowerCase()
15
+ const target = String(req.target ?? '').toLowerCase()
16
+
17
+ // Destructive operations = high risk
18
+ if (['delete', 'drop', 'purge', 'format', 'destroy'].some((w) => type.includes(w))) return 'high'
19
+ // Production targets = elevated risk
20
+ if (target.includes('prod') || target.includes('production')) {
21
+ if (['restart', 'stop', 'kill', 'deploy', 'patch', 'update'].some((w) => type.includes(w))) return 'high'
22
+ return 'medium'
23
+ }
24
+ // Restart/stop operations = medium risk
25
+ if (['restart', 'stop', 'kill', 'deploy', 'patch', 'update', 'reboot', 'shutdown'].some((w) => type.includes(w))) return 'medium'
26
+ // Access requests = medium risk
27
+ if (['access', 'grant', 'permission', 'role', 'sudo', 'admin'].some((w) => type.includes(w))) return 'medium'
28
+ // Read-only = low risk
29
+ if (['status', 'check', 'list', 'show', 'get', 'describe', 'read'].some((w) => type.includes(w))) return 'low'
30
+ // Default: medium
31
+ return 'medium'
32
+ }
33
+
34
+ // --- Pure: route a request based on risk ---
35
+ export function routeRequest(request) {
36
+ const req = request ?? {}
37
+ const risk = classifyRequest(req)
38
+ const urgency = String(req.urgency ?? 'low').toLowerCase()
39
+
40
+ if (risk === 'low') return { route: 'auto_approve', risk, reason: 'low-risk read-only operation' }
41
+ if (risk === 'medium') {
42
+ if (urgency === 'critical' || urgency === 'high') {
43
+ return { route: 'queue', risk, reason: 'medium-risk with high urgency — expedited approval' }
44
+ }
45
+ return { route: 'queue', risk, reason: 'medium-risk operation — requires operator approval' }
46
+ }
47
+ return { route: 'mop', risk, reason: 'high-risk operation — requires MOP change (plan → approve → run)' }
48
+ }
49
+
50
+ // --- Pure: build a request ID ---
51
+ export function buildRequestId() {
52
+ return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
53
+ }
54
+
55
+ // --- Pure: build an approval record ---
56
+ export function buildApprovalRecord(requestId, approvedBy, rationale, decision) {
57
+ return {
58
+ requestId,
59
+ approvedBy,
60
+ rationale,
61
+ decision, // approved | denied
62
+ at: Date.now(),
63
+ }
64
+ }
65
+
66
+ // --- Pure: build a request queue entry ---
67
+ export function buildQueueEntry(request, requestId) {
68
+ const req = request ?? {}
69
+ const { route, risk, reason } = routeRequest(req)
70
+ return {
71
+ id: requestId,
72
+ type: req.type,
73
+ target: req.target,
74
+ justification: req.justification ?? '',
75
+ urgency: req.urgency ?? 'low',
76
+ risk,
77
+ route,
78
+ routeReason: reason,
79
+ status: route === 'auto_approve' ? 'auto_approved' : 'pending',
80
+ submittedBy: req.submittedBy ?? 'unknown',
81
+ submittedAt: Date.now(),
82
+ ...(route === 'auto_approve' ? { approvedAt: Date.now(), approvedBy: 'system' } : {}),
83
+ }
84
+ }
85
+
86
+ // --- Pure: filter the request queue ---
87
+ export function filterQueue(queue, filter = {}) {
88
+ let out = Array.isArray(queue) ? [...queue] : []
89
+ const f = filter ?? {}
90
+ if (f.status) out = out.filter((r) => r.status === f.status)
91
+ if (f.risk) out = out.filter((r) => r.risk === f.risk)
92
+ if (f.urgency) out = out.filter((r) => r.urgency === f.urgency)
93
+ if (f.target) out = out.filter((r) => String(r.target ?? '').includes(f.target))
94
+ return out
95
+ }
96
+
97
+ // --- Plugin entry ---
98
+ export function register(ctx) {
99
+ const { registerTool, registerTrigger, registerPanel, exec, readLedger, log } = ctx
100
+ const requestQueue = []
101
+
102
+ // Tool: submit_request — submit an operational request
103
+ registerTool({
104
+ name: 'submit_request',
105
+ description: 'Submit an operational request for approval and execution. The request is classified by risk and routed: low-risk → auto-approve, medium-risk → queue for operator approval, high-risk → MOP change.',
106
+ params: {
107
+ type: { type: 'string', description: 'Request type: access, restart, deploy, patch, status, custom' },
108
+ target: { type: 'string', description: 'Target host or service' },
109
+ justification: { type: 'string', description: 'Business justification' },
110
+ urgency: { type: 'string', description: 'low, medium, high, critical' },
111
+ submittedBy: { type: 'string', description: 'Who is submitting the request' },
112
+ },
113
+ handler: async (params) => {
114
+ const requestId = buildRequestId()
115
+ const entry = buildQueueEntry(params, requestId)
116
+ requestQueue.push(entry)
117
+ log(`[request-router] ${entry.id}: ${entry.type} on ${entry.target} → ${entry.route} (${entry.risk} risk)`)
118
+ return { requestId, ...entry }
119
+ },
120
+ })
121
+
122
+ // Tool: approve_request — approve or deny a pending request
123
+ registerTool({
124
+ name: 'approve_request',
125
+ description: 'Approve or deny a pending request. Executes the request if approved. Records the approval in the audit trail.',
126
+ params: {
127
+ requestId: { type: 'string', description: 'Request ID to approve/deny' },
128
+ approvedBy: { type: 'string', description: 'Who is approving' },
129
+ rationale: { type: 'string', description: 'Reason for the decision' },
130
+ decision: { type: 'string', description: 'approved or denied' },
131
+ },
132
+ handler: async (params) => {
133
+ const { requestId, approvedBy, rationale, decision } = params ?? {}
134
+ if (!requestId || !approvedBy) return { error: 'requestId and approvedBy are required' }
135
+ const entry = requestQueue.find((r) => r.id === requestId)
136
+ if (!entry) return { error: `request ${requestId} not found` }
137
+ if (entry.status !== 'pending') return { error: `request ${requestId} is already ${entry.status}` }
138
+
139
+ const approval = buildApprovalRecord(requestId, approvedBy, rationale ?? '', decision ?? 'approved')
140
+ entry.status = approval.decision === 'approved' ? 'approved' : 'denied'
141
+ entry.approvedBy = approvedBy
142
+ entry.approvedAt = approval.at
143
+ entry.rationale = rationale
144
+
145
+ log(`[request-router] ${requestId} ${approval.decision} by ${approvedBy}`)
146
+
147
+ // If approved, execute the request
148
+ if (approval.decision === 'approved') {
149
+ const cmd = buildRequestCommand(entry)
150
+ if (cmd) {
151
+ const output = await exec(cmd, { host: entry.target })
152
+ entry.executionOutput = output.slice(0, 1000)
153
+ entry.executedAt = Date.now()
154
+ }
155
+ }
156
+
157
+ return { requestId, ...entry, approval }
158
+ },
159
+ })
160
+
161
+ // Tool: list_requests — list requests in the queue
162
+ registerTool({
163
+ name: 'list_requests',
164
+ description: 'List requests in the queue with optional filters (status, risk, urgency, target).',
165
+ params: {
166
+ status: { type: 'string', description: 'Filter by status: pending, approved, denied, auto_approved' },
167
+ risk: { type: 'string', description: 'Filter by risk: low, medium, high' },
168
+ urgency: { type: 'string', description: 'Filter by urgency' },
169
+ target: { type: 'string', description: 'Filter by target (substring match)' },
170
+ },
171
+ handler: async (params) => {
172
+ const filtered = filterQueue(requestQueue, params ?? {})
173
+ return { total: filtered.length, requests: filtered }
174
+ },
175
+ })
176
+
177
+ // Tool: request_status — get the status of a specific request
178
+ registerTool({
179
+ name: 'request_status',
180
+ description: 'Get the status of a specific request by ID.',
181
+ params: { requestId: { type: 'string', description: 'Request ID' } },
182
+ handler: async (params) => {
183
+ const entry = requestQueue.find((r) => r.id === params?.requestId)
184
+ if (!entry) return { error: `request ${params?.requestId} not found` }
185
+ return entry
186
+ },
187
+ })
188
+
189
+ // Trigger: request_urgent — fires when a high-urgency request is submitted
190
+ registerTrigger({
191
+ name: 'request_urgent',
192
+ description: 'Fires when a request with critical/high urgency is submitted. Use for immediate notification.',
193
+ match: (event) => {
194
+ if (event?.source !== 'request-router') return false
195
+ return event.labels?.urgency === 'critical' || event.labels?.urgency === 'high'
196
+ },
197
+ action: 'run-playbook',
198
+ })
199
+
200
+ // Trigger: request_approved — fires when a request is approved
201
+ registerTrigger({
202
+ name: 'request_approved',
203
+ description: 'Fires when a request is approved. Use for post-approval automation.',
204
+ match: (event) => {
205
+ if (event?.source !== 'request-router') return false
206
+ return event.labels?.status === 'approved'
207
+ },
208
+ action: 'run-playbook',
209
+ })
210
+
211
+ // Panel: request-queue — request queue dashboard
212
+ registerPanel({
213
+ name: 'request-queue',
214
+ title: 'Request Queue',
215
+ render: (data) => {
216
+ const queue = Array.isArray(data) ? data : requestQueue
217
+ const rows = queue.map((r) =>
218
+ `<tr><td>${r.id}</td><td>${r.type}</td><td>${r.target}</td><td>${r.risk}</td><td>${r.urgency}</td><td>${r.status}</td><td>${r.submittedBy ?? ''}</td></tr>`
219
+ ).join('')
220
+ return `<div class="request-queue"><h3>Request Queue</h3><p>Total: ${queue.length} | Pending: ${queue.filter((r) => r.status === 'pending').length} | Approved: ${queue.filter((r) => r.status === 'approved').length}</p><table><thead><tr><th>ID</th><th>Type</th><th>Target</th><th>Risk</th><th>Urgency</th><th>Status</th><th>By</th></tr></thead><tbody>${rows}</tbody></table></div>`
221
+ },
222
+ })
223
+
224
+ log('[request-router] registered: 4 tools, 2 triggers, 1 panel')
225
+ }
226
+
227
+ // --- Pure: build the command to execute for an approved request ---
228
+ function buildRequestCommand(entry) {
229
+ const type = String(entry.type ?? '').toLowerCase()
230
+ const target = entry.target
231
+ if (type.includes('restart')) return `systemctl restart ${target} 2>/dev/null || Restart-Service ${target} -Force 2>/dev/null`
232
+ if (type.includes('stop')) return `systemctl stop ${target} 2>/dev/null || Stop-Service ${target} -Force 2>/dev/null`
233
+ if (type.includes('status')) return `systemctl status ${target} 2>/dev/null || Get-Service ${target} 2>/dev/null`
234
+ if (type.includes('deploy')) return `echo "deploying to ${target}"`
235
+ if (type.includes('patch')) return `echo "patching ${target}"`
236
+ return null // custom requests don't have a built-in command
237
+ }
238
+
239
+ export default { register, classifyRequest, routeRequest, buildRequestId, buildApprovalRecord, buildQueueEntry, filterQueue }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "request-router",
3
+ "version": "1.0.0",
4
+ "description": "Automated request handling and approval workflow for RTerm — receive operational requests, classify risk, route for approval (auto-approve/queue/MOP), execute end-to-end, and audit every step.",
5
+ "entry": "index.mjs",
6
+ "tools": ["submit_request", "approve_request", "list_requests", "request_status"],
7
+ "triggers": ["request_urgent", "request_approved"],
8
+ "panels": ["request-queue"],
9
+ "permissions": ["exec", "readLedger:incidents"]
10
+ }
@@ -0,0 +1,9 @@
1
+ // sop-assistant plugin type declarations
2
+ export function register(ctx: any): void
3
+ export function searchSops(query: string, sops?: any[]): Array<{ id: string; title: string; category: string; relevance: number; steps: number }>
4
+ export function getSop(id: string, sops?: any[]): any
5
+ export function searchIamPolicies(query: string, policies?: any[]): Array<{ id: string; title: string; category: string; relevance: number; rules: string[] }>
6
+ export function buildStepCommand(step: any, vars?: Record<string, any>): string
7
+ export const BUILTIN_SOPS: any[]
8
+ export const IAM_POLICIES: any[]
9
+ export default any