web-agent-bridge 2.4.0 → 2.5.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/README.ar.md +18 -0
- package/README.md +18 -0
- package/package.json +1 -1
- package/sdk/index.d.ts +170 -0
- package/sdk/index.js +246 -1
- package/sdk/package.json +1 -1
- package/server/control-plane/index.js +301 -0
- package/server/data-plane/index.js +354 -0
- package/server/index.js +2 -0
- package/server/llm/index.js +404 -0
- package/server/observability/index.js +394 -0
- package/server/protocol/capabilities.js +223 -0
- package/server/protocol/index.js +243 -0
- package/server/protocol/schema.js +584 -0
- package/server/registry/index.js +326 -0
- package/server/routes/runtime.js +725 -0
- package/server/runtime/event-bus.js +210 -0
- package/server/runtime/index.js +233 -0
- package/server/runtime/sandbox.js +266 -0
- package/server/runtime/scheduler.js +395 -0
- package/server/runtime/state-manager.js +188 -0
- package/server/security/index.js +355 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Control Plane
|
|
5
|
+
*
|
|
6
|
+
* Management layer for the Agent OS. Handles:
|
|
7
|
+
* - Agent lifecycle management
|
|
8
|
+
* - Policy enforcement
|
|
9
|
+
* - Deployment management
|
|
10
|
+
* - Configuration distribution
|
|
11
|
+
*
|
|
12
|
+
* The Control Plane is separated from the Data Plane.
|
|
13
|
+
* It decides WHAT to do and WHO can do it.
|
|
14
|
+
* The Data Plane executes the actual work.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { bus } = require('../runtime/event-bus');
|
|
19
|
+
const { identity } = require('../security');
|
|
20
|
+
|
|
21
|
+
// ─── Agent Lifecycle Manager ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class AgentManager {
|
|
24
|
+
constructor() {
|
|
25
|
+
this._deployments = new Map(); // deploymentId → deployment config
|
|
26
|
+
this._assignments = new Map(); // agentId → Set<siteId>
|
|
27
|
+
this._healthChecks = new Map(); // agentId → last health check
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deploy an agent to the runtime
|
|
32
|
+
*/
|
|
33
|
+
deploy(agentId, config = {}) {
|
|
34
|
+
const agent = identity.getAgent(agentId);
|
|
35
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
36
|
+
|
|
37
|
+
const deploymentId = `deploy_${crypto.randomBytes(12).toString('hex')}`;
|
|
38
|
+
const deployment = {
|
|
39
|
+
id: deploymentId,
|
|
40
|
+
agentId,
|
|
41
|
+
config: {
|
|
42
|
+
autoRestart: config.autoRestart !== false,
|
|
43
|
+
maxRetries: config.maxRetries || 5,
|
|
44
|
+
healthCheckInterval: config.healthCheckInterval || 60_000,
|
|
45
|
+
resources: {
|
|
46
|
+
maxMemory: config.maxMemory || 256 * 1024 * 1024,
|
|
47
|
+
maxCpu: config.maxCpu || 80, // percentage
|
|
48
|
+
maxTasks: config.maxTasks || 10,
|
|
49
|
+
},
|
|
50
|
+
environment: config.environment || 'production',
|
|
51
|
+
version: config.version || '1.0.0',
|
|
52
|
+
},
|
|
53
|
+
status: 'deployed',
|
|
54
|
+
restartCount: 0,
|
|
55
|
+
deployedAt: Date.now(),
|
|
56
|
+
lastHealthCheck: null,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this._deployments.set(deploymentId, deployment);
|
|
60
|
+
bus.emit('agent.deployed', { agentId, deploymentId });
|
|
61
|
+
|
|
62
|
+
return deployment;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Assign agent to sites
|
|
67
|
+
*/
|
|
68
|
+
assign(agentId, siteIds) {
|
|
69
|
+
if (!this._assignments.has(agentId)) this._assignments.set(agentId, new Set());
|
|
70
|
+
const set = this._assignments.get(agentId);
|
|
71
|
+
for (const siteId of siteIds) set.add(siteId);
|
|
72
|
+
bus.emit('agent.assigned', { agentId, sites: siteIds });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Unassign agent from sites
|
|
77
|
+
*/
|
|
78
|
+
unassign(agentId, siteIds) {
|
|
79
|
+
const set = this._assignments.get(agentId);
|
|
80
|
+
if (!set) return;
|
|
81
|
+
for (const siteId of siteIds) set.delete(siteId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get sites assigned to an agent
|
|
86
|
+
*/
|
|
87
|
+
getAssignments(agentId) {
|
|
88
|
+
const set = this._assignments.get(agentId);
|
|
89
|
+
return set ? [...set] : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Record a health check
|
|
94
|
+
*/
|
|
95
|
+
recordHealthCheck(agentId, health) {
|
|
96
|
+
this._healthChecks.set(agentId, {
|
|
97
|
+
...health,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
status: health.healthy ? 'healthy' : 'unhealthy',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!health.healthy) {
|
|
103
|
+
bus.emit('agent.unhealthy', { agentId, reason: health.reason || 'unknown' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get agent health
|
|
109
|
+
*/
|
|
110
|
+
getHealth(agentId) {
|
|
111
|
+
return this._healthChecks.get(agentId) || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Undeploy an agent
|
|
116
|
+
*/
|
|
117
|
+
undeploy(deploymentId) {
|
|
118
|
+
const deployment = this._deployments.get(deploymentId);
|
|
119
|
+
if (deployment) {
|
|
120
|
+
deployment.status = 'undeployed';
|
|
121
|
+
bus.emit('agent.undeployed', { agentId: deployment.agentId, deploymentId });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* List all deployments
|
|
127
|
+
*/
|
|
128
|
+
listDeployments(filter = {}) {
|
|
129
|
+
const result = [];
|
|
130
|
+
for (const [, d] of this._deployments) {
|
|
131
|
+
if (filter.status && d.status !== filter.status) continue;
|
|
132
|
+
if (filter.agentId && d.agentId !== filter.agentId) continue;
|
|
133
|
+
result.push(d);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Policy Engine ──────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
class PolicyEngine {
|
|
142
|
+
constructor() {
|
|
143
|
+
this._policies = new Map(); // policyId → policy definition
|
|
144
|
+
this._bindings = new Map(); // entityId → Set<policyId>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a policy
|
|
149
|
+
*/
|
|
150
|
+
createPolicy(policy) {
|
|
151
|
+
const policyId = `policy_${crypto.randomBytes(12).toString('hex')}`;
|
|
152
|
+
const def = {
|
|
153
|
+
id: policyId,
|
|
154
|
+
name: policy.name || 'Unnamed Policy',
|
|
155
|
+
description: policy.description || '',
|
|
156
|
+
type: policy.type || 'agent', // agent, site, global
|
|
157
|
+
|
|
158
|
+
// Rules
|
|
159
|
+
rules: (policy.rules || []).map(r => ({
|
|
160
|
+
id: `rule_${crypto.randomBytes(6).toString('hex')}`,
|
|
161
|
+
action: r.action, // allow, deny, require
|
|
162
|
+
resource: r.resource, // capability, selector, domain, rate
|
|
163
|
+
condition: r.condition || {}, // { equals, contains, pattern, min, max }
|
|
164
|
+
effect: r.effect || 'deny', // allow, deny, audit
|
|
165
|
+
})),
|
|
166
|
+
|
|
167
|
+
// Rate limits
|
|
168
|
+
rateLimit: policy.rateLimit || null,
|
|
169
|
+
|
|
170
|
+
// Time constraints
|
|
171
|
+
schedule: policy.schedule || null, // { start, end, timezone, days: [] }
|
|
172
|
+
|
|
173
|
+
priority: policy.priority || 0,
|
|
174
|
+
enabled: policy.enabled !== false,
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this._policies.set(policyId, def);
|
|
179
|
+
return def;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Bind a policy to an entity (agent, site, global)
|
|
184
|
+
*/
|
|
185
|
+
bind(entityId, policyId) {
|
|
186
|
+
if (!this._bindings.has(entityId)) this._bindings.set(entityId, new Set());
|
|
187
|
+
this._bindings.get(entityId).add(policyId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Unbind a policy
|
|
192
|
+
*/
|
|
193
|
+
unbind(entityId, policyId) {
|
|
194
|
+
const bindings = this._bindings.get(entityId);
|
|
195
|
+
if (bindings) bindings.delete(policyId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Evaluate policies for an entity against an action
|
|
200
|
+
*/
|
|
201
|
+
evaluate(entityId, action, context = {}) {
|
|
202
|
+
const policyIds = this._bindings.get(entityId) || new Set();
|
|
203
|
+
const globalIds = this._bindings.get('*') || new Set();
|
|
204
|
+
const allIds = new Set([...policyIds, ...globalIds]);
|
|
205
|
+
|
|
206
|
+
const results = [];
|
|
207
|
+
let finalEffect = 'allow'; // default allow
|
|
208
|
+
|
|
209
|
+
// Sort policies by priority
|
|
210
|
+
const policies = [];
|
|
211
|
+
for (const pid of allIds) {
|
|
212
|
+
const policy = this._policies.get(pid);
|
|
213
|
+
if (policy && policy.enabled) policies.push(policy);
|
|
214
|
+
}
|
|
215
|
+
policies.sort((a, b) => b.priority - a.priority);
|
|
216
|
+
|
|
217
|
+
for (const policy of policies) {
|
|
218
|
+
for (const rule of policy.rules) {
|
|
219
|
+
if (rule.action !== action && rule.action !== '*') continue;
|
|
220
|
+
|
|
221
|
+
const match = this._evaluateCondition(rule.condition, context);
|
|
222
|
+
if (match) {
|
|
223
|
+
results.push({
|
|
224
|
+
policyId: policy.id,
|
|
225
|
+
policyName: policy.name,
|
|
226
|
+
ruleId: rule.id,
|
|
227
|
+
effect: rule.effect,
|
|
228
|
+
resource: rule.resource,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (rule.effect === 'deny') finalEffect = 'deny';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
allowed: finalEffect === 'allow',
|
|
238
|
+
effect: finalEffect,
|
|
239
|
+
evaluatedPolicies: results,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Evaluate a rule condition
|
|
245
|
+
*/
|
|
246
|
+
_evaluateCondition(condition, context) {
|
|
247
|
+
if (!condition || Object.keys(condition).length === 0) return true;
|
|
248
|
+
|
|
249
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
250
|
+
const contextValue = context[key];
|
|
251
|
+
if (contextValue === undefined) return false;
|
|
252
|
+
|
|
253
|
+
if (typeof value === 'object' && value !== null) {
|
|
254
|
+
if (value.equals !== undefined && contextValue !== value.equals) return false;
|
|
255
|
+
if (value.contains && !String(contextValue).includes(value.contains)) return false;
|
|
256
|
+
if (value.pattern && !new RegExp(value.pattern).test(String(contextValue))) return false;
|
|
257
|
+
if (value.min !== undefined && contextValue < value.min) return false;
|
|
258
|
+
if (value.max !== undefined && contextValue > value.max) return false;
|
|
259
|
+
if (value.in && !value.in.includes(contextValue)) return false;
|
|
260
|
+
} else {
|
|
261
|
+
if (contextValue !== value) return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get policy
|
|
269
|
+
*/
|
|
270
|
+
getPolicy(policyId) {
|
|
271
|
+
return this._policies.get(policyId) || null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* List policies
|
|
276
|
+
*/
|
|
277
|
+
listPolicies(entityId) {
|
|
278
|
+
if (entityId) {
|
|
279
|
+
const ids = this._bindings.get(entityId) || new Set();
|
|
280
|
+
return [...ids].map(id => this._policies.get(id)).filter(Boolean);
|
|
281
|
+
}
|
|
282
|
+
return Array.from(this._policies.values());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Delete a policy
|
|
287
|
+
*/
|
|
288
|
+
deletePolicy(policyId) {
|
|
289
|
+
this._policies.delete(policyId);
|
|
290
|
+
for (const [, bindings] of this._bindings) {
|
|
291
|
+
bindings.delete(policyId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Singletons ─────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
const agentManager = new AgentManager();
|
|
299
|
+
const policyEngine = new PolicyEngine();
|
|
300
|
+
|
|
301
|
+
module.exports = { AgentManager, PolicyEngine, agentManager, policyEngine };
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Data Plane
|
|
5
|
+
*
|
|
6
|
+
* Execution engine that actually does the work:
|
|
7
|
+
* - Browser automation (via puppeteer/playwright or bridge)
|
|
8
|
+
* - API execution
|
|
9
|
+
* - Workflow orchestration
|
|
10
|
+
* - DOM abstraction (semantic actions instead of raw selectors)
|
|
11
|
+
*
|
|
12
|
+
* The Data Plane only executes what the Control Plane authorizes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { bus } = require('../runtime/event-bus');
|
|
16
|
+
const { tracer, metrics } = require('../observability');
|
|
17
|
+
const { isolation } = require('../security');
|
|
18
|
+
|
|
19
|
+
// ─── Semantic DOM Abstraction ───────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Maps semantic actions to site-specific implementations.
|
|
23
|
+
* Instead of click('.add-to-cart'), you call execute('checkout.addItem', { productId })
|
|
24
|
+
*/
|
|
25
|
+
class SemanticActionResolver {
|
|
26
|
+
constructor() {
|
|
27
|
+
this._mappings = new Map(); // `${domain}:${semanticAction}` → implementation
|
|
28
|
+
this._defaults = new Map(); // semanticAction → default implementation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a semantic action mapping for a domain
|
|
33
|
+
*/
|
|
34
|
+
register(domain, semanticAction, implementation) {
|
|
35
|
+
const key = `${domain}:${semanticAction}`;
|
|
36
|
+
this._mappings.set(key, {
|
|
37
|
+
domain,
|
|
38
|
+
action: semanticAction,
|
|
39
|
+
selector: implementation.selector || null,
|
|
40
|
+
handler: implementation.handler || null,
|
|
41
|
+
params: implementation.params || {},
|
|
42
|
+
strategy: implementation.strategy || 'selector', // selector, handler, api
|
|
43
|
+
confidence: implementation.confidence || 1.0,
|
|
44
|
+
lastVerified: Date.now(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a default semantic action (fallback)
|
|
50
|
+
*/
|
|
51
|
+
registerDefault(semanticAction, implementation) {
|
|
52
|
+
this._defaults.set(semanticAction, implementation);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a semantic action to a concrete implementation
|
|
57
|
+
*/
|
|
58
|
+
resolve(domain, semanticAction) {
|
|
59
|
+
const key = `${domain}:${semanticAction}`;
|
|
60
|
+
let impl = this._mappings.get(key);
|
|
61
|
+
|
|
62
|
+
// Try wildcard domain
|
|
63
|
+
if (!impl) {
|
|
64
|
+
const wildKey = `*:${semanticAction}`;
|
|
65
|
+
impl = this._mappings.get(wildKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try default
|
|
69
|
+
if (!impl) {
|
|
70
|
+
impl = this._defaults.get(semanticAction);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return impl || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List all semantic actions for a domain
|
|
78
|
+
*/
|
|
79
|
+
listActions(domain) {
|
|
80
|
+
const actions = [];
|
|
81
|
+
for (const [key, impl] of this._mappings) {
|
|
82
|
+
if (key.startsWith(`${domain}:`) || key.startsWith('*:')) {
|
|
83
|
+
actions.push({
|
|
84
|
+
action: impl.action,
|
|
85
|
+
domain: impl.domain,
|
|
86
|
+
strategy: impl.strategy,
|
|
87
|
+
confidence: impl.confidence,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return actions;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Task Executor ──────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
class Executor {
|
|
98
|
+
constructor() {
|
|
99
|
+
this._resolver = new SemanticActionResolver();
|
|
100
|
+
this._handlers = new Map();
|
|
101
|
+
this._stats = { executed: 0, succeeded: 0, failed: 0 };
|
|
102
|
+
|
|
103
|
+
// Register built-in semantic actions defaults
|
|
104
|
+
this._registerDefaults();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get resolver() { return this._resolver; }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register an execution handler
|
|
111
|
+
*/
|
|
112
|
+
registerHandler(type, handler) {
|
|
113
|
+
this._handlers.set(type, handler);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute a task
|
|
118
|
+
*/
|
|
119
|
+
async execute(task, context = {}) {
|
|
120
|
+
const { traceId, spanId } = tracer.startTrace(`execute:${task.type || 'general'}`);
|
|
121
|
+
const endTimer = metrics.startTimer('executor.task.duration', { type: task.type || 'general' });
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Check site isolation
|
|
125
|
+
if (task.siteId && task.agentId) {
|
|
126
|
+
if (!isolation.canAccess(task.siteId, task.agentId)) {
|
|
127
|
+
throw new Error(`Agent ${task.agentId} denied access to site ${task.siteId}`);
|
|
128
|
+
}
|
|
129
|
+
isolation.enter(task.siteId, task.agentId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let result;
|
|
133
|
+
|
|
134
|
+
// Route to handler based on task type
|
|
135
|
+
const handler = this._handlers.get(task.type);
|
|
136
|
+
if (handler) {
|
|
137
|
+
result = await handler(task, { ...context, traceId, spanId, resolver: this._resolver });
|
|
138
|
+
} else if (task.type === 'semantic') {
|
|
139
|
+
result = await this._executeSemantic(task, traceId);
|
|
140
|
+
} else if (task.type === 'pipeline') {
|
|
141
|
+
result = await this._executePipeline(task, traceId);
|
|
142
|
+
} else if (task.type === 'parallel') {
|
|
143
|
+
result = await this._executeParallel(task, traceId);
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`Unknown task type: ${task.type}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this._stats.executed++;
|
|
149
|
+
this._stats.succeeded++;
|
|
150
|
+
metrics.increment('executor.tasks.success', 1, { type: task.type });
|
|
151
|
+
|
|
152
|
+
tracer.endSpan(traceId, spanId, { success: true });
|
|
153
|
+
endTimer();
|
|
154
|
+
|
|
155
|
+
bus.emit('executor.completed', {
|
|
156
|
+
taskId: task.id,
|
|
157
|
+
type: task.type,
|
|
158
|
+
traceId,
|
|
159
|
+
duration: endTimer(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return { success: true, result, traceId };
|
|
163
|
+
} catch (err) {
|
|
164
|
+
this._stats.executed++;
|
|
165
|
+
this._stats.failed++;
|
|
166
|
+
metrics.increment('executor.tasks.failure', 1, { type: task.type });
|
|
167
|
+
|
|
168
|
+
tracer.endSpan(traceId, spanId, { error: err.message });
|
|
169
|
+
endTimer();
|
|
170
|
+
|
|
171
|
+
bus.emit('executor.failed', {
|
|
172
|
+
taskId: task.id,
|
|
173
|
+
type: task.type,
|
|
174
|
+
error: err.message,
|
|
175
|
+
traceId,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { success: false, error: err.message, traceId };
|
|
179
|
+
} finally {
|
|
180
|
+
// Leave site isolation
|
|
181
|
+
if (task.siteId && task.agentId) {
|
|
182
|
+
isolation.leave(task.siteId, task.agentId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Execute a semantic action (domain.action instead of raw selector)
|
|
189
|
+
*/
|
|
190
|
+
async _executeSemantic(task, traceId) {
|
|
191
|
+
const { domain, action, params } = task;
|
|
192
|
+
if (!domain || !action) throw new Error('Semantic tasks require domain and action');
|
|
193
|
+
|
|
194
|
+
const span = tracer.startSpan(traceId, `semantic:${domain}.${action}`);
|
|
195
|
+
|
|
196
|
+
const impl = this._resolver.resolve(task.siteDomain || '*', `${domain}.${action}`);
|
|
197
|
+
if (!impl) {
|
|
198
|
+
tracer.endSpan(traceId, span.id, { error: 'No implementation' });
|
|
199
|
+
throw new Error(`No semantic action found: ${domain}.${action}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
tracer.addEvent(traceId, span.id, 'resolved', {
|
|
203
|
+
strategy: impl.strategy,
|
|
204
|
+
confidence: impl.confidence,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
let result;
|
|
208
|
+
if (impl.strategy === 'handler' && impl.handler) {
|
|
209
|
+
result = await impl.handler(params);
|
|
210
|
+
} else if (impl.strategy === 'api') {
|
|
211
|
+
result = { delegated: 'api', endpoint: impl.selector, params };
|
|
212
|
+
} else {
|
|
213
|
+
result = {
|
|
214
|
+
resolvedSelector: impl.selector,
|
|
215
|
+
params: { ...impl.params, ...params },
|
|
216
|
+
confidence: impl.confidence,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tracer.endSpan(traceId, span.id, { success: true });
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Execute a pipeline (sequential steps)
|
|
226
|
+
*/
|
|
227
|
+
async _executePipeline(task, traceId) {
|
|
228
|
+
const steps = task.steps || [];
|
|
229
|
+
const results = [];
|
|
230
|
+
let previousResult = null;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < steps.length; i++) {
|
|
233
|
+
const step = steps[i];
|
|
234
|
+
const span = tracer.startSpan(traceId, `pipeline:step:${i}:${step.action || step.type}`);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const stepTask = {
|
|
238
|
+
...step,
|
|
239
|
+
type: step.type || 'semantic',
|
|
240
|
+
input: previousResult,
|
|
241
|
+
siteId: task.siteId,
|
|
242
|
+
agentId: task.agentId,
|
|
243
|
+
siteDomain: task.siteDomain,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result = await this.execute(stepTask, { parentTraceId: traceId });
|
|
247
|
+
previousResult = result.result;
|
|
248
|
+
results.push({ step: i, success: true, result: result.result });
|
|
249
|
+
|
|
250
|
+
tracer.endSpan(traceId, span.id, { success: true });
|
|
251
|
+
|
|
252
|
+
if (!result.success && (task.stopOnError !== false)) {
|
|
253
|
+
throw new Error(`Pipeline step ${i} failed: ${result.error}`);
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
tracer.endSpan(traceId, span.id, { error: err.message });
|
|
257
|
+
results.push({ step: i, success: false, error: err.message });
|
|
258
|
+
if (task.stopOnError !== false) throw err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { steps: results, totalSteps: steps.length };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute tasks in parallel
|
|
267
|
+
*/
|
|
268
|
+
async _executeParallel(task, traceId) {
|
|
269
|
+
const tasks = task.tasks || [];
|
|
270
|
+
const span = tracer.startSpan(traceId, `parallel:${tasks.length}_tasks`);
|
|
271
|
+
|
|
272
|
+
const promises = tasks.map((t, i) => {
|
|
273
|
+
const subTask = {
|
|
274
|
+
...t,
|
|
275
|
+
type: t.type || 'semantic',
|
|
276
|
+
siteId: task.siteId,
|
|
277
|
+
agentId: task.agentId,
|
|
278
|
+
siteDomain: task.siteDomain,
|
|
279
|
+
};
|
|
280
|
+
return this.execute(subTask, { parentTraceId: traceId })
|
|
281
|
+
.then(r => ({ index: i, ...r }))
|
|
282
|
+
.catch(e => ({ index: i, success: false, error: e.message }));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const results = await Promise.all(promises);
|
|
286
|
+
tracer.endSpan(traceId, span.id, { success: true, count: results.length });
|
|
287
|
+
|
|
288
|
+
return { results, totalTasks: tasks.length };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Register default semantic actions
|
|
293
|
+
*/
|
|
294
|
+
_registerDefaults() {
|
|
295
|
+
// Commerce domain
|
|
296
|
+
this._resolver.registerDefault('checkout.addItem', {
|
|
297
|
+
selector: '[data-action="add-to-cart"], .add-to-cart, #add-to-cart',
|
|
298
|
+
strategy: 'selector',
|
|
299
|
+
confidence: 0.8,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
this._resolver.registerDefault('checkout.viewCart', {
|
|
303
|
+
selector: '[data-action="view-cart"], .cart-icon, #cart',
|
|
304
|
+
strategy: 'selector',
|
|
305
|
+
confidence: 0.7,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
this._resolver.registerDefault('checkout.submit', {
|
|
309
|
+
selector: '[data-action="checkout"], .checkout-btn, #checkout',
|
|
310
|
+
strategy: 'selector',
|
|
311
|
+
confidence: 0.7,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this._resolver.registerDefault('search.query', {
|
|
315
|
+
selector: 'input[type="search"], input[name="q"], #search',
|
|
316
|
+
strategy: 'selector',
|
|
317
|
+
confidence: 0.8,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
this._resolver.registerDefault('search.submit', {
|
|
321
|
+
selector: 'button[type="submit"], .search-btn',
|
|
322
|
+
strategy: 'selector',
|
|
323
|
+
confidence: 0.7,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this._resolver.registerDefault('auth.login', {
|
|
327
|
+
selector: 'form[action*="login"], #login-form',
|
|
328
|
+
strategy: 'selector',
|
|
329
|
+
confidence: 0.7,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
this._resolver.registerDefault('navigation.next', {
|
|
333
|
+
selector: 'a[rel="next"], .next-page, .pagination .next',
|
|
334
|
+
strategy: 'selector',
|
|
335
|
+
confidence: 0.7,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this._resolver.registerDefault('content.read', {
|
|
339
|
+
selector: 'main, article, .content, #content',
|
|
340
|
+
strategy: 'selector',
|
|
341
|
+
confidence: 0.8,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
getStats() {
|
|
346
|
+
return { ...this._stats };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Singleton ──────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
const executor = new Executor();
|
|
353
|
+
|
|
354
|
+
module.exports = { Executor, SemanticActionResolver, executor };
|
package/server/index.js
CHANGED
|
@@ -32,6 +32,7 @@ const premiumRoutes = require('./routes/premium');
|
|
|
32
32
|
const adminPremiumRoutes = require('./routes/admin-premium');
|
|
33
33
|
const workspaceRoutes = require('./routes/agent-workspace');
|
|
34
34
|
const universalRoutes = require('./routes/universal');
|
|
35
|
+
const runtimeRoutes = require('./routes/runtime');
|
|
35
36
|
const { handleWebhookRequest } = require('./services/stripe');
|
|
36
37
|
|
|
37
38
|
const app = express();
|
|
@@ -138,6 +139,7 @@ app.use('/api/premium', apiLimiter, premiumRoutes);
|
|
|
138
139
|
app.use('/api/admin/premium', apiLimiter, adminPremiumRoutes);
|
|
139
140
|
app.use('/api/workspace', apiLimiter, workspaceRoutes);
|
|
140
141
|
app.use('/api/universal', apiLimiter, universalRoutes);
|
|
142
|
+
app.use('/api/os', apiLimiter, runtimeRoutes);
|
|
141
143
|
|
|
142
144
|
// ─── WAB Search Engine ────────────────────────────────────────────────
|
|
143
145
|
|