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.
Files changed (44) hide show
  1. package/README.md +4 -56
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/hooks/validate-bash.sh +5 -2
  5. package/dashboard/__init__.py +1 -1
  6. package/dashboard/server.py +1 -1
  7. package/docs/INSTALLATION.md +1 -1
  8. package/docs/alternative-installations.md +3 -3
  9. package/docs/certification/01-core-concepts/lab.md +174 -0
  10. package/docs/certification/01-core-concepts/lesson.md +182 -0
  11. package/docs/certification/01-core-concepts/quiz.md +93 -0
  12. package/docs/certification/02-enterprise-features/lab.md +154 -0
  13. package/docs/certification/02-enterprise-features/lesson.md +202 -0
  14. package/docs/certification/02-enterprise-features/quiz.md +93 -0
  15. package/docs/certification/03-advanced-patterns/lab.md +138 -0
  16. package/docs/certification/03-advanced-patterns/lesson.md +199 -0
  17. package/docs/certification/03-advanced-patterns/quiz.md +93 -0
  18. package/docs/certification/04-production-deployment/lab.md +160 -0
  19. package/docs/certification/04-production-deployment/lesson.md +261 -0
  20. package/docs/certification/04-production-deployment/quiz.md +93 -0
  21. package/docs/certification/05-troubleshooting/lab.md +254 -0
  22. package/docs/certification/05-troubleshooting/lesson.md +266 -0
  23. package/docs/certification/05-troubleshooting/quiz.md +93 -0
  24. package/docs/certification/README.md +80 -0
  25. package/docs/certification/answer-key.md +117 -0
  26. package/docs/certification/certification-exam.md +471 -0
  27. package/docs/certification/sample-prds/microservices-platform.md +100 -0
  28. package/docs/certification/sample-prds/saas-dashboard.md +60 -0
  29. package/docs/certification/sample-prds/todo-app.md +44 -0
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/server.py +230 -0
  32. package/package.json +1 -1
  33. package/src/plugins/agent-plugin.js +123 -0
  34. package/src/plugins/gate-plugin.js +153 -0
  35. package/src/plugins/index.js +116 -0
  36. package/src/plugins/integration-plugin.js +174 -0
  37. package/src/plugins/loader.js +275 -0
  38. package/src/plugins/mcp-plugin.js +190 -0
  39. package/src/plugins/schemas/agent.json +59 -0
  40. package/src/plugins/schemas/integration.json +62 -0
  41. package/src/plugins/schemas/mcp_tool.json +73 -0
  42. package/src/plugins/schemas/quality_gate.json +52 -0
  43. package/src/plugins/validator.js +297 -0
  44. /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