rterm-backend 2.7.3 → 2.7.4
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/bin/gybackend.js +12 -1
- package/package.json +2 -1
- package/plugins/fraudops/index.d.mts +9 -0
- package/plugins/fraudops/index.mjs +184 -0
- package/plugins/fraudops/plugin.json +10 -0
- package/plugins/iam-connector/index.d.mts +10 -0
- package/plugins/iam-connector/index.mjs +225 -0
- package/plugins/iam-connector/plugin.json +10 -0
- package/plugins/netdata-rterm/index.mjs +229 -0
- package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +252 -0
- package/plugins/netdata-rterm/plugin.json +22 -0
- package/plugins/patch-manager/index.d.mts +10 -0
- package/plugins/patch-manager/index.mjs +288 -0
- package/plugins/patch-manager/plugin.json +10 -0
- package/plugins/request-router/index.d.mts +9 -0
- package/plugins/request-router/index.mjs +235 -0
- package/plugins/request-router/plugin.json +10 -0
- package/plugins/sop-assistant/index.d.mts +9 -0
- package/plugins/sop-assistant/index.mjs +325 -0
- package/plugins/sop-assistant/plugin.json +10 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sop-assistant plugin — IAM Knowledge & SOP Assistant.
|
|
3
|
+
*
|
|
4
|
+
* SOP retrieval with keyword search, step-by-step guided flows, and IAM policy
|
|
5
|
+
* lookup. Answers "how do I X?" with relevant SOPs and executes them with
|
|
6
|
+
* user confirmation. Built-in SOP library for common ops tasks + extensible
|
|
7
|
+
* via custom SOP registration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- Pure: the built-in SOP library ---
|
|
11
|
+
export const BUILTIN_SOPS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'restart-service',
|
|
14
|
+
title: 'Restart a Service',
|
|
15
|
+
category: 'operations',
|
|
16
|
+
keywords: ['restart', 'service', 'systemctl', 'reboot', 'reload'],
|
|
17
|
+
steps: [
|
|
18
|
+
{ step: 1, action: 'Check service status', command: 'systemctl status {service}', verify: 'service is running or stopped' },
|
|
19
|
+
{ step: 2, action: 'Restart the service', command: 'systemctl restart {service}', verify: 'command succeeds' },
|
|
20
|
+
{ step: 3, action: 'Verify service is running', command: 'systemctl status {service}', verify: 'active (running)' },
|
|
21
|
+
{ step: 4, action: 'Check recent logs', command: 'journalctl -u {service} -n 20 --no-pager', verify: 'no errors in logs' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'disk-cleanup',
|
|
26
|
+
title: 'Disk Space Cleanup',
|
|
27
|
+
category: 'operations',
|
|
28
|
+
keywords: ['disk', 'space', 'cleanup', 'full', 'storage', 'df'],
|
|
29
|
+
steps: [
|
|
30
|
+
{ step: 1, action: 'Check disk usage', command: 'df -h /', verify: 'identify full filesystem' },
|
|
31
|
+
{ step: 2, action: 'Find large files', command: 'find / -type f -size +100M -exec ls -lh {} \\; 2>/dev/null | head -20', verify: 'identify large files' },
|
|
32
|
+
{ step: 3, action: 'Clean package cache', command: 'yum clean all 2>/dev/null || apt-get clean 2>/dev/null', verify: 'cache cleaned' },
|
|
33
|
+
{ step: 4, action: 'Clean old logs', command: 'journalctl --vacuum-time=7d 2>/dev/null; find /var/log -name "*.gz" -mtime +30 -delete 2>/dev/null', verify: 'old logs removed' },
|
|
34
|
+
{ step: 5, action: 'Verify disk space recovered', command: 'df -h /', verify: 'usage below 80%' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'reset-password',
|
|
39
|
+
title: 'Reset User Password',
|
|
40
|
+
category: 'iam',
|
|
41
|
+
keywords: ['password', 'reset', 'user', 'account', 'passwd', 'login'],
|
|
42
|
+
steps: [
|
|
43
|
+
{ step: 1, action: 'Verify user exists', command: 'id {username} 2>/dev/null || Get-LocalUser {username} 2>/dev/null', verify: 'user found' },
|
|
44
|
+
{ step: 2, action: 'Reset password', command: 'passwd {username} 2>/dev/null || Set-LocalUser -Name {username} -Password (ConvertTo-SecureString -AsPlainText "{new_password}" -Force) 2>/dev/null', verify: 'password changed' },
|
|
45
|
+
{ step: 3, action: 'Force password change at next login', command: 'passwd -e {username} 2>/dev/null || Set-LocalUser -Name {username} -PasswordNeverExpires:$false 2>/dev/null', verify: 'flag set' },
|
|
46
|
+
{ step: 4, action: 'Verify account is not locked', command: 'passwd -S {username} 2>/dev/null || Get-LocalUser {username} | Select-Object LockedOut 2>/dev/null', verify: 'not locked' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'database-failover',
|
|
51
|
+
title: 'Database Failover Procedure',
|
|
52
|
+
category: 'operations',
|
|
53
|
+
keywords: ['database', 'failover', 'mysql', 'postgres', 'replica', 'primary', 'switchover'],
|
|
54
|
+
steps: [
|
|
55
|
+
{ step: 1, action: 'Verify replica is healthy', command: 'SHOW REPLICA STATUS 2>/dev/null || SELECT * FROM pg_stat_replication 2>/dev/null', verify: 'replication is running' },
|
|
56
|
+
{ step: 2, action: 'Stop writes to primary', command: 'SET GLOBAL read_only = ON 2>/dev/null || ALTER SYSTEM SET default_transaction_read_only = on 2>/dev/null', verify: 'read-only mode' },
|
|
57
|
+
{ step: 3, action: 'Wait for replica to catch up', command: 'SELECT MASTER_POS_WAIT(MASTER_LOG_FILE, MASTER_LOG_POS) 2>/dev/null || SELECT pg_last_wal_replay_lsn() 2>/dev/null', verify: 'caught up' },
|
|
58
|
+
{ step: 4, action: 'Promote replica to primary', command: 'STOP REPLICA 2>/dev/null || SELECT pg_promote() 2>/dev/null', verify: 'promoted' },
|
|
59
|
+
{ step: 5, action: 'Update DNS/load balancer', command: 'echo "update DNS to point to new primary"', verify: 'DNS updated' },
|
|
60
|
+
{ step: 6, action: 'Verify application connectivity', command: 'echo "verify app connects to new primary"', verify: 'app connected' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'ssl-cert-renewal',
|
|
65
|
+
title: 'SSL Certificate Renewal',
|
|
66
|
+
category: 'security',
|
|
67
|
+
keywords: ['ssl', 'certificate', 'cert', 'renew', 'letsencrypt', 'tls', 'https'],
|
|
68
|
+
steps: [
|
|
69
|
+
{ step: 1, action: 'Check certificate expiry', command: 'openssl x509 -enddate -noout -in /etc/ssl/certs/{domain}.crt 2>/dev/null', verify: 'check days remaining' },
|
|
70
|
+
{ step: 2, action: 'Renew certificate', command: 'certbot renew --dry-run 2>/dev/null || echo "manual renewal needed"', verify: 'renewal works' },
|
|
71
|
+
{ step: 3, action: 'Apply new certificate', command: 'certbot renew --force-renewal 2>/dev/null || echo "apply manually"', verify: 'certificate renewed' },
|
|
72
|
+
{ step: 4, action: 'Reload web server', command: 'systemctl reload nginx 2>/dev/null || systemctl reload apache2 2>/dev/null || Restart-Service W3SVC 2>/dev/null', verify: 'reloaded' },
|
|
73
|
+
{ step: 5, action: 'Verify new certificate', command: 'openssl x509 -enddate -noout -in /etc/ssl/certs/{domain}.crt 2>/dev/null', verify: 'new expiry date' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'user-offboarding',
|
|
78
|
+
title: 'User Offboarding',
|
|
79
|
+
category: 'iam',
|
|
80
|
+
keywords: ['offboard', 'disable', 'deactivate', 'remove user', 'terminate', 'leave'],
|
|
81
|
+
steps: [
|
|
82
|
+
{ step: 1, action: 'Disable user account', command: 'usermod -L {username} 2>/dev/null || Disable-LocalUser -Name {username} 2>/dev/null', verify: 'account disabled' },
|
|
83
|
+
{ step: 2, action: 'Kill active sessions', command: 'pkill -u {username} 2>/dev/null || echo "check active sessions"', verify: 'sessions terminated' },
|
|
84
|
+
{ step: 3, action: 'Remove from groups', command: 'gpasswd -d {username} sudo 2>/dev/null || Remove-LocalGroupMember -Group Administrators -Member {username} 2>/dev/null', verify: 'removed from privileged groups' },
|
|
85
|
+
{ step: 4, action: 'Archive home directory', command: 'tar -czf /backup/{username}-home-$(date +%Y%m%d).tar.gz /home/{username} 2>/dev/null || Compress-Archive -Path C:\\Users\\{username} -DestinationPath C:\\Backup\\{username}.zip 2>/dev/null', verify: 'archived' },
|
|
86
|
+
{ step: 5, action: 'Disable SSH keys', command: 'mv /home/{username}/.ssh/authorized_keys /home/{username}/.ssh/authorized_keys.disabled 2>/dev/null || echo "no SSH keys"', verify: 'SSH keys disabled' },
|
|
87
|
+
{ step: 6, action: 'Verify no active processes', command: 'ps -u {username} 2>/dev/null || Get-Process -IncludeUserName | Where-Object {$_.UserName -like "*{username}*"} 2>/dev/null', verify: 'no processes' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'backup-restore',
|
|
92
|
+
title: 'Database Backup and Restore',
|
|
93
|
+
category: 'operations',
|
|
94
|
+
keywords: ['backup', 'restore', 'dump', 'mysqldump', 'pg_dump', 'database backup'],
|
|
95
|
+
steps: [
|
|
96
|
+
{ step: 1, action: 'Create backup directory', command: 'mkdir -p /backup/$(date +%Y%m%d)', verify: 'directory exists' },
|
|
97
|
+
{ step: 2, action: 'Backup database', command: 'mysqldump -u root --all-databases > /backup/$(date +%Y%m%d)/all-databases.sql 2>/dev/null || pg_dumpall > /backup/$(date +%Y%m%d)/all-databases.sql 2>/dev/null', verify: 'backup created' },
|
|
98
|
+
{ step: 3, action: 'Compress backup', command: 'gzip /backup/$(date +%Y%m%d)/all-databases.sql', verify: 'compressed' },
|
|
99
|
+
{ step: 4, action: 'Verify backup integrity', command: 'gunzip -t /backup/$(date +%Y%m%d)/all-databases.sql.gz', verify: 'integrity OK' },
|
|
100
|
+
{ step: 5, action: 'Clean old backups', command: 'find /backup -name "*.sql.gz" -mtime +30 -delete', verify: 'old backups removed' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'incident-response',
|
|
105
|
+
title: 'Incident Response Procedure',
|
|
106
|
+
category: 'security',
|
|
107
|
+
keywords: ['incident', 'response', 'breach', 'security', 'compromise', 'alert'],
|
|
108
|
+
steps: [
|
|
109
|
+
{ step: 1, action: 'Identify affected systems', command: 'echo "identify scope of incident"', verify: 'scope identified' },
|
|
110
|
+
{ step: 2, action: 'Isolate affected systems', command: 'echo "isolate from network if needed"', verify: 'isolated' },
|
|
111
|
+
{ step: 3, action: 'Collect evidence', command: 'echo "preserve logs, memory dumps, network captures"', verify: 'evidence preserved' },
|
|
112
|
+
{ step: 4, action: 'Assess damage', command: 'echo "determine what was accessed/modified"', verify: 'damage assessed' },
|
|
113
|
+
{ step: 5, action: 'Contain and eradicate', command: 'echo "remove attacker access, patch vulnerabilities"', verify: 'contained' },
|
|
114
|
+
{ step: 6, action: 'Recover systems', command: 'echo "restore from clean backup, verify integrity"', verify: 'recovered' },
|
|
115
|
+
{ step: 7, action: 'Post-incident review', command: 'echo "document lessons learned, update procedures"', verify: 'documented' },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// --- Pure: the built-in IAM policy library ---
|
|
121
|
+
export const IAM_POLICIES = [
|
|
122
|
+
{
|
|
123
|
+
id: 'password-policy',
|
|
124
|
+
title: 'Password Policy',
|
|
125
|
+
category: 'iam',
|
|
126
|
+
rules: [
|
|
127
|
+
'Minimum 12 characters',
|
|
128
|
+
'Require uppercase, lowercase, number, special character',
|
|
129
|
+
'Maximum 90 days before forced change',
|
|
130
|
+
'No reuse of last 5 passwords',
|
|
131
|
+
'Account lockout after 5 failed attempts (15 min lockout)',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'access-control',
|
|
136
|
+
title: 'Access Control Policy',
|
|
137
|
+
category: 'iam',
|
|
138
|
+
rules: [
|
|
139
|
+
'Least privilege: users get minimum permissions needed',
|
|
140
|
+
'Role-based access control (RBAC) for all systems',
|
|
141
|
+
'Privileged access requires MFA',
|
|
142
|
+
'Service accounts must have named owners',
|
|
143
|
+
'Access reviews quarterly',
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'ssh-access',
|
|
148
|
+
title: 'SSH Access Policy',
|
|
149
|
+
category: 'iam',
|
|
150
|
+
rules: [
|
|
151
|
+
'SSH key-based authentication only (no password auth)',
|
|
152
|
+
'Root login disabled',
|
|
153
|
+
'SSH on non-standard port (not 22)',
|
|
154
|
+
'Fail2ban enabled for brute-force protection',
|
|
155
|
+
'SSH keys rotated annually',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'service-account',
|
|
160
|
+
title: 'Service Account Policy',
|
|
161
|
+
category: 'iam',
|
|
162
|
+
rules: [
|
|
163
|
+
'Service accounts must have named owners',
|
|
164
|
+
'No interactive login for service accounts',
|
|
165
|
+
'Service account credentials rotated quarterly',
|
|
166
|
+
'Least privilege for service accounts',
|
|
167
|
+
'Audit logging for all service account activity',
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
// --- Pure: search SOPs by keyword ---
|
|
173
|
+
export function searchSops(query, sops = BUILTIN_SOPS) {
|
|
174
|
+
const q = String(query ?? '').toLowerCase()
|
|
175
|
+
if (!q) return []
|
|
176
|
+
const words = q.split(/\s+/)
|
|
177
|
+
return sops
|
|
178
|
+
.map((sop) => {
|
|
179
|
+
let score = 0
|
|
180
|
+
const text = `${sop.title} ${sop.category} ${sop.keywords.join(' ')}`.toLowerCase()
|
|
181
|
+
for (const word of words) {
|
|
182
|
+
if (text.includes(word)) score += 1
|
|
183
|
+
if (sop.keywords.some((k) => k.includes(word))) score += 2
|
|
184
|
+
}
|
|
185
|
+
return { sop, score }
|
|
186
|
+
})
|
|
187
|
+
.filter((r) => r.score > 0)
|
|
188
|
+
.sort((a, b) => b.score - a.score)
|
|
189
|
+
.map((r) => ({ id: r.sop.id, title: r.sop.title, category: r.sop.category, relevance: r.score, steps: r.sop.steps.length }))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Pure: get a SOP by ID ---
|
|
193
|
+
export function getSop(id, sops = BUILTIN_SOPS) {
|
|
194
|
+
return sops.find((s) => s.id === id) ?? null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Pure: search IAM policies ---
|
|
198
|
+
export function searchIamPolicies(query, policies = IAM_POLICIES) {
|
|
199
|
+
const q = String(query ?? '').toLowerCase()
|
|
200
|
+
if (!q) return []
|
|
201
|
+
return policies
|
|
202
|
+
.map((policy) => {
|
|
203
|
+
let score = 0
|
|
204
|
+
const text = `${policy.title} ${policy.category} ${policy.rules.join(' ')}`.toLowerCase()
|
|
205
|
+
for (const word of q.split(/\s+/)) {
|
|
206
|
+
if (text.includes(word)) score += 1
|
|
207
|
+
}
|
|
208
|
+
return { policy, score }
|
|
209
|
+
})
|
|
210
|
+
.filter((r) => r.score > 0)
|
|
211
|
+
.sort((a, b) => b.score - a.score)
|
|
212
|
+
.map((r) => ({ id: r.policy.id, title: r.policy.title, category: r.policy.category, relevance: r.score, rules: r.policy.rules }))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Pure: build the command for a SOP step (with variable substitution) ---
|
|
216
|
+
export function buildStepCommand(step, vars = {}) {
|
|
217
|
+
let cmd = step.command
|
|
218
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
219
|
+
cmd = cmd.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value))
|
|
220
|
+
}
|
|
221
|
+
return cmd
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- Plugin entry ---
|
|
225
|
+
export function register(ctx) {
|
|
226
|
+
const { registerTool, registerTrigger, registerPanel, exec, log } = ctx
|
|
227
|
+
const customSops = []
|
|
228
|
+
|
|
229
|
+
// Tool: sop_search — search SOPs by natural language query
|
|
230
|
+
registerTool({
|
|
231
|
+
name: 'sop_search',
|
|
232
|
+
description: 'Search SOPs, runbooks, and procedures by natural language query. Returns matching SOPs with step counts.',
|
|
233
|
+
params: { query: { type: 'string', description: 'Natural language query (e.g., "how to restart a service", "database failover procedure")' } },
|
|
234
|
+
handler: async (params) => {
|
|
235
|
+
const results = searchSops(params?.query ?? '', [...BUILTIN_SOPS, ...customSops])
|
|
236
|
+
log(`[sop-assistant] search "${params?.query}": ${results.length} results`)
|
|
237
|
+
return { query: params?.query, results }
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Tool: sop_get — get a specific SOP by ID
|
|
242
|
+
registerTool({
|
|
243
|
+
name: 'sop_get',
|
|
244
|
+
description: 'Get a specific SOP by ID. Returns the full SOP with all steps.',
|
|
245
|
+
params: { id: { type: 'string', description: 'SOP ID (e.g., "restart-service", "database-failover")' } },
|
|
246
|
+
handler: async (params) => {
|
|
247
|
+
const sop = getSop(params?.id ?? '', [...BUILTIN_SOPS, ...customSops])
|
|
248
|
+
if (!sop) return { error: `SOP ${params?.id} not found` }
|
|
249
|
+
return sop
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Tool: sop_execute — execute a SOP step-by-step
|
|
254
|
+
registerTool({
|
|
255
|
+
name: 'sop_execute',
|
|
256
|
+
description: 'Execute a SOP step-by-step with variable substitution. Each step is executed and verified before proceeding.',
|
|
257
|
+
params: {
|
|
258
|
+
id: { type: 'string', description: 'SOP ID to execute' },
|
|
259
|
+
vars: { type: 'object', description: 'Variables to substitute (e.g., {"service": "nginx", "username": "john"})' },
|
|
260
|
+
startStep: { type: 'number', description: 'Step to start from (default 1)' },
|
|
261
|
+
dryRun: { type: 'boolean', description: 'If true, show commands without executing' },
|
|
262
|
+
},
|
|
263
|
+
handler: async (params) => {
|
|
264
|
+
const sop = getSop(params?.id ?? '', [...BUILTIN_SOPS, ...customSops])
|
|
265
|
+
if (!sop) return { error: `SOP ${params?.id} not found` }
|
|
266
|
+
const vars = params?.vars ?? {}
|
|
267
|
+
const startStep = params?.startStep ?? 1
|
|
268
|
+
const dryRun = params?.dryRun ?? false
|
|
269
|
+
|
|
270
|
+
const results = []
|
|
271
|
+
for (const step of sop.steps) {
|
|
272
|
+
if (step.step < startStep) continue
|
|
273
|
+
const cmd = buildStepCommand(step, vars)
|
|
274
|
+
if (dryRun) {
|
|
275
|
+
results.push({ step: step.step, action: step.action, command: cmd, status: 'dry_run' })
|
|
276
|
+
} else {
|
|
277
|
+
log(`[sop-assistant] executing step ${step.step}: ${step.action}`)
|
|
278
|
+
const output = await exec(cmd, {})
|
|
279
|
+
results.push({ step: step.step, action: step.action, command: cmd, output: output.slice(0, 500), status: 'executed' })
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { sopId: sop.id, title: sop.title, dryRun, results }
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// Tool: iam_lookup — search IAM policies
|
|
287
|
+
registerTool({
|
|
288
|
+
name: 'iam_lookup',
|
|
289
|
+
description: 'Search IAM policies by keyword. Returns matching policies with rules.',
|
|
290
|
+
params: { query: { type: 'string', description: 'Search query (e.g., "password policy", "SSH access", "service account")' } },
|
|
291
|
+
handler: async (params) => {
|
|
292
|
+
const results = searchIamPolicies(params?.query ?? '')
|
|
293
|
+
log(`[sop-assistant] IAM lookup "${params?.query}": ${results.length} policies`)
|
|
294
|
+
return { query: params?.query, results }
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Trigger: sop_escalation — fires when a SOP execution fails
|
|
299
|
+
registerTrigger({
|
|
300
|
+
name: 'sop_escalation',
|
|
301
|
+
description: 'Fires when a SOP execution step fails. Use for escalation to senior operator.',
|
|
302
|
+
match: (event) => {
|
|
303
|
+
if (event?.source !== 'sop-assistant') return false
|
|
304
|
+
return event.labels?.status === 'failed'
|
|
305
|
+
},
|
|
306
|
+
action: 'propose-change',
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Panel: sop-library — SOP library browser
|
|
310
|
+
registerPanel({
|
|
311
|
+
name: 'sop-library',
|
|
312
|
+
title: 'SOP Library',
|
|
313
|
+
render: (data) => {
|
|
314
|
+
const sops = [...BUILTIN_SOPS, ...customSops]
|
|
315
|
+
const rows = sops.map((s) =>
|
|
316
|
+
`<tr><td>${s.id}</td><td>${s.title}</td><td>${s.category}</td><td>${s.steps.length}</td></tr>`
|
|
317
|
+
).join('')
|
|
318
|
+
return `<div class="sop-library"><h3>SOP Library</h3><p>${sops.length} SOPs available</p><table><thead><tr><th>ID</th><th>Title</th><th>Category</th><th>Steps</th></tr></thead><tbody>${rows}</tbody></table></div>`
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
log('[sop-assistant] registered: 4 tools, 1 trigger, 1 panel')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export default { register, searchSops, getSop, searchIamPolicies, buildStepCommand, BUILTIN_SOPS, IAM_POLICIES }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sop-assistant",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "IAM Knowledge & SOP Assistant for RTerm — SOP retrieval with keyword search, step-by-step guided flows, and IAM policy lookup. Answers 'how do I X?' with relevant SOPs and executes them with user confirmation.",
|
|
5
|
+
"entry": "index.mjs",
|
|
6
|
+
"tools": ["sop_search", "sop_get", "sop_execute", "iam_lookup"],
|
|
7
|
+
"triggers": ["sop_escalation"],
|
|
8
|
+
"panels": ["sop-library"],
|
|
9
|
+
"permissions": ["exec"]
|
|
10
|
+
}
|