web-agent-bridge 3.4.0 → 3.9.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 +84 -84
- package/README.ar.md +1565 -1304
- package/README.md +171 -298
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -237
- package/bin/wab-init.js +244 -223
- package/bin/wab.js +80 -80
- package/examples/azure-dns-wab.js +83 -83
- package/examples/bidi-agent.js +119 -119
- package/examples/cloudflare-wab-dns.js +121 -121
- package/examples/cpanel-wab-dns.js +114 -114
- package/examples/cross-site-agent.js +91 -91
- package/examples/dns-discovery-agent.js +166 -166
- package/examples/gcp-dns-wab.js +76 -76
- package/examples/governance-agent.js +169 -169
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/plesk-wab-dns.js +103 -103
- package/examples/puppeteer-agent.js +108 -108
- package/examples/route53-wab-dns.js +144 -144
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/safe-mode-agent.js +96 -96
- package/examples/self-discovery.js +106 -0
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wab-sign.js +74 -74
- package/examples/wab-verify.js +60 -60
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +93 -93
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -8
- package/public/.well-known/wab.json +28 -28
- package/public/activate.html +448 -368
- package/public/adopt.html +236 -0
- package/public/adoption-metrics.html +188 -188
- package/public/agent-workspace.html +359 -349
- package/public/ai.html +198 -198
- package/public/api.html +397 -413
- package/public/atp.html +171 -0
- package/public/azure-dns-integration.html +289 -289
- package/public/browser.html +486 -486
- package/public/cloudflare-integration.html +380 -380
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/cpanel-integration.html +398 -398
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1401 -1263
- package/public/dashboard-shieldlink.html +295 -0
- package/public/dashboard.html +711 -707
- package/public/dns.html +436 -436
- package/public/docs.html +588 -588
- package/public/enterprise-mesh.ar.html +80 -0
- package/public/enterprise-mesh.html +81 -0
- package/public/feed.xml +89 -89
- package/public/gcp-dns-integration.html +318 -318
- package/public/governance.ar.html +70 -0
- package/public/governance.html +69 -0
- package/public/growth.html +465 -465
- package/public/index.html +1372 -1266
- package/public/integrations.html +556 -556
- package/public/js/activate.js +449 -145
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +117 -65
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/dns.js +438 -438
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/l-preview.html +242 -0
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/milestones.html +346 -0
- package/public/one-click.html +779 -0
- package/public/openapi.json +669 -669
- package/public/partners.ar.html +145 -0
- package/public/partners.html +143 -0
- package/public/phone-shield.html +281 -281
- package/public/plesk-integration.html +375 -375
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/provider-onboarding.html +172 -172
- package/public/provider-sandbox.html +134 -134
- package/public/providers.html +359 -359
- package/public/refusals.html +172 -0
- package/public/register.html +105 -105
- package/public/registrar-integrations.html +141 -141
- package/public/ring4.html +292 -0
- package/public/robots.txt +99 -99
- package/public/route53-integration.html +531 -531
- package/public/score.html +263 -0
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -8
- package/public/shieldlink.html +244 -0
- package/public/shieldqr.html +231 -231
- package/public/sitemap.xml +13 -1
- package/public/terms.html +256 -256
- package/public/trust-graph-api.ar.html +92 -0
- package/public/trust-graph-api.html +91 -0
- package/public/wab-features.html +560 -0
- package/public/wab-trust.html +200 -200
- package/public/wab-truth.html +375 -0
- package/public/wab-vs-protocols.html +210 -210
- package/public/whitepaper.html +449 -449
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/atp.js +103 -0
- package/sdk/auto-discovery.js +301 -288
- package/sdk/commander.js +262 -262
- package/sdk/governance.js +262 -262
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +653 -649
- package/sdk/multi-agent.js +318 -318
- package/sdk/safe-mode.js +221 -221
- package/sdk/safety-shield.js +219 -219
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +412 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +793 -670
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/api-tier.js +170 -0
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -157
- package/server/middleware/wab-trust.js +141 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/migrations/006_growth_suite.sql +138 -0
- package/server/migrations/007_governance.sql +106 -106
- package/server/migrations/008_plans.sql +144 -144
- package/server/migrations/009_shieldqr.sql +30 -30
- package/server/migrations/010_extended_trust.sql +33 -33
- package/server/migrations/011_outreach.sql +47 -0
- package/server/migrations/012_shieldlink.sql +116 -0
- package/server/migrations/013_ct_monitor.sql +13 -0
- package/server/migrations/014_wab_advanced_features.sql +128 -0
- package/server/migrations/015_wab_truth_layer.sql +101 -0
- package/server/migrations/016_ring4_external_trust.sql +84 -0
- package/server/migrations/017_ring4_extensions.sql +69 -0
- package/server/migrations/018_commercial_foundations.sql +167 -0
- package/server/migrations/019_unify_tier_constraints.sql +133 -0
- package/server/migrations/020_agent_transaction_primitive.sql +119 -0
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +740 -740
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/activate.js +478 -0
- package/server/routes/admin-outreach.js +239 -0
- package/server/routes/admin-plans.js +76 -76
- package/server/routes/admin-premium.js +674 -673
- package/server/routes/admin-shieldlink.js +137 -0
- package/server/routes/admin-shieldqr.js +90 -90
- package/server/routes/admin-trust-monitor.js +139 -83
- package/server/routes/admin.js +550 -549
- package/server/routes/adopt.js +61 -0
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api-keys.js +127 -0
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +57 -57
- package/server/routes/commander.js +316 -316
- package/server/routes/customer-shieldlink.js +133 -0
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -154
- package/server/routes/diagnose.js +373 -0
- package/server/routes/discovery.js +2348 -2348
- package/server/routes/enterprise-mesh.js +170 -0
- package/server/routes/gateway.js +173 -173
- package/server/routes/governance-saas.js +203 -0
- package/server/routes/governance.js +208 -208
- package/server/routes/growth.js +1048 -0
- package/server/routes/intent.js +328 -0
- package/server/routes/license.js +251 -251
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/partners.js +201 -0
- package/server/routes/plans.js +33 -33
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/providers.js +650 -650
- package/server/routes/reputation.js +411 -0
- package/server/routes/ring4.js +885 -0
- package/server/routes/runtime.js +2148 -2148
- package/server/routes/shieldlink.js +70 -0
- package/server/routes/shieldqr.js +88 -88
- package/server/routes/sovereign.js +465 -465
- package/server/routes/transactions.js +233 -0
- package/server/routes/truth-layer.js +670 -0
- package/server/routes/universal.js +200 -200
- package/server/routes/unsubscribe.js +51 -0
- package/server/routes/wab-api.js +850 -850
- package/server/routes/wab-cache.js +282 -0
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/secrets/wab-signing-key.pem +3 -0
- package/server/secrets/wab-signing-pub.pem +3 -0
- package/server/security/cross-site-redactor.js +196 -196
- package/server/security/dry-run.js +180 -180
- package/server/security/human-gate-rate-limit.js +147 -147
- package/server/security/human-gate-transports.js +178 -178
- package/server/security/human-gate.js +281 -281
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -245
- package/server/security/reward-guard.js +171 -171
- package/server/security/rollback-store.js +239 -239
- package/server/security/token-scope.js +404 -404
- package/server/security/url-policy.js +139 -139
- package/server/services/adoption-agent.js +182 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -601
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -555
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -292
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +233 -233
- package/server/services/fairness-engine.js +409 -0
- package/server/services/fairness.js +420 -0
- package/server/services/governance.js +466 -466
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/outreach-agent.js +312 -0
- package/server/services/plans.js +214 -214
- package/server/services/plugins.js +771 -771
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/provider-clients.js +740 -740
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/shieldlink.js +492 -0
- package/server/services/shieldqr.js +322 -322
- package/server/services/sovereign-shield.js +542 -542
- package/server/services/ssl-ct-monitor.js +224 -0
- package/server/services/ssl-inspector.js +42 -42
- package/server/services/ssl-monitor.js +167 -167
- package/server/services/stripe.js +206 -205
- package/server/services/swarm.js +788 -788
- package/server/services/transactions.js +525 -0
- package/server/services/universal-scraper.js +662 -662
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/services/wab-crypto.js +178 -178
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -228
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/templates/ring4/banking-sovereign.yaml +55 -0
- package/templates/ring4/ecommerce-sovereign.yaml +58 -0
- package/templates/ring4/healthcare-sovereign.yaml +60 -0
|
@@ -1,245 +1,245 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WAB Safety Shield — Intent Analysis Engine (SPEC §8.12)
|
|
5
|
-
*
|
|
6
|
-
* Deterministic, dependency-free risk scorer that runs on every agent
|
|
7
|
-
* action AFTER scope/dry-run/human-gate. The engine looks at the
|
|
8
|
-
* COMPOSITION of the request (verb class, target environment, magnitude,
|
|
9
|
-
* danger tokens in params, recent agent behaviour) and decides whether
|
|
10
|
-
* to ALLOW, escalate to DRY_RUN, escalate to HUMAN_GATE, or BLOCK.
|
|
11
|
-
*
|
|
12
|
-
* Why a separate layer?
|
|
13
|
-
* - Scope tokens enforce *what the token is allowed to do*.
|
|
14
|
-
* - Dry-run forces a 2-step preview for known destructive verbs.
|
|
15
|
-
* - Human gate adds OOB approval for Pro+ deployments.
|
|
16
|
-
* - The Intent Engine catches panic patterns that pass all three:
|
|
17
|
-
* e.g. an agent that just got an error tries `delete-all + force`
|
|
18
|
-
* on a production database — score=critical → block, even if the
|
|
19
|
-
* token has admin scope.
|
|
20
|
-
*
|
|
21
|
-
* Scoring is bounded 0..100. The engine returns the highest-severity
|
|
22
|
-
* required gate; the route is responsible for enforcing it.
|
|
23
|
-
*
|
|
24
|
-
* The engine is intentionally pure & synchronous so it can run on the
|
|
25
|
-
* hot path without I/O. A tiny in-memory ring buffer tracks per-actor
|
|
26
|
-
* velocity & burst patterns over the last 60 seconds.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
const DEFAULT_THRESHOLDS = Object.freeze({
|
|
30
|
-
low: 30, // 0-29 → allow
|
|
31
|
-
medium: 70, // 30-69 → require dry-run
|
|
32
|
-
high: 90, // 70-89 → require human gate
|
|
33
|
-
// 90+ → block outright
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const DANGER_TOKENS = [
|
|
37
|
-
'force', 'skip-checks', 'skip_checks', 'no-backup', 'no_backup',
|
|
38
|
-
'permanent', 'permanently', 'irrecoverable', 'cascade', 'recursive',
|
|
39
|
-
'all', 'everything', 'wildcard', '--force', '--yes', 'confirm-destroy',
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
const DESTRUCTIVE_VERB_PATTERNS = [
|
|
43
|
-
/^(delete|drop|destroy|wipe|purge|truncate|remove|erase|annihilate)/i,
|
|
44
|
-
/^(uninstall|deprovision|terminate)/i,
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const WRITE_VERB_PATTERNS = [
|
|
48
|
-
/^(create|update|modify|patch|set|write|insert|put|post|publish|deploy|merge|push)/i,
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const ENV_WEIGHTS = Object.freeze({
|
|
52
|
-
production: 30,
|
|
53
|
-
prod: 30,
|
|
54
|
-
live: 30,
|
|
55
|
-
staging: 10,
|
|
56
|
-
preview: 5,
|
|
57
|
-
development: 0,
|
|
58
|
-
dev: 0,
|
|
59
|
-
test: 0,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ─── per-actor velocity tracker ──────────────────────────────────────
|
|
63
|
-
// actorKey -> [{ ts, action, env, score }]
|
|
64
|
-
const _history = new Map();
|
|
65
|
-
const HISTORY_WINDOW_MS = 60_000;
|
|
66
|
-
const HISTORY_MAX_PER_ACTOR = 50;
|
|
67
|
-
|
|
68
|
-
function _trim(arr) {
|
|
69
|
-
const cutoff = Date.now() - HISTORY_WINDOW_MS;
|
|
70
|
-
while (arr.length && arr[0].ts < cutoff) arr.shift();
|
|
71
|
-
while (arr.length > HISTORY_MAX_PER_ACTOR) arr.shift();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function _record(actorKey, entry) {
|
|
75
|
-
if (!actorKey) return;
|
|
76
|
-
let arr = _history.get(actorKey);
|
|
77
|
-
if (!arr) { arr = []; _history.set(actorKey, arr); }
|
|
78
|
-
arr.push(entry);
|
|
79
|
-
_trim(arr);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function _getHistory(actorKey) {
|
|
83
|
-
if (!actorKey) return [];
|
|
84
|
-
const arr = _history.get(actorKey);
|
|
85
|
-
if (!arr) return [];
|
|
86
|
-
_trim(arr);
|
|
87
|
-
return arr;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ─── helpers ─────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
function _classifyVerb(actionName) {
|
|
93
|
-
if (!actionName) return 'unknown';
|
|
94
|
-
if (DESTRUCTIVE_VERB_PATTERNS.some((re) => re.test(actionName))) return 'destructive';
|
|
95
|
-
if (WRITE_VERB_PATTERNS.some((re) => re.test(actionName))) return 'write';
|
|
96
|
-
return 'read';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function _envWeight(env) {
|
|
100
|
-
return ENV_WEIGHTS[String(env || 'production').toLowerCase()] ?? 30;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function _flatStrings(value, out = [], depth = 0) {
|
|
104
|
-
if (depth > 4 || value == null) return out;
|
|
105
|
-
if (typeof value === 'string') { out.push(value.toLowerCase()); return out; }
|
|
106
|
-
if (typeof value === 'number' || typeof value === 'boolean') { out.push(String(value).toLowerCase()); return out; }
|
|
107
|
-
if (Array.isArray(value)) { for (const v of value) _flatStrings(v, out, depth + 1); return out; }
|
|
108
|
-
if (typeof value === 'object') {
|
|
109
|
-
for (const [k, v] of Object.entries(value)) {
|
|
110
|
-
out.push(String(k).toLowerCase());
|
|
111
|
-
_flatStrings(v, out, depth + 1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function _detectDangerTokens(params) {
|
|
118
|
-
const flat = _flatStrings(params || {});
|
|
119
|
-
const found = new Set();
|
|
120
|
-
for (const s of flat) {
|
|
121
|
-
if (s === 'true') continue;
|
|
122
|
-
for (const t of DANGER_TOKENS) {
|
|
123
|
-
if (s === t || s.includes(t)) { found.add(t); break; }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return [...found];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function _magnitudeSignal(params) {
|
|
130
|
-
const flat = _flatStrings(params || {});
|
|
131
|
-
let score = 0;
|
|
132
|
-
const reasons = [];
|
|
133
|
-
// Wildcards
|
|
134
|
-
for (const s of flat) {
|
|
135
|
-
if (s === '*' || s === '%' || s === 'all' || s === 'everything') {
|
|
136
|
-
score += 15; reasons.push(`wildcard:"${s}"`); break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// Large arrays anywhere → bulk operation
|
|
140
|
-
function walk(v, depth = 0) {
|
|
141
|
-
if (depth > 4 || v == null) return;
|
|
142
|
-
if (Array.isArray(v)) {
|
|
143
|
-
if (v.length >= 20) { score += 10; reasons.push(`large_array:${v.length}`); }
|
|
144
|
-
for (const x of v) walk(x, depth + 1);
|
|
145
|
-
} else if (typeof v === 'object') {
|
|
146
|
-
for (const x of Object.values(v)) walk(x, depth + 1);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
walk(params || {});
|
|
150
|
-
return { score: Math.min(score, 30), reasons };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── public API ──────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Score a request. Returns:
|
|
157
|
-
* {
|
|
158
|
-
* score: number (0..100, capped),
|
|
159
|
-
* level: 'low' | 'medium' | 'high' | 'critical',
|
|
160
|
-
* verb_class:'read'|'write'|'destructive'|'unknown',
|
|
161
|
-
* required_gate: null | 'dry_run' | 'human_gate' | 'block',
|
|
162
|
-
* reasons: string[],
|
|
163
|
-
* rewrites: Array<{from:string, to:string, reason:string}>,
|
|
164
|
-
* }
|
|
165
|
-
*
|
|
166
|
-
* Inputs:
|
|
167
|
-
* ctx = { actorId, sessionToken, siteId, actionName, params, env, tier }
|
|
168
|
-
* siteConfig may override thresholds via siteConfig.intentEngine = {
|
|
169
|
-
* thresholds:{ low,medium,high }, weights:{...},
|
|
170
|
-
* dangerTokens:[...], rewrites:{ from->to }
|
|
171
|
-
* }
|
|
172
|
-
*/
|
|
173
|
-
function score(ctx, siteConfig = {}) {
|
|
174
|
-
const cfg = siteConfig.intentEngine || {};
|
|
175
|
-
const thresholds = { ...DEFAULT_THRESHOLDS, ...(cfg.thresholds || {}) };
|
|
176
|
-
const reasons = [];
|
|
177
|
-
const rewrites = [];
|
|
178
|
-
let s = 0;
|
|
179
|
-
|
|
180
|
-
const verb = _classifyVerb(ctx.actionName);
|
|
181
|
-
if (verb === 'destructive') { s += 50; reasons.push('verb:destructive'); }
|
|
182
|
-
else if (verb === 'write') { s += 10; reasons.push('verb:write'); }
|
|
183
|
-
|
|
184
|
-
const env = ctx.env || siteConfig.environment || 'production';
|
|
185
|
-
const ew = _envWeight(env);
|
|
186
|
-
// Reads on production are not by themselves risky — only weight env for write/destructive.
|
|
187
|
-
if (ew > 0 && verb !== 'read') { s += ew; reasons.push(`env:${env}(+${ew})`); }
|
|
188
|
-
|
|
189
|
-
const danger = _detectDangerTokens(ctx.params);
|
|
190
|
-
if (danger.length) { s += Math.min(30, danger.length * 15); reasons.push(`danger_tokens:${danger.join(',')}`); }
|
|
191
|
-
|
|
192
|
-
const mag = _magnitudeSignal(ctx.params);
|
|
193
|
-
if (mag.score) { s += mag.score; reasons.push(...mag.reasons); }
|
|
194
|
-
|
|
195
|
-
// Burst / velocity: count similar destructive actions in window
|
|
196
|
-
const actorKey = ctx.actorId || ctx.sessionToken || ctx.siteId || 'anon';
|
|
197
|
-
const history = _getHistory(actorKey);
|
|
198
|
-
const recentDestructive = history.filter((h) => h.verb === 'destructive').length;
|
|
199
|
-
if (recentDestructive >= 3) { s += 15; reasons.push(`burst:${recentDestructive}_destructive_in_60s`); }
|
|
200
|
-
const last = history[history.length - 1];
|
|
201
|
-
if (last && Date.now() - last.ts < 1000 && verb !== 'read') {
|
|
202
|
-
s += 10; reasons.push('velocity:<1s_since_last');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Optional site rewrites: e.g. "delete-account" -> "deactivate-account"
|
|
206
|
-
const rwMap = cfg.rewrites || {};
|
|
207
|
-
if (rwMap[ctx.actionName]) {
|
|
208
|
-
rewrites.push({
|
|
209
|
-
from: ctx.actionName, to: rwMap[ctx.actionName],
|
|
210
|
-
reason: 'site policy prefers a reversible alternative',
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Heuristic safe-rewrite for destructive verbs touching well-known nouns.
|
|
215
|
-
if (verb === 'destructive' && /account|user|invoice|order/i.test(ctx.actionName) && !rwMap[ctx.actionName]) {
|
|
216
|
-
const safe = ctx.actionName.replace(/^(delete|destroy|wipe|purge|remove)/i, 'archive');
|
|
217
|
-
if (safe !== ctx.actionName) rewrites.push({ from: ctx.actionName, to: safe, reason: 'archive-instead-of-delete fallback' });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Cap and classify
|
|
221
|
-
s = Math.min(100, Math.max(0, Math.round(s)));
|
|
222
|
-
let level = 'low';
|
|
223
|
-
let required_gate = null;
|
|
224
|
-
if (s >= thresholds.high) { level = 'critical'; required_gate = 'block'; }
|
|
225
|
-
else if (s >= thresholds.medium) { level = 'high'; required_gate = 'human_gate'; }
|
|
226
|
-
else if (s >= thresholds.low) { level = 'medium'; required_gate = 'dry_run'; }
|
|
227
|
-
|
|
228
|
-
// Record for next call
|
|
229
|
-
_record(actorKey, { ts: Date.now(), action: ctx.actionName, env, verb, score: s });
|
|
230
|
-
|
|
231
|
-
return { score: s, level, verb_class: verb, required_gate, reasons, rewrites };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function _resetForTests() {
|
|
235
|
-
_history.clear();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
module.exports = {
|
|
239
|
-
score,
|
|
240
|
-
_classifyVerb,
|
|
241
|
-
_detectDangerTokens,
|
|
242
|
-
_resetForTests,
|
|
243
|
-
DEFAULT_THRESHOLDS,
|
|
244
|
-
DANGER_TOKENS,
|
|
245
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Safety Shield — Intent Analysis Engine (SPEC §8.12)
|
|
5
|
+
*
|
|
6
|
+
* Deterministic, dependency-free risk scorer that runs on every agent
|
|
7
|
+
* action AFTER scope/dry-run/human-gate. The engine looks at the
|
|
8
|
+
* COMPOSITION of the request (verb class, target environment, magnitude,
|
|
9
|
+
* danger tokens in params, recent agent behaviour) and decides whether
|
|
10
|
+
* to ALLOW, escalate to DRY_RUN, escalate to HUMAN_GATE, or BLOCK.
|
|
11
|
+
*
|
|
12
|
+
* Why a separate layer?
|
|
13
|
+
* - Scope tokens enforce *what the token is allowed to do*.
|
|
14
|
+
* - Dry-run forces a 2-step preview for known destructive verbs.
|
|
15
|
+
* - Human gate adds OOB approval for Pro+ deployments.
|
|
16
|
+
* - The Intent Engine catches panic patterns that pass all three:
|
|
17
|
+
* e.g. an agent that just got an error tries `delete-all + force`
|
|
18
|
+
* on a production database — score=critical → block, even if the
|
|
19
|
+
* token has admin scope.
|
|
20
|
+
*
|
|
21
|
+
* Scoring is bounded 0..100. The engine returns the highest-severity
|
|
22
|
+
* required gate; the route is responsible for enforcing it.
|
|
23
|
+
*
|
|
24
|
+
* The engine is intentionally pure & synchronous so it can run on the
|
|
25
|
+
* hot path without I/O. A tiny in-memory ring buffer tracks per-actor
|
|
26
|
+
* velocity & burst patterns over the last 60 seconds.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const DEFAULT_THRESHOLDS = Object.freeze({
|
|
30
|
+
low: 30, // 0-29 → allow
|
|
31
|
+
medium: 70, // 30-69 → require dry-run
|
|
32
|
+
high: 90, // 70-89 → require human gate
|
|
33
|
+
// 90+ → block outright
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const DANGER_TOKENS = [
|
|
37
|
+
'force', 'skip-checks', 'skip_checks', 'no-backup', 'no_backup',
|
|
38
|
+
'permanent', 'permanently', 'irrecoverable', 'cascade', 'recursive',
|
|
39
|
+
'all', 'everything', 'wildcard', '--force', '--yes', 'confirm-destroy',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const DESTRUCTIVE_VERB_PATTERNS = [
|
|
43
|
+
/^(delete|drop|destroy|wipe|purge|truncate|remove|erase|annihilate)/i,
|
|
44
|
+
/^(uninstall|deprovision|terminate)/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const WRITE_VERB_PATTERNS = [
|
|
48
|
+
/^(create|update|modify|patch|set|write|insert|put|post|publish|deploy|merge|push)/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const ENV_WEIGHTS = Object.freeze({
|
|
52
|
+
production: 30,
|
|
53
|
+
prod: 30,
|
|
54
|
+
live: 30,
|
|
55
|
+
staging: 10,
|
|
56
|
+
preview: 5,
|
|
57
|
+
development: 0,
|
|
58
|
+
dev: 0,
|
|
59
|
+
test: 0,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── per-actor velocity tracker ──────────────────────────────────────
|
|
63
|
+
// actorKey -> [{ ts, action, env, score }]
|
|
64
|
+
const _history = new Map();
|
|
65
|
+
const HISTORY_WINDOW_MS = 60_000;
|
|
66
|
+
const HISTORY_MAX_PER_ACTOR = 50;
|
|
67
|
+
|
|
68
|
+
function _trim(arr) {
|
|
69
|
+
const cutoff = Date.now() - HISTORY_WINDOW_MS;
|
|
70
|
+
while (arr.length && arr[0].ts < cutoff) arr.shift();
|
|
71
|
+
while (arr.length > HISTORY_MAX_PER_ACTOR) arr.shift();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _record(actorKey, entry) {
|
|
75
|
+
if (!actorKey) return;
|
|
76
|
+
let arr = _history.get(actorKey);
|
|
77
|
+
if (!arr) { arr = []; _history.set(actorKey, arr); }
|
|
78
|
+
arr.push(entry);
|
|
79
|
+
_trim(arr);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _getHistory(actorKey) {
|
|
83
|
+
if (!actorKey) return [];
|
|
84
|
+
const arr = _history.get(actorKey);
|
|
85
|
+
if (!arr) return [];
|
|
86
|
+
_trim(arr);
|
|
87
|
+
return arr;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── helpers ─────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function _classifyVerb(actionName) {
|
|
93
|
+
if (!actionName) return 'unknown';
|
|
94
|
+
if (DESTRUCTIVE_VERB_PATTERNS.some((re) => re.test(actionName))) return 'destructive';
|
|
95
|
+
if (WRITE_VERB_PATTERNS.some((re) => re.test(actionName))) return 'write';
|
|
96
|
+
return 'read';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _envWeight(env) {
|
|
100
|
+
return ENV_WEIGHTS[String(env || 'production').toLowerCase()] ?? 30;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _flatStrings(value, out = [], depth = 0) {
|
|
104
|
+
if (depth > 4 || value == null) return out;
|
|
105
|
+
if (typeof value === 'string') { out.push(value.toLowerCase()); return out; }
|
|
106
|
+
if (typeof value === 'number' || typeof value === 'boolean') { out.push(String(value).toLowerCase()); return out; }
|
|
107
|
+
if (Array.isArray(value)) { for (const v of value) _flatStrings(v, out, depth + 1); return out; }
|
|
108
|
+
if (typeof value === 'object') {
|
|
109
|
+
for (const [k, v] of Object.entries(value)) {
|
|
110
|
+
out.push(String(k).toLowerCase());
|
|
111
|
+
_flatStrings(v, out, depth + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _detectDangerTokens(params) {
|
|
118
|
+
const flat = _flatStrings(params || {});
|
|
119
|
+
const found = new Set();
|
|
120
|
+
for (const s of flat) {
|
|
121
|
+
if (s === 'true') continue;
|
|
122
|
+
for (const t of DANGER_TOKENS) {
|
|
123
|
+
if (s === t || s.includes(t)) { found.add(t); break; }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...found];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _magnitudeSignal(params) {
|
|
130
|
+
const flat = _flatStrings(params || {});
|
|
131
|
+
let score = 0;
|
|
132
|
+
const reasons = [];
|
|
133
|
+
// Wildcards
|
|
134
|
+
for (const s of flat) {
|
|
135
|
+
if (s === '*' || s === '%' || s === 'all' || s === 'everything') {
|
|
136
|
+
score += 15; reasons.push(`wildcard:"${s}"`); break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Large arrays anywhere → bulk operation
|
|
140
|
+
function walk(v, depth = 0) {
|
|
141
|
+
if (depth > 4 || v == null) return;
|
|
142
|
+
if (Array.isArray(v)) {
|
|
143
|
+
if (v.length >= 20) { score += 10; reasons.push(`large_array:${v.length}`); }
|
|
144
|
+
for (const x of v) walk(x, depth + 1);
|
|
145
|
+
} else if (typeof v === 'object') {
|
|
146
|
+
for (const x of Object.values(v)) walk(x, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
walk(params || {});
|
|
150
|
+
return { score: Math.min(score, 30), reasons };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── public API ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Score a request. Returns:
|
|
157
|
+
* {
|
|
158
|
+
* score: number (0..100, capped),
|
|
159
|
+
* level: 'low' | 'medium' | 'high' | 'critical',
|
|
160
|
+
* verb_class:'read'|'write'|'destructive'|'unknown',
|
|
161
|
+
* required_gate: null | 'dry_run' | 'human_gate' | 'block',
|
|
162
|
+
* reasons: string[],
|
|
163
|
+
* rewrites: Array<{from:string, to:string, reason:string}>,
|
|
164
|
+
* }
|
|
165
|
+
*
|
|
166
|
+
* Inputs:
|
|
167
|
+
* ctx = { actorId, sessionToken, siteId, actionName, params, env, tier }
|
|
168
|
+
* siteConfig may override thresholds via siteConfig.intentEngine = {
|
|
169
|
+
* thresholds:{ low,medium,high }, weights:{...},
|
|
170
|
+
* dangerTokens:[...], rewrites:{ from->to }
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
function score(ctx, siteConfig = {}) {
|
|
174
|
+
const cfg = siteConfig.intentEngine || {};
|
|
175
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...(cfg.thresholds || {}) };
|
|
176
|
+
const reasons = [];
|
|
177
|
+
const rewrites = [];
|
|
178
|
+
let s = 0;
|
|
179
|
+
|
|
180
|
+
const verb = _classifyVerb(ctx.actionName);
|
|
181
|
+
if (verb === 'destructive') { s += 50; reasons.push('verb:destructive'); }
|
|
182
|
+
else if (verb === 'write') { s += 10; reasons.push('verb:write'); }
|
|
183
|
+
|
|
184
|
+
const env = ctx.env || siteConfig.environment || 'production';
|
|
185
|
+
const ew = _envWeight(env);
|
|
186
|
+
// Reads on production are not by themselves risky — only weight env for write/destructive.
|
|
187
|
+
if (ew > 0 && verb !== 'read') { s += ew; reasons.push(`env:${env}(+${ew})`); }
|
|
188
|
+
|
|
189
|
+
const danger = _detectDangerTokens(ctx.params);
|
|
190
|
+
if (danger.length) { s += Math.min(30, danger.length * 15); reasons.push(`danger_tokens:${danger.join(',')}`); }
|
|
191
|
+
|
|
192
|
+
const mag = _magnitudeSignal(ctx.params);
|
|
193
|
+
if (mag.score) { s += mag.score; reasons.push(...mag.reasons); }
|
|
194
|
+
|
|
195
|
+
// Burst / velocity: count similar destructive actions in window
|
|
196
|
+
const actorKey = ctx.actorId || ctx.sessionToken || ctx.siteId || 'anon';
|
|
197
|
+
const history = _getHistory(actorKey);
|
|
198
|
+
const recentDestructive = history.filter((h) => h.verb === 'destructive').length;
|
|
199
|
+
if (recentDestructive >= 3) { s += 15; reasons.push(`burst:${recentDestructive}_destructive_in_60s`); }
|
|
200
|
+
const last = history[history.length - 1];
|
|
201
|
+
if (last && Date.now() - last.ts < 1000 && verb !== 'read') {
|
|
202
|
+
s += 10; reasons.push('velocity:<1s_since_last');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Optional site rewrites: e.g. "delete-account" -> "deactivate-account"
|
|
206
|
+
const rwMap = cfg.rewrites || {};
|
|
207
|
+
if (rwMap[ctx.actionName]) {
|
|
208
|
+
rewrites.push({
|
|
209
|
+
from: ctx.actionName, to: rwMap[ctx.actionName],
|
|
210
|
+
reason: 'site policy prefers a reversible alternative',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Heuristic safe-rewrite for destructive verbs touching well-known nouns.
|
|
215
|
+
if (verb === 'destructive' && /account|user|invoice|order/i.test(ctx.actionName) && !rwMap[ctx.actionName]) {
|
|
216
|
+
const safe = ctx.actionName.replace(/^(delete|destroy|wipe|purge|remove)/i, 'archive');
|
|
217
|
+
if (safe !== ctx.actionName) rewrites.push({ from: ctx.actionName, to: safe, reason: 'archive-instead-of-delete fallback' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Cap and classify
|
|
221
|
+
s = Math.min(100, Math.max(0, Math.round(s)));
|
|
222
|
+
let level = 'low';
|
|
223
|
+
let required_gate = null;
|
|
224
|
+
if (s >= thresholds.high) { level = 'critical'; required_gate = 'block'; }
|
|
225
|
+
else if (s >= thresholds.medium) { level = 'high'; required_gate = 'human_gate'; }
|
|
226
|
+
else if (s >= thresholds.low) { level = 'medium'; required_gate = 'dry_run'; }
|
|
227
|
+
|
|
228
|
+
// Record for next call
|
|
229
|
+
_record(actorKey, { ts: Date.now(), action: ctx.actionName, env, verb, score: s });
|
|
230
|
+
|
|
231
|
+
return { score: s, level, verb_class: verb, required_gate, reasons, rewrites };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _resetForTests() {
|
|
235
|
+
_history.clear();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
score,
|
|
240
|
+
_classifyVerb,
|
|
241
|
+
_detectDangerTokens,
|
|
242
|
+
_resetForTests,
|
|
243
|
+
DEFAULT_THRESHOLDS,
|
|
244
|
+
DANGER_TOKENS,
|
|
245
|
+
};
|