loki-mode 5.51.0 → 5.52.1
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/README.md +4 -56
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/hooks/validate-bash.sh +5 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/alternative-installations.md +3 -3
- package/docs/certification/01-core-concepts/lab.md +174 -0
- package/docs/certification/01-core-concepts/lesson.md +182 -0
- package/docs/certification/01-core-concepts/quiz.md +93 -0
- package/docs/certification/02-enterprise-features/lab.md +154 -0
- package/docs/certification/02-enterprise-features/lesson.md +202 -0
- package/docs/certification/02-enterprise-features/quiz.md +93 -0
- package/docs/certification/03-advanced-patterns/lab.md +138 -0
- package/docs/certification/03-advanced-patterns/lesson.md +199 -0
- package/docs/certification/03-advanced-patterns/quiz.md +93 -0
- package/docs/certification/04-production-deployment/lab.md +160 -0
- package/docs/certification/04-production-deployment/lesson.md +261 -0
- package/docs/certification/04-production-deployment/quiz.md +93 -0
- package/docs/certification/05-troubleshooting/lab.md +254 -0
- package/docs/certification/05-troubleshooting/lesson.md +266 -0
- package/docs/certification/05-troubleshooting/quiz.md +93 -0
- package/docs/certification/README.md +80 -0
- package/docs/certification/answer-key.md +117 -0
- package/docs/certification/certification-exam.md +471 -0
- package/docs/certification/sample-prds/microservices-platform.md +100 -0
- package/docs/certification/sample-prds/saas-dashboard.md +60 -0
- package/docs/certification/sample-prds/todo-app.md +44 -0
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +230 -0
- package/package.json +1 -1
- package/src/plugins/agent-plugin.js +123 -0
- package/src/plugins/gate-plugin.js +153 -0
- package/src/plugins/index.js +116 -0
- package/src/plugins/integration-plugin.js +174 -0
- package/src/plugins/loader.js +275 -0
- package/src/plugins/mcp-plugin.js +190 -0
- package/src/plugins/schemas/agent.json +59 -0
- package/src/plugins/schemas/integration.json +62 -0
- package/src/plugins/schemas/mcp_tool.json +73 -0
- package/src/plugins/schemas/quality_gate.json +52 -0
- package/src/plugins/validator.js +297 -0
- /package/dashboard/{secrets.py → app_secrets.py} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Quality Gate Plugin Schema",
|
|
4
|
+
"description": "Schema for custom quality gate plugins in Loki Mode",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["type", "name", "description", "command"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": {
|
|
9
|
+
"const": "quality_gate"
|
|
10
|
+
},
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]{2,49}$",
|
|
14
|
+
"description": "Unique gate name (lowercase, 3-50 chars, alphanumeric and hyphens)"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"maxLength": 500,
|
|
19
|
+
"description": "Human-readable description of the quality gate"
|
|
20
|
+
},
|
|
21
|
+
"phase": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["pre-commit", "post-commit", "pre-deploy", "post-deploy", "review"],
|
|
24
|
+
"default": "pre-commit",
|
|
25
|
+
"description": "SDLC phase when this gate runs"
|
|
26
|
+
},
|
|
27
|
+
"command": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"maxLength": 1000,
|
|
30
|
+
"description": "Command to execute (exit 0 = pass, non-zero = fail)"
|
|
31
|
+
},
|
|
32
|
+
"timeout_ms": {
|
|
33
|
+
"type": "integer",
|
|
34
|
+
"minimum": 1000,
|
|
35
|
+
"maximum": 300000,
|
|
36
|
+
"default": 30000,
|
|
37
|
+
"description": "Maximum execution time in milliseconds"
|
|
38
|
+
},
|
|
39
|
+
"blocking": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"default": true,
|
|
42
|
+
"description": "Whether failure blocks the pipeline"
|
|
43
|
+
},
|
|
44
|
+
"severity": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["critical", "high", "medium", "low"],
|
|
47
|
+
"default": "high",
|
|
48
|
+
"description": "Severity level of gate failure"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readFileSync } = require('fs');
|
|
4
|
+
const { join } = require('path');
|
|
5
|
+
|
|
6
|
+
// Built-in agent names that cannot be overridden by plugins
|
|
7
|
+
const BUILTIN_AGENT_NAMES = [
|
|
8
|
+
'eng-frontend', 'eng-backend', 'eng-database', 'eng-mobile',
|
|
9
|
+
'eng-api', 'eng-qa', 'eng-perf', 'eng-infra',
|
|
10
|
+
'ops-devops', 'ops-sre', 'ops-security', 'ops-monitor',
|
|
11
|
+
'ops-incident', 'ops-release', 'ops-cost', 'ops-compliance',
|
|
12
|
+
'biz-marketing', 'biz-sales', 'biz-finance', 'biz-legal',
|
|
13
|
+
'biz-support', 'biz-hr', 'biz-investor', 'biz-partnerships',
|
|
14
|
+
'data-ml', 'data-eng', 'data-analytics',
|
|
15
|
+
'prod-pm', 'prod-design', 'prod-techwriter',
|
|
16
|
+
'growth-hacker', 'growth-community', 'growth-success', 'growth-lifecycle',
|
|
17
|
+
'review-code', 'review-business', 'review-security',
|
|
18
|
+
'orch-planner', 'orch-sub-planner', 'orch-judge', 'orch-coordinator',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Dangerous shell metacharacters
|
|
22
|
+
const SHELL_INJECTION_PATTERN = /[|;&`<>]|\$\(|`.*`|\$\{(?!ENV_)|\n|\r/;
|
|
23
|
+
|
|
24
|
+
// Allowed template variable patterns
|
|
25
|
+
const ALLOWED_TEMPLATE_PATTERNS = [
|
|
26
|
+
/\{\{event\.\w+(\.\w+)*\}\}/, // {{event.type}}, {{event.data.field}}
|
|
27
|
+
/\$\{ENV_[A-Z_]+\}/, // ${ENV_VAR_NAME}
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Valid plugin types
|
|
31
|
+
const VALID_PLUGIN_TYPES = ['agent', 'quality_gate', 'integration', 'mcp_tool'];
|
|
32
|
+
|
|
33
|
+
// Schema file mapping
|
|
34
|
+
const SCHEMA_FILES = {
|
|
35
|
+
agent: 'agent.json',
|
|
36
|
+
quality_gate: 'quality_gate.json',
|
|
37
|
+
integration: 'integration.json',
|
|
38
|
+
mcp_tool: 'mcp_tool.json',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class PluginValidator {
|
|
42
|
+
/**
|
|
43
|
+
* Create a new PluginValidator.
|
|
44
|
+
* @param {string} schemasDir - Path to the schemas directory
|
|
45
|
+
*/
|
|
46
|
+
constructor(schemasDir) {
|
|
47
|
+
this.schemasDir = schemasDir || join(__dirname, 'schemas');
|
|
48
|
+
this._schemaCache = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load a JSON schema for a plugin type.
|
|
53
|
+
* @param {string} pluginType - The plugin type
|
|
54
|
+
* @returns {object|null} The parsed schema or null
|
|
55
|
+
*/
|
|
56
|
+
_loadSchema(pluginType) {
|
|
57
|
+
if (this._schemaCache[pluginType]) {
|
|
58
|
+
return this._schemaCache[pluginType];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const schemaFile = SCHEMA_FILES[pluginType];
|
|
62
|
+
if (!schemaFile) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const schemaPath = join(this.schemasDir, schemaFile);
|
|
68
|
+
const content = readFileSync(schemaPath, 'utf8');
|
|
69
|
+
const schema = JSON.parse(content);
|
|
70
|
+
this._schemaCache[pluginType] = schema;
|
|
71
|
+
return schema;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a plugin configuration.
|
|
79
|
+
* @param {object} pluginConfig - The plugin configuration to validate
|
|
80
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
validate(pluginConfig) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
|
|
85
|
+
// 1. Check that config is an object
|
|
86
|
+
if (!pluginConfig || typeof pluginConfig !== 'object' || Array.isArray(pluginConfig)) {
|
|
87
|
+
return { valid: false, errors: ['Plugin config must be a non-null object'] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Check required base fields
|
|
91
|
+
if (!pluginConfig.type) {
|
|
92
|
+
errors.push('Missing required field: type');
|
|
93
|
+
}
|
|
94
|
+
if (!pluginConfig.name) {
|
|
95
|
+
errors.push('Missing required field: name');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If we cannot determine type, return early
|
|
99
|
+
if (!pluginConfig.type) {
|
|
100
|
+
return { valid: false, errors };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Check plugin type is valid
|
|
104
|
+
if (!VALID_PLUGIN_TYPES.includes(pluginConfig.type)) {
|
|
105
|
+
errors.push(`Unknown plugin type: "${pluginConfig.type}". Valid types: ${VALID_PLUGIN_TYPES.join(', ')}`);
|
|
106
|
+
return { valid: false, errors };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Load and validate against schema
|
|
110
|
+
const schema = this._loadSchema(pluginConfig.type);
|
|
111
|
+
if (schema) {
|
|
112
|
+
const schemaErrors = this._validateAgainstSchema(pluginConfig, schema);
|
|
113
|
+
errors.push(...schemaErrors);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 5. Security checks
|
|
117
|
+
const securityErrors = this._securityChecks(pluginConfig);
|
|
118
|
+
errors.push(...securityErrors);
|
|
119
|
+
|
|
120
|
+
// 6. Built-in name collision check (for agents)
|
|
121
|
+
if (pluginConfig.type === 'agent' && pluginConfig.name) {
|
|
122
|
+
if (BUILTIN_AGENT_NAMES.includes(pluginConfig.name)) {
|
|
123
|
+
errors.push(`Name "${pluginConfig.name}" conflicts with a built-in agent type. Custom agents must use unique names.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { valid: errors.length === 0, errors };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate config against a JSON schema (simplified validator).
|
|
132
|
+
* @param {object} config - The config to validate
|
|
133
|
+
* @param {object} schema - The JSON schema
|
|
134
|
+
* @returns {string[]} List of validation errors
|
|
135
|
+
*/
|
|
136
|
+
_validateAgainstSchema(config, schema) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
|
|
139
|
+
// Check required fields
|
|
140
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
141
|
+
for (const field of schema.required) {
|
|
142
|
+
if (config[field] === undefined || config[field] === null || config[field] === '') {
|
|
143
|
+
errors.push(`Missing required field: ${field}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check property types and constraints
|
|
149
|
+
if (schema.properties) {
|
|
150
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
151
|
+
const value = config[key];
|
|
152
|
+
if (value === undefined || value === null) {
|
|
153
|
+
continue; // Skip if not present (required check above handles that)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Type check
|
|
157
|
+
if (propSchema.type) {
|
|
158
|
+
const typeValid = this._checkType(value, propSchema.type);
|
|
159
|
+
if (!typeValid) {
|
|
160
|
+
errors.push(`Field "${key}" must be of type ${propSchema.type}, got ${typeof value}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Const check
|
|
166
|
+
if (propSchema.const !== undefined && value !== propSchema.const) {
|
|
167
|
+
errors.push(`Field "${key}" must be "${propSchema.const}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Enum check
|
|
171
|
+
if (propSchema.enum && !propSchema.enum.includes(value)) {
|
|
172
|
+
errors.push(`Field "${key}" must be one of: ${propSchema.enum.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Pattern check (string)
|
|
176
|
+
if (propSchema.pattern && typeof value === 'string') {
|
|
177
|
+
const regex = new RegExp(propSchema.pattern);
|
|
178
|
+
if (!regex.test(value)) {
|
|
179
|
+
errors.push(`Field "${key}" does not match pattern ${propSchema.pattern}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// MaxLength check (string)
|
|
184
|
+
if (propSchema.maxLength !== undefined && typeof value === 'string') {
|
|
185
|
+
if (value.length > propSchema.maxLength) {
|
|
186
|
+
errors.push(`Field "${key}" exceeds maximum length of ${propSchema.maxLength} (got ${value.length})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// MinItems check (array)
|
|
191
|
+
if (propSchema.minItems !== undefined && Array.isArray(value)) {
|
|
192
|
+
if (value.length < propSchema.minItems) {
|
|
193
|
+
errors.push(`Field "${key}" must have at least ${propSchema.minItems} items`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Minimum check (integer/number)
|
|
198
|
+
if (propSchema.minimum !== undefined && typeof value === 'number') {
|
|
199
|
+
if (value < propSchema.minimum) {
|
|
200
|
+
errors.push(`Field "${key}" must be >= ${propSchema.minimum}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Maximum check (integer/number)
|
|
205
|
+
if (propSchema.maximum !== undefined && typeof value === 'number') {
|
|
206
|
+
if (value > propSchema.maximum) {
|
|
207
|
+
errors.push(`Field "${key}" must be <= ${propSchema.maximum}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for additional properties (if additionalProperties is false)
|
|
213
|
+
if (schema.additionalProperties === false) {
|
|
214
|
+
const allowedKeys = Object.keys(schema.properties);
|
|
215
|
+
for (const key of Object.keys(config)) {
|
|
216
|
+
if (!allowedKeys.includes(key)) {
|
|
217
|
+
errors.push(`Unknown field: "${key}" is not allowed`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return errors;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if a value matches a JSON schema type.
|
|
228
|
+
* @param {*} value - The value to check
|
|
229
|
+
* @param {string} type - The expected type
|
|
230
|
+
* @returns {boolean}
|
|
231
|
+
*/
|
|
232
|
+
_checkType(value, type) {
|
|
233
|
+
switch (type) {
|
|
234
|
+
case 'string':
|
|
235
|
+
return typeof value === 'string';
|
|
236
|
+
case 'number':
|
|
237
|
+
case 'integer':
|
|
238
|
+
return typeof value === 'number' && (type !== 'integer' || Number.isInteger(value));
|
|
239
|
+
case 'boolean':
|
|
240
|
+
return typeof value === 'boolean';
|
|
241
|
+
case 'array':
|
|
242
|
+
return Array.isArray(value);
|
|
243
|
+
case 'object':
|
|
244
|
+
return typeof value === 'object' && !Array.isArray(value) && value !== null;
|
|
245
|
+
default:
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Run security checks on a plugin config.
|
|
252
|
+
* @param {object} config - The plugin configuration
|
|
253
|
+
* @returns {string[]} List of security errors
|
|
254
|
+
*/
|
|
255
|
+
_securityChecks(config) {
|
|
256
|
+
const errors = [];
|
|
257
|
+
|
|
258
|
+
// Check command fields for shell injection
|
|
259
|
+
const commandFields = ['command'];
|
|
260
|
+
for (const field of commandFields) {
|
|
261
|
+
if (typeof config[field] === 'string') {
|
|
262
|
+
if (SHELL_INJECTION_PATTERN.test(config[field])) {
|
|
263
|
+
errors.push(`Security: field "${field}" contains potentially dangerous shell metacharacters (|, ;, &, $(), backticks). Use simple commands only.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check prompt_template for size and suspicious patterns
|
|
269
|
+
if (typeof config.prompt_template === 'string') {
|
|
270
|
+
if (config.prompt_template.length > 10000) {
|
|
271
|
+
errors.push(`Field "prompt_template" exceeds maximum length of 10000`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check payload_template for injection
|
|
276
|
+
if (typeof config.payload_template === 'string') {
|
|
277
|
+
// Scan for template variables that are not in the allowed list
|
|
278
|
+
const templateVarPattern = /\{\{(?!event\.)[^}]+\}\}/g;
|
|
279
|
+
const disallowed = config.payload_template.match(templateVarPattern);
|
|
280
|
+
if (disallowed && disallowed.length > 0) {
|
|
281
|
+
errors.push(`Security: payload_template contains disallowed template variables: ${disallowed.join(', ')}. Only {{event.*}} patterns are allowed.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check webhook_url is HTTPS or localhost
|
|
286
|
+
if (typeof config.webhook_url === 'string') {
|
|
287
|
+
const url = config.webhook_url.toLowerCase();
|
|
288
|
+
if (!url.startsWith('https://') && !url.startsWith('http://localhost') && !url.startsWith('http://127.0.0.1')) {
|
|
289
|
+
errors.push('Security: webhook_url must use HTTPS or localhost');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return errors;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { PluginValidator, BUILTIN_AGENT_NAMES, VALID_PLUGIN_TYPES };
|
|
File without changes
|