prepia 1.0.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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/prepia.mjs +119 -0
- package/package.json +53 -0
- package/skill/SKILL.md +148 -0
- package/skill/config.json +29 -0
- package/src/analytics/dashboard.mjs +84 -0
- package/src/analytics/tracker.mjs +131 -0
- package/src/api/middleware.mjs +219 -0
- package/src/api/routes.mjs +142 -0
- package/src/api/server.mjs +150 -0
- package/src/cache/disk-store.mjs +199 -0
- package/src/cache/manager.mjs +142 -0
- package/src/cache/memory-store.mjs +205 -0
- package/src/chain/dag.mjs +209 -0
- package/src/chain/executor.mjs +103 -0
- package/src/chain/scheduler.mjs +89 -0
- package/src/client/adapters.mjs +483 -0
- package/src/client/connector.mjs +391 -0
- package/src/client/index.mjs +483 -0
- package/src/client/websocket.mjs +353 -0
- package/src/core/context-packager.mjs +169 -0
- package/src/core/engine.mjs +338 -0
- package/src/core/event-bus.mjs +84 -0
- package/src/core/prepimshot.mjs +120 -0
- package/src/core/task-decomposer.mjs +158 -0
- package/src/edge/lite.mjs +90 -0
- package/src/guard/checker.mjs +123 -0
- package/src/guard/fact-checker.mjs +105 -0
- package/src/guard/hallucination.mjs +108 -0
- package/src/index.mjs +67 -0
- package/src/models/local-model.mjs +171 -0
- package/src/models/provider.mjs +192 -0
- package/src/models/router.mjs +156 -0
- package/src/morph/optimizer.mjs +142 -0
- package/src/network/p2p.mjs +146 -0
- package/src/persona/detector.mjs +118 -0
- package/src/plugins/loader.mjs +120 -0
- package/src/plugins/registry.mjs +164 -0
- package/src/plugins/sandbox.mjs +79 -0
- package/src/rate/limiter.mjs +145 -0
- package/src/rate/shield.mjs +150 -0
- package/src/script/executor.mjs +164 -0
- package/src/script/parser.mjs +134 -0
- package/src/security/privacy.mjs +108 -0
- package/src/security/sanitizer.mjs +133 -0
- package/src/shadow/daemon.mjs +128 -0
- package/src/stream/handler.mjs +204 -0
- package/src/tools/calculator.mjs +312 -0
- package/src/tools/file-ops.mjs +138 -0
- package/src/tools/http-client.mjs +127 -0
- package/src/tools/orchestrator.mjs +205 -0
- package/src/tools/web-scraper.mjs +159 -0
- package/src/tools/web-search.mjs +129 -0
- package/src/vault/knowledge-base.mjs +207 -0
- package/src/vault/pattern-learner.mjs +192 -0
- package/workflows/analyze.json +32 -0
- package/workflows/automate.json +32 -0
- package/workflows/research.json +37 -0
- package/workflows/summarize.json +32 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate limit monitoring and protection.
|
|
3
|
+
* @module rate/shield
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { TokenBucket, SlidingWindow } from './limiter.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} ProviderLimits
|
|
11
|
+
* @property {number} [requestsPerMinute] - RPM limit
|
|
12
|
+
* @property {number} [tokensPerMinute] - TPM limit
|
|
13
|
+
* @property {number} [requestsPerDay] - Daily request limit
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class RateShield extends EventEmitter {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {Object<string, ProviderLimits>} [options.providers] - Per-provider limits
|
|
20
|
+
* @param {number} [options.warningThreshold=0.8] - Warn at this % of limit
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this._warningThreshold = options.warningThreshold ?? 0.8;
|
|
25
|
+
/** @type {Map<string, { bucket: TokenBucket, window: SlidingWindow, limits: ProviderLimits, usage: Object }>} */
|
|
26
|
+
this._providers = new Map();
|
|
27
|
+
|
|
28
|
+
if (options.providers) {
|
|
29
|
+
for (const [name, limits] of Object.entries(options.providers)) {
|
|
30
|
+
this.addProvider(name, limits);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add a provider with rate limits.
|
|
37
|
+
* @param {string} name - Provider name
|
|
38
|
+
* @param {ProviderLimits} limits
|
|
39
|
+
*/
|
|
40
|
+
addProvider(name, limits) {
|
|
41
|
+
const rpm = limits.requestsPerMinute ?? 60;
|
|
42
|
+
const bucket = new TokenBucket({ capacity: rpm, refillRate: rpm / 60 });
|
|
43
|
+
const window = new SlidingWindow({ maxRequests: rpm, windowMs: 60000 });
|
|
44
|
+
this._providers.set(name, {
|
|
45
|
+
bucket,
|
|
46
|
+
window,
|
|
47
|
+
limits,
|
|
48
|
+
usage: { requests: 0, tokens: 0, rejected: 0, lastReset: Date.now() },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a request is allowed for a provider.
|
|
54
|
+
* @param {string} provider - Provider name
|
|
55
|
+
* @param {number} [tokens=0] - Token count for this request
|
|
56
|
+
* @returns {Object} { allowed: boolean, reason?: string, waitMs?: number }
|
|
57
|
+
*/
|
|
58
|
+
check(provider, tokens = 0) {
|
|
59
|
+
const entry = this._providers.get(provider);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
return { allowed: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check sliding window
|
|
65
|
+
if (!entry.window.consume()) {
|
|
66
|
+
entry.usage.rejected++;
|
|
67
|
+
const waitMs = entry.window.waitFor();
|
|
68
|
+
this.emit('rate:rejected', { provider, waitMs });
|
|
69
|
+
return { allowed: false, reason: 'Rate limit exceeded', waitMs };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check token bucket
|
|
73
|
+
if (!entry.bucket.consume()) {
|
|
74
|
+
entry.usage.rejected++;
|
|
75
|
+
const waitMs = entry.bucket.waitFor();
|
|
76
|
+
this.emit('rate:rejected', { provider, waitMs });
|
|
77
|
+
return { allowed: false, reason: 'Token bucket empty', waitMs };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
entry.usage.requests++;
|
|
81
|
+
entry.usage.tokens += tokens;
|
|
82
|
+
|
|
83
|
+
// Check warning threshold
|
|
84
|
+
const usageRatio = entry.usage.requests / (entry.limits.requestsPerMinute ?? 60);
|
|
85
|
+
if (usageRatio >= this._warningThreshold) {
|
|
86
|
+
this.emit('rate:warning', { provider, usageRatio });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { allowed: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Record token usage for a provider.
|
|
94
|
+
* @param {string} provider
|
|
95
|
+
* @param {number} tokens
|
|
96
|
+
*/
|
|
97
|
+
recordUsage(provider, tokens) {
|
|
98
|
+
const entry = this._providers.get(provider);
|
|
99
|
+
if (entry) {
|
|
100
|
+
entry.usage.tokens += tokens;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get usage stats for a provider.
|
|
106
|
+
* @param {string} provider
|
|
107
|
+
* @returns {Object|null}
|
|
108
|
+
*/
|
|
109
|
+
getUsage(provider) {
|
|
110
|
+
const entry = this._providers.get(provider);
|
|
111
|
+
if (!entry) return null;
|
|
112
|
+
return { ...entry.usage };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all provider stats.
|
|
117
|
+
* @returns {Object}
|
|
118
|
+
*/
|
|
119
|
+
getAllUsage() {
|
|
120
|
+
const result = {};
|
|
121
|
+
for (const [name, entry] of this._providers) {
|
|
122
|
+
result[name] = { ...entry.usage };
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reset usage counters for a provider.
|
|
129
|
+
* @param {string} provider
|
|
130
|
+
*/
|
|
131
|
+
reset(provider) {
|
|
132
|
+
const entry = this._providers.get(provider);
|
|
133
|
+
if (entry) {
|
|
134
|
+
entry.usage = { requests: 0, tokens: 0, rejected: 0, lastReset: Date.now() };
|
|
135
|
+
entry.bucket.reset();
|
|
136
|
+
entry.window.reset();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reset all providers.
|
|
142
|
+
*/
|
|
143
|
+
resetAll() {
|
|
144
|
+
for (const name of this._providers.keys()) {
|
|
145
|
+
this.reset(name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default RateShield;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PrepiScript executor - runs parsed scripts.
|
|
3
|
+
* @module script/executor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
|
|
8
|
+
export class ScriptExecutor extends EventEmitter {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} [options]
|
|
11
|
+
* @param {Object} [options.tools] - Tool instances for execution
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
super();
|
|
15
|
+
this._tools = options.tools || {};
|
|
16
|
+
this._results = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute a parsed PrepiScript.
|
|
21
|
+
* @param {import('./parser.mjs').ParsedScript} script - Parsed script
|
|
22
|
+
* @param {Object} [context] - Execution context
|
|
23
|
+
* @returns {Promise<Object>} Execution result
|
|
24
|
+
*/
|
|
25
|
+
async execute(script, context = {}) {
|
|
26
|
+
const variables = { ...script.variables, ...context };
|
|
27
|
+
const stepResults = [];
|
|
28
|
+
let lastResult = null;
|
|
29
|
+
|
|
30
|
+
for (const step of script.steps) {
|
|
31
|
+
this.emit('step:start', { keyword: step.keyword, line: step.line });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await this._executeStep(step, variables, lastResult);
|
|
35
|
+
stepResults.push({ step, result, success: true });
|
|
36
|
+
lastResult = result;
|
|
37
|
+
|
|
38
|
+
// Store in variables if argument looks like a variable assignment
|
|
39
|
+
if (step.argument?.includes('->')) {
|
|
40
|
+
const [_, varName] = step.argument.split('->').map(s => s.trim());
|
|
41
|
+
if (varName) variables[varName] = result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.emit('step:complete', { keyword: step.keyword, line: step.line, result });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
stepResults.push({ step, error: err.message, success: false });
|
|
47
|
+
this.emit('step:error', { keyword: step.keyword, line: step.line, error: err.message });
|
|
48
|
+
|
|
49
|
+
// Stop on error unless it's a non-critical step
|
|
50
|
+
if (step.keyword !== 'CACHE' && step.keyword !== 'FILTER') {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
scriptName: script.name,
|
|
58
|
+
steps: stepResults,
|
|
59
|
+
variables,
|
|
60
|
+
success: stepResults.every(r => r.success),
|
|
61
|
+
output: lastResult,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Execute a single step.
|
|
67
|
+
* @param {Object} step
|
|
68
|
+
* @param {Object} variables
|
|
69
|
+
* @param {*} lastResult
|
|
70
|
+
* @returns {Promise<*>}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
async _executeStep(step, variables, lastResult) {
|
|
74
|
+
const arg = this._resolveVariables(step.argument, variables);
|
|
75
|
+
|
|
76
|
+
switch (step.keyword) {
|
|
77
|
+
case 'SEARCH':
|
|
78
|
+
return this._executeSearch(arg, step.options);
|
|
79
|
+
case 'EXTRACT':
|
|
80
|
+
return this._executeExtract(arg, lastResult, step.options);
|
|
81
|
+
case 'FORMAT':
|
|
82
|
+
return this._executeFormat(arg, lastResult, step.options);
|
|
83
|
+
case 'DELIVER':
|
|
84
|
+
return this._executeDeliver(arg, lastResult, step.options);
|
|
85
|
+
case 'CACHE':
|
|
86
|
+
return { cached: true, key: arg };
|
|
87
|
+
case 'FILTER':
|
|
88
|
+
return this._executeFilter(arg, lastResult, step.options);
|
|
89
|
+
case 'MERGE':
|
|
90
|
+
return this._executeMerge(lastResult, step.options);
|
|
91
|
+
case 'IF':
|
|
92
|
+
return this._executeIf(arg, lastResult, step.options);
|
|
93
|
+
case 'OUTPUT':
|
|
94
|
+
return lastResult;
|
|
95
|
+
default:
|
|
96
|
+
throw new Error(`Unknown step keyword: ${step.keyword}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve variable references in a string.
|
|
102
|
+
* @param {string} str
|
|
103
|
+
* @param {Object} variables
|
|
104
|
+
* @returns {string}
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_resolveVariables(str, variables) {
|
|
108
|
+
if (!str) return str;
|
|
109
|
+
return str.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
110
|
+
return variables[name] !== undefined ? String(variables[name]) : `\${${name}}`;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _executeSearch(query, options) {
|
|
115
|
+
if (this._tools.search) {
|
|
116
|
+
return this._tools.search(query, options);
|
|
117
|
+
}
|
|
118
|
+
return { query, results: [], source: 'no-search-tool' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async _executeExtract(target, source, options) {
|
|
122
|
+
if (this._tools.extract && source) {
|
|
123
|
+
return this._tools.extract(target, source, options);
|
|
124
|
+
}
|
|
125
|
+
return { target, extracted: source };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async _executeFormat(format, data, options) {
|
|
129
|
+
if (format === 'json') return JSON.stringify(data, null, 2);
|
|
130
|
+
if (format === 'text') return typeof data === 'string' ? data : JSON.stringify(data);
|
|
131
|
+
if (format === 'markdown') return typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data);
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async _executeDeliver(target, data, options) {
|
|
136
|
+
return { target, data, delivered: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async _executeFilter(filter, data, options) {
|
|
140
|
+
if (!data || !Array.isArray(data)) return data;
|
|
141
|
+
return data; // In a real implementation, apply filter criteria
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async _executeMerge(data, options) {
|
|
145
|
+
return { merged: true, data };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _executeIf(condition, data, options) {
|
|
149
|
+
// Simple condition evaluation
|
|
150
|
+
if (condition && data) return data;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Register a tool for script execution.
|
|
156
|
+
* @param {string} name
|
|
157
|
+
* @param {Function} fn
|
|
158
|
+
*/
|
|
159
|
+
registerTool(name, fn) {
|
|
160
|
+
this._tools[name] = fn;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default ScriptExecutor;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PrepiScript parser - custom task definition language.
|
|
3
|
+
* @module script/parser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} ParsedScript
|
|
8
|
+
* @property {string} name - Script name
|
|
9
|
+
* @property {Object[]} steps - Parsed steps
|
|
10
|
+
* @property {Object} variables - Variable declarations
|
|
11
|
+
* @property {Object[]} errors - Parse errors
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ScriptStep
|
|
16
|
+
* @property {string} keyword - Step keyword (SEARCH, EXTRACT, FORMAT, DELIVER, etc.)
|
|
17
|
+
* @property {string} argument - Step argument
|
|
18
|
+
* @property {Object} options - Step options
|
|
19
|
+
* @property {number} line - Source line number
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Valid keywords */
|
|
23
|
+
const KEYWORDS = ['TASK', 'SEARCH', 'EXTRACT', 'FORMAT', 'DELIVER', 'CACHE', 'FILTER', 'MERGE', 'IF', 'SET', 'OUTPUT'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a PrepiScript string.
|
|
27
|
+
* @param {string} script - Script source
|
|
28
|
+
* @returns {ParsedScript}
|
|
29
|
+
*/
|
|
30
|
+
export function parse(script) {
|
|
31
|
+
if (!script || typeof script !== 'string') {
|
|
32
|
+
return { name: '', steps: [], variables: {}, errors: [{ line: 0, message: 'Empty script' }] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = script.split('\n');
|
|
36
|
+
const steps = [];
|
|
37
|
+
const variables = {};
|
|
38
|
+
const errors = [];
|
|
39
|
+
let name = '';
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const lineNum = i + 1;
|
|
43
|
+
const line = lines[i].trim();
|
|
44
|
+
|
|
45
|
+
// Skip empty lines and comments
|
|
46
|
+
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
|
|
47
|
+
|
|
48
|
+
// Parse keyword and argument
|
|
49
|
+
const match = line.match(/^(\w+)\s*(?:["']([^"']*)["']|\s+(.+))?$/);
|
|
50
|
+
if (!match) {
|
|
51
|
+
if (line.length > 0) {
|
|
52
|
+
errors.push({ line: lineNum, message: `Invalid syntax: ${line.substring(0, 50)}` });
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const keyword = match[1].toUpperCase();
|
|
58
|
+
const argument = (match[2] || match[3] || '').trim();
|
|
59
|
+
|
|
60
|
+
if (!KEYWORDS.includes(keyword)) {
|
|
61
|
+
errors.push({ line: lineNum, message: `Unknown keyword: ${keyword}` });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (keyword === 'TASK') {
|
|
66
|
+
name = argument;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Parse options (key=value pairs)
|
|
71
|
+
const options = {};
|
|
72
|
+
const optPattern = /(\w+)=["']?([^"'\s]+)["']?/g;
|
|
73
|
+
let optMatch;
|
|
74
|
+
const remaining = line.substring(match[0].indexOf(argument || '') + (argument || '').length);
|
|
75
|
+
while ((optMatch = optPattern.exec(remaining)) !== null) {
|
|
76
|
+
options[optMatch[1]] = optMatch[2];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (keyword === 'SET') {
|
|
80
|
+
const varMatch = argument.match(/^(\w+)\s*=\s*(.+)$/);
|
|
81
|
+
if (varMatch) {
|
|
82
|
+
variables[varMatch[1]] = varMatch[2];
|
|
83
|
+
} else {
|
|
84
|
+
errors.push({ line: lineNum, message: `Invalid SET syntax: ${argument}` });
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
steps.push({
|
|
90
|
+
keyword,
|
|
91
|
+
argument,
|
|
92
|
+
options,
|
|
93
|
+
line: lineNum,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!name && steps.length > 0) {
|
|
98
|
+
errors.unshift({ line: 0, message: 'Missing TASK declaration' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { name, steps, variables, errors };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate a parsed script.
|
|
106
|
+
* @param {ParsedScript} parsed
|
|
107
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
108
|
+
*/
|
|
109
|
+
export function validate(parsed) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
|
|
112
|
+
if (parsed.errors.length > 0) {
|
|
113
|
+
errors.push(...parsed.errors.map(e => `Line ${e.line}: ${e.message}`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!parsed.name) {
|
|
117
|
+
errors.push('Script must have a TASK name');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (parsed.steps.length === 0) {
|
|
121
|
+
errors.push('Script must have at least one step');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check that DELIVER has a target
|
|
125
|
+
for (const step of parsed.steps) {
|
|
126
|
+
if (step.keyword === 'DELIVER' && !step.argument) {
|
|
127
|
+
errors.push(`Line ${step.line}: DELIVER requires a target`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { valid: errors.length === 0, errors };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default { parse, validate };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PII detection and redaction.
|
|
3
|
+
* @module security/privacy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} PIIMatch
|
|
8
|
+
* @property {string} type - PII type
|
|
9
|
+
* @property {string} value - Matched value
|
|
10
|
+
* @property {number} start - Start index
|
|
11
|
+
* @property {number} end - End index
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** PII detection patterns */
|
|
15
|
+
const PII_PATTERNS = {
|
|
16
|
+
email: {
|
|
17
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
18
|
+
replacement: '[EMAIL]',
|
|
19
|
+
},
|
|
20
|
+
phone: {
|
|
21
|
+
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
22
|
+
replacement: '[PHONE]',
|
|
23
|
+
},
|
|
24
|
+
ssn: {
|
|
25
|
+
pattern: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g,
|
|
26
|
+
replacement: '[SSN]',
|
|
27
|
+
},
|
|
28
|
+
creditCard: {
|
|
29
|
+
pattern: /\b(?:\d{4}[-.\s]?){3}\d{4}\b/g,
|
|
30
|
+
replacement: '[CREDIT_CARD]',
|
|
31
|
+
},
|
|
32
|
+
ipAddress: {
|
|
33
|
+
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
34
|
+
replacement: '[IP]',
|
|
35
|
+
},
|
|
36
|
+
// US addresses (simplified)
|
|
37
|
+
address: {
|
|
38
|
+
pattern: /\b\d{1,5}\s+[\w\s]+(?:street|st|avenue|ave|road|rd|boulevard|blvd|drive|dr|lane|ln|way|court|ct)\b/gi,
|
|
39
|
+
replacement: '[ADDRESS]',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect PII in text.
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @returns {PIIMatch[]}
|
|
47
|
+
*/
|
|
48
|
+
export function detectPII(text) {
|
|
49
|
+
if (!text || typeof text !== 'string') return [];
|
|
50
|
+
|
|
51
|
+
const matches = [];
|
|
52
|
+
for (const [type, { pattern }] of Object.entries(PII_PATTERNS)) {
|
|
53
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = regex.exec(text)) !== null) {
|
|
56
|
+
matches.push({
|
|
57
|
+
type,
|
|
58
|
+
value: match[0],
|
|
59
|
+
start: match.index,
|
|
60
|
+
end: match.index + match[0].length,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return matches.sort((a, b) => a.start - b.start);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Redact PII from text.
|
|
70
|
+
* @param {string} text
|
|
71
|
+
* @param {Object} [options]
|
|
72
|
+
* @param {string[]} [options.types] - PII types to redact (default: all)
|
|
73
|
+
* @param {string} [options.replacement] - Custom replacement marker
|
|
74
|
+
* @returns {{ text: string, redacted: number }}
|
|
75
|
+
*/
|
|
76
|
+
export function redactPII(text, options = {}) {
|
|
77
|
+
if (!text || typeof text !== 'string') {
|
|
78
|
+
return { text: '', redacted: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const types = options.types || Object.keys(PII_PATTERNS);
|
|
82
|
+
let result = text;
|
|
83
|
+
let redacted = 0;
|
|
84
|
+
|
|
85
|
+
for (const type of types) {
|
|
86
|
+
const config = PII_PATTERNS[type];
|
|
87
|
+
if (!config) continue;
|
|
88
|
+
const replacement = options.replacement || config.replacement;
|
|
89
|
+
const regex = new RegExp(config.pattern.source, config.pattern.flags);
|
|
90
|
+
const before = result;
|
|
91
|
+
result = result.replace(regex, replacement);
|
|
92
|
+
const matches = before.match(regex);
|
|
93
|
+
if (matches) redacted += matches.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { text: result, redacted };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if text contains PII.
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
export function containsPII(text) {
|
|
105
|
+
return detectPII(text).length > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default { detectPII, redactPII, containsPII };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Input/output sanitization.
|
|
3
|
+
* @module security/sanitizer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} SanitizeResult
|
|
8
|
+
* @property {string} text - Sanitized text
|
|
9
|
+
* @property {boolean} modified - Whether text was modified
|
|
10
|
+
* @property {string[]} issues - Issues found
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Injection patterns to detect and strip */
|
|
14
|
+
const INJECTION_PATTERNS = [
|
|
15
|
+
// Prompt injection attempts
|
|
16
|
+
/ignore\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|rules?)/gi,
|
|
17
|
+
/you\s+are\s+now\s+(?:a|an|the)/gi,
|
|
18
|
+
/system\s*:\s*/gi,
|
|
19
|
+
/assistant\s*:\s*/gi,
|
|
20
|
+
/user\s*:\s*/gi,
|
|
21
|
+
/\[INST\]/gi,
|
|
22
|
+
/\[\/INST\]/gi,
|
|
23
|
+
/<\|im_start\|>/gi,
|
|
24
|
+
/<\|im_end\|>/gi,
|
|
25
|
+
/###\s*(?:system|instruction)/gi,
|
|
26
|
+
// Code injection
|
|
27
|
+
/```(?:bash|sh|shell)\s*\n[\s\S]*?rm\s+-rf/gi,
|
|
28
|
+
/```(?:bash|sh|shell)\s*\n[\s\S]*?curl\s+.*\|\s*sh/gi,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize user input.
|
|
33
|
+
* @param {string} input - Raw user input
|
|
34
|
+
* @param {Object} [options]
|
|
35
|
+
* @param {boolean} [options.stripInjection=true] - Strip injection patterns
|
|
36
|
+
* @param {number} [options.maxLength=100000] - Max input length
|
|
37
|
+
* @returns {SanitizeResult}
|
|
38
|
+
*/
|
|
39
|
+
export function sanitize(input, options = {}) {
|
|
40
|
+
if (!input || typeof input !== 'string') {
|
|
41
|
+
return { text: '', modified: false, issues: ['Empty or invalid input'] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { stripInjection = true, maxLength = 100000 } = options;
|
|
45
|
+
let text = input;
|
|
46
|
+
let modified = false;
|
|
47
|
+
const issues = [];
|
|
48
|
+
|
|
49
|
+
// Truncate if too long
|
|
50
|
+
if (text.length > maxLength) {
|
|
51
|
+
text = text.substring(0, maxLength);
|
|
52
|
+
modified = true;
|
|
53
|
+
issues.push(`Input truncated to ${maxLength} characters`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Strip null bytes
|
|
57
|
+
if (text.includes('\0')) {
|
|
58
|
+
text = text.replace(/\0/g, '');
|
|
59
|
+
modified = true;
|
|
60
|
+
issues.push('Null bytes removed');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Strip injection patterns
|
|
64
|
+
if (stripInjection) {
|
|
65
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
66
|
+
if (pattern.test(text)) {
|
|
67
|
+
text = text.replace(pattern, '[REDACTED]');
|
|
68
|
+
modified = true;
|
|
69
|
+
issues.push(`Injection pattern detected and redacted: ${pattern.source.substring(0, 40)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { text, modified, issues };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate task parameters.
|
|
79
|
+
* @param {Object} params - Task parameters
|
|
80
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function validateParams(params) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
|
|
85
|
+
if (!params || typeof params !== 'object') {
|
|
86
|
+
return { valid: false, errors: ['Parameters must be an object'] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (params.query && typeof params.query !== 'string') {
|
|
90
|
+
errors.push('Query must be a string');
|
|
91
|
+
}
|
|
92
|
+
if (params.query && params.query.length > 100000) {
|
|
93
|
+
errors.push('Query exceeds maximum length');
|
|
94
|
+
}
|
|
95
|
+
if (params.type && typeof params.type !== 'string') {
|
|
96
|
+
errors.push('Type must be a string');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { valid: errors.length === 0, errors };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sanitize output before returning to user.
|
|
104
|
+
* @param {string} output
|
|
105
|
+
* @returns {SanitizeResult}
|
|
106
|
+
*/
|
|
107
|
+
export function sanitizeOutput(output) {
|
|
108
|
+
if (!output || typeof output !== 'string') {
|
|
109
|
+
return { text: '', modified: false, issues: ['Empty output'] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let text = output;
|
|
113
|
+
let modified = false;
|
|
114
|
+
const issues = [];
|
|
115
|
+
|
|
116
|
+
// Remove any system-level markers that might have leaked
|
|
117
|
+
const leakPatterns = [
|
|
118
|
+
/system\s*:\s*.*$/gim,
|
|
119
|
+
/\[INST\][\s\S]*?\[\/INST\]/gi,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const pattern of leakPatterns) {
|
|
123
|
+
if (pattern.test(text)) {
|
|
124
|
+
text = text.replace(pattern, '');
|
|
125
|
+
modified = true;
|
|
126
|
+
issues.push('System-level content leaked in output');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { text: text.trim(), modified, issues };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default { sanitize, validateParams, sanitizeOutput };
|