opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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 +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnosis — Causal Loop (C05)
|
|
3
|
+
*
|
|
4
|
+
* Implements a closed diagnostic loop:
|
|
5
|
+
* observation → classify failure class → generate candidate causes →
|
|
6
|
+
* derive discriminating experiment → execute & observe → assess cause.
|
|
7
|
+
*
|
|
8
|
+
* The engine NEVER assumes stealth / anti-bot measures unless the evidence
|
|
9
|
+
* supports it (Chromium golden test: an executable-missing error is not
|
|
10
|
+
* labelled ANTI_BOT just because the agent feels like it).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AssessmentResult,
|
|
15
|
+
} from "../context/assessment-rules.js";
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Failure class taxonomy
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export const FailureClass = Object.freeze({
|
|
22
|
+
BROWSER_RUNTIME: "BROWSER_RUNTIME",
|
|
23
|
+
MISSING_EXECUTABLE: "MISSING_EXECUTABLE",
|
|
24
|
+
ANTI_BOT: "ANTI_BOT",
|
|
25
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
26
|
+
AUTH: "AUTH",
|
|
27
|
+
CONFIGURATION: "CONFIGURATION",
|
|
28
|
+
RATE_LIMIT: "RATE_LIMIT",
|
|
29
|
+
UNKNOWN: "UNKNOWN",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Classification rules
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ClassificationRule
|
|
38
|
+
* @property {string} class
|
|
39
|
+
* @property {RegExp[]} errorCodeMatchers
|
|
40
|
+
* @property {RegExp[]} messageMatchers
|
|
41
|
+
* @property {number[]} statusCodes
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** @type {ClassificationRule[]} */
|
|
45
|
+
const CLASSIFICATION_RULES = [
|
|
46
|
+
{
|
|
47
|
+
class: FailureClass.MISSING_EXECUTABLE,
|
|
48
|
+
errorCodeMatchers: [
|
|
49
|
+
/executable.*not.*found/i,
|
|
50
|
+
/no.*such.*file/i,
|
|
51
|
+
/enoent/i,
|
|
52
|
+
/browser.*not.*installed/i,
|
|
53
|
+
],
|
|
54
|
+
messageMatchers: [
|
|
55
|
+
/executable doesn't exist/i,
|
|
56
|
+
/browser.*launch.*failed/i,
|
|
57
|
+
/chromium.*not.*found/i,
|
|
58
|
+
/playwright.*install/i,
|
|
59
|
+
],
|
|
60
|
+
statusCodes: [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
class: FailureClass.ANTI_BOT,
|
|
64
|
+
errorCodeMatchers: [/captcha/i, /cloudflare/i, /waf/i, /forbidden/i, /access.denied/i],
|
|
65
|
+
messageMatchers: [
|
|
66
|
+
/captcha/i,
|
|
67
|
+
/cloudflare/i,
|
|
68
|
+
/forbidden/i,
|
|
69
|
+
/403 forbidden/i,
|
|
70
|
+
/access denied/i,
|
|
71
|
+
/bot detected/i,
|
|
72
|
+
/please verify/i,
|
|
73
|
+
],
|
|
74
|
+
statusCodes: [403, 429],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
class: FailureClass.RATE_LIMIT,
|
|
78
|
+
errorCodeMatchers: [/rate.?limit/i, /too.?many.?requests/i],
|
|
79
|
+
messageMatchers: [/rate.?limit/i, /too.?many.?requests/i, /quota.?exceeded/i],
|
|
80
|
+
statusCodes: [429],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
class: FailureClass.AUTH,
|
|
84
|
+
errorCodeMatchers: [/unauthorized/i, /auth/i, /token/i],
|
|
85
|
+
messageMatchers: [/unauthorized/i, /auth.*fail/i, /invalid.*token/i, /401/i],
|
|
86
|
+
statusCodes: [401, 407],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
class: FailureClass.NETWORK_ERROR,
|
|
90
|
+
errorCodeMatchers: [
|
|
91
|
+
/econnrefused/i,
|
|
92
|
+
/econnreset/i,
|
|
93
|
+
/etimedout/i,
|
|
94
|
+
/enotfound/i,
|
|
95
|
+
/network/i,
|
|
96
|
+
/dns/i,
|
|
97
|
+
],
|
|
98
|
+
messageMatchers: [
|
|
99
|
+
/connection.*refused/i,
|
|
100
|
+
/connection.*reset/i,
|
|
101
|
+
/timed.?out/i,
|
|
102
|
+
/getaddrinfo/i,
|
|
103
|
+
/network/i,
|
|
104
|
+
],
|
|
105
|
+
statusCodes: [],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
class: FailureClass.BROWSER_RUNTIME,
|
|
109
|
+
errorCodeMatchers: [
|
|
110
|
+
/browser/i,
|
|
111
|
+
/playwright/i,
|
|
112
|
+
/puppeteer/i,
|
|
113
|
+
/chromium/i,
|
|
114
|
+
/navigation.?timeout/i,
|
|
115
|
+
/page.?crash/i,
|
|
116
|
+
],
|
|
117
|
+
messageMatchers: [
|
|
118
|
+
/browser.*crash/i,
|
|
119
|
+
/page.*crash/i,
|
|
120
|
+
/navigation.*timeout/i,
|
|
121
|
+
/context.*destroyed/i,
|
|
122
|
+
],
|
|
123
|
+
statusCodes: [],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
class: FailureClass.CONFIGURATION,
|
|
127
|
+
errorCodeMatchers: [/config/i, /option/i],
|
|
128
|
+
messageMatchers: [/invalid.*config/i, /missing.*option/i, /bad.*parameter/i],
|
|
129
|
+
statusCodes: [],
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Classify a raw observation into a FailureClass.
|
|
135
|
+
* Pure function: deterministic given the same input shape.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} observation - shape: { errorCode?, message?, statusCode?, name? }
|
|
138
|
+
* @returns {string} FailureClass
|
|
139
|
+
*/
|
|
140
|
+
export function classifyObservation(observation) {
|
|
141
|
+
if (!observation || typeof observation !== "object") {
|
|
142
|
+
return FailureClass.UNKNOWN;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const errorCode = String(observation.errorCode ?? "");
|
|
146
|
+
const message = String(observation.message ?? "");
|
|
147
|
+
const name = String(observation.name ?? "");
|
|
148
|
+
const statusCode =
|
|
149
|
+
typeof observation.statusCode === "number" ? observation.statusCode : null;
|
|
150
|
+
|
|
151
|
+
const haystack = `${errorCode}\n${message}\n${name}`;
|
|
152
|
+
|
|
153
|
+
// Status-code-first: anti-bot 403 wins over a generic browser error
|
|
154
|
+
if (statusCode !== null) {
|
|
155
|
+
const statusHit = CLASSIFICATION_RULES.find(
|
|
156
|
+
(r) => r.statusCodes.includes(statusCode),
|
|
157
|
+
);
|
|
158
|
+
if (statusHit) return statusHit.class;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Pattern matchers — order matters (most specific first)
|
|
162
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
163
|
+
const codeHit = rule.errorCodeMatchers.some((rx) => rx.test(haystack));
|
|
164
|
+
const msgHit = rule.messageMatchers.some((rx) => rx.test(haystack));
|
|
165
|
+
if (codeHit || msgHit) return rule.class;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return FailureClass.UNKNOWN;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Candidate cause generation
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @typedef {Object} CandidateCause
|
|
177
|
+
* @property {string} id
|
|
178
|
+
* @property {string} failureClass
|
|
179
|
+
* @property {string} label
|
|
180
|
+
* @property {string} description
|
|
181
|
+
* @property {number} prior - base plausibility 0..1
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate plausible candidate causes for a failure class.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} failureClass
|
|
188
|
+
* @param {Object} [observation]
|
|
189
|
+
* @returns {CandidateCause[]}
|
|
190
|
+
*/
|
|
191
|
+
export function generateCandidateCauses(failureClass, observation = {}) {
|
|
192
|
+
switch (failureClass) {
|
|
193
|
+
case FailureClass.MISSING_EXECUTABLE:
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
id: "MISSING_EXECUTABLE",
|
|
197
|
+
failureClass,
|
|
198
|
+
label: "Browser executable not installed",
|
|
199
|
+
description:
|
|
200
|
+
"Playwright/Puppeteer binary is missing from the host (e.g. Chromium not downloaded).",
|
|
201
|
+
prior: 0.9,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "WRONG_EXECUTABLE_PATH",
|
|
205
|
+
failureClass,
|
|
206
|
+
label: "Browser executable path misconfigured",
|
|
207
|
+
description: "The configured executablePath points to a non-existent binary.",
|
|
208
|
+
prior: 0.2,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
case FailureClass.BROWSER_RUNTIME:
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
id: "LAUNCH_FAILED",
|
|
216
|
+
failureClass,
|
|
217
|
+
label: "Browser launch failed",
|
|
218
|
+
description: "Browser process failed to start (missing deps, sandboxing, etc.).",
|
|
219
|
+
prior: 0.7,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "PAGE_CRASH",
|
|
223
|
+
failureClass,
|
|
224
|
+
label: "Page crashed during operation",
|
|
225
|
+
description: "The page or tab crashed mid-operation (OOM, renderer kill).",
|
|
226
|
+
prior: 0.3,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "NAVIGATION_TIMEOUT",
|
|
230
|
+
failureClass,
|
|
231
|
+
label: "Navigation timeout",
|
|
232
|
+
description: "Page took too long to load (network or heavy SPA).",
|
|
233
|
+
prior: 0.3,
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
case FailureClass.ANTI_BOT:
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
id: "WAF_BLOCK",
|
|
241
|
+
failureClass,
|
|
242
|
+
label: "WAF / Cloudflare block",
|
|
243
|
+
description: "Edge WAF detected the request fingerprint and blocked it (403/captcha).",
|
|
244
|
+
prior: 0.7,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "GEO_BLOCK",
|
|
248
|
+
failureClass,
|
|
249
|
+
label: "Geo / IP block",
|
|
250
|
+
description: "The site is restricting access based on IP geography.",
|
|
251
|
+
prior: 0.2,
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
case FailureClass.NETWORK_ERROR:
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
id: "DNS_FAILURE",
|
|
259
|
+
failureClass,
|
|
260
|
+
label: "DNS resolution failure",
|
|
261
|
+
description: "Hostname could not be resolved.",
|
|
262
|
+
prior: 0.4,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "CONNECTION_REFUSED",
|
|
266
|
+
failureClass,
|
|
267
|
+
label: "Connection refused by remote",
|
|
268
|
+
description: "TCP connection actively refused (service down or filtered).",
|
|
269
|
+
prior: 0.4,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "TLS_ERROR",
|
|
273
|
+
failureClass,
|
|
274
|
+
label: "TLS / certificate error",
|
|
275
|
+
description: "TLS handshake or certificate validation failed.",
|
|
276
|
+
prior: 0.2,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
case FailureClass.AUTH:
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
id: "INVALID_CREDENTIALS",
|
|
284
|
+
failureClass,
|
|
285
|
+
label: "Invalid credentials",
|
|
286
|
+
description: "Provided credentials/token are rejected by the server.",
|
|
287
|
+
prior: 0.8,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "EXPIRED_TOKEN",
|
|
291
|
+
failureClass,
|
|
292
|
+
label: "Expired token",
|
|
293
|
+
description: "Authentication token has expired and needs refresh.",
|
|
294
|
+
prior: 0.4,
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
case FailureClass.RATE_LIMIT:
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
id: "REQUESTS_THROTTLED",
|
|
302
|
+
failureClass,
|
|
303
|
+
label: "Requests throttled",
|
|
304
|
+
description: "Server is throttling requests; backoff is required.",
|
|
305
|
+
prior: 0.9,
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
case FailureClass.CONFIGURATION:
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
id: "BAD_CONFIG",
|
|
313
|
+
failureClass,
|
|
314
|
+
label: "Invalid configuration value",
|
|
315
|
+
description: "One or more required configuration values are invalid.",
|
|
316
|
+
prior: 0.9,
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
case FailureClass.UNKNOWN:
|
|
321
|
+
default:
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
id: "UNCLASSIFIED",
|
|
325
|
+
failureClass,
|
|
326
|
+
label: "Unclassified failure",
|
|
327
|
+
description: "Failure does not match any known pattern; further investigation needed.",
|
|
328
|
+
prior: 1.0,
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
// Discriminating experiment derivation
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @typedef {Object} Experiment
|
|
340
|
+
* @property {string} kind - "shell" | "http" | "browser-probe" | "fs-probe"
|
|
341
|
+
* @property {string} candidateId
|
|
342
|
+
* @property {string} command - shell command / HTTP URL / probe path
|
|
343
|
+
* @property {Object} expectedObservation - shape passed to assessCause
|
|
344
|
+
* @property {string} description
|
|
345
|
+
*/
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Derive the MINIMAL experiment that would discriminate a candidate cause.
|
|
349
|
+
* Experiments are deliberately side-effect-free probes.
|
|
350
|
+
*
|
|
351
|
+
* @param {CandidateCause} candidate
|
|
352
|
+
* @returns {Experiment}
|
|
353
|
+
*/
|
|
354
|
+
export function deriveDiscriminatingExperiment(candidate) {
|
|
355
|
+
switch (candidate.id) {
|
|
356
|
+
case "MISSING_EXECUTABLE":
|
|
357
|
+
return {
|
|
358
|
+
kind: "shell",
|
|
359
|
+
candidateId: candidate.id,
|
|
360
|
+
command: "command -v chromium || command -v google-chrome || npx playwright --version",
|
|
361
|
+
expectedObservation: {
|
|
362
|
+
errorCode: "PRESENT",
|
|
363
|
+
message: "browser executable resolvable",
|
|
364
|
+
statusCode: 200,
|
|
365
|
+
},
|
|
366
|
+
description: "Verify a browser executable is reachable on PATH or installed via Playwright.",
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
case "WRONG_EXECUTABLE_PATH":
|
|
370
|
+
return {
|
|
371
|
+
kind: "fs-probe",
|
|
372
|
+
candidateId: candidate.id,
|
|
373
|
+
command: "stat -c '%F' ${EXECUTABLE_PATH:-/nonexistent}",
|
|
374
|
+
expectedObservation: { errorCode: "ENOENT", message: "No such file" },
|
|
375
|
+
description: "Stat the configured executablePath; expect ENOENT if misconfigured.",
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
case "LAUNCH_FAILED":
|
|
379
|
+
return {
|
|
380
|
+
kind: "shell",
|
|
381
|
+
candidateId: candidate.id,
|
|
382
|
+
command: "node -e \"require('playwright').chromium.launch().then(b=>b.close())\"",
|
|
383
|
+
expectedObservation: { errorCode: "OK", message: "launch succeeded" },
|
|
384
|
+
description: "Attempt a minimal headless launch to isolate the failure.",
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
case "PAGE_CRASH":
|
|
388
|
+
return {
|
|
389
|
+
kind: "browser-probe",
|
|
390
|
+
candidateId: candidate.id,
|
|
391
|
+
command: "goto about:blank",
|
|
392
|
+
expectedObservation: { errorCode: "OK", message: "about:blank loaded" },
|
|
393
|
+
description: "Load a trivial page; if it crashes here, browser itself is unstable.",
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
case "NAVIGATION_TIMEOUT":
|
|
397
|
+
return {
|
|
398
|
+
kind: "http",
|
|
399
|
+
candidateId: candidate.id,
|
|
400
|
+
command: "GET /",
|
|
401
|
+
expectedObservation: { statusCode: 200, message: "ok" },
|
|
402
|
+
description: "Hit the target URL via plain HTTP to confirm reachability independent of JS.",
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
case "WAF_BLOCK":
|
|
406
|
+
case "GEO_BLOCK":
|
|
407
|
+
return {
|
|
408
|
+
kind: "http",
|
|
409
|
+
candidateId: candidate.id,
|
|
410
|
+
command: "GET / -H 'User-Agent: curl/8.0'",
|
|
411
|
+
expectedObservation: { statusCode: 200, message: "ok" },
|
|
412
|
+
description:
|
|
413
|
+
"Probe with a neutral UA; if 200, the prior block was fingerprint-based.",
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
case "DNS_FAILURE":
|
|
417
|
+
return {
|
|
418
|
+
kind: "shell",
|
|
419
|
+
candidateId: candidate.id,
|
|
420
|
+
command: "getent hosts example.com",
|
|
421
|
+
expectedObservation: { errorCode: "OK", message: "host resolves" },
|
|
422
|
+
description: "Resolve DNS for the target hostname.",
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
case "CONNECTION_REFUSED":
|
|
426
|
+
return {
|
|
427
|
+
kind: "shell",
|
|
428
|
+
candidateId: candidate.id,
|
|
429
|
+
command: "nc -zv -w 2 example.com 443",
|
|
430
|
+
expectedObservation: { errorCode: "OK", message: "open" },
|
|
431
|
+
description: "TCP probe to verify the port is open from this host.",
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
case "TLS_ERROR":
|
|
435
|
+
return {
|
|
436
|
+
kind: "shell",
|
|
437
|
+
candidateId: candidate.id,
|
|
438
|
+
command: "openssl s_client -connect example.com:443 -servername example.com </dev/null",
|
|
439
|
+
expectedObservation: { errorCode: "OK", message: "verify return:1" },
|
|
440
|
+
description: "TLS handshake probe with certificate verification.",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
case "INVALID_CREDENTIALS":
|
|
444
|
+
case "EXPIRED_TOKEN":
|
|
445
|
+
return {
|
|
446
|
+
kind: "http",
|
|
447
|
+
candidateId: candidate.id,
|
|
448
|
+
command: "GET /api/me -H 'Authorization: Bearer ${TOKEN}'",
|
|
449
|
+
expectedObservation: { statusCode: 200, message: "ok" },
|
|
450
|
+
description: "Hit a known authenticated endpoint to validate the token.",
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
case "REQUESTS_THROTTLED":
|
|
454
|
+
return {
|
|
455
|
+
kind: "shell",
|
|
456
|
+
candidateId: candidate.id,
|
|
457
|
+
command: "sleep 30 && curl -sS -o /dev/null -w '%{http_code}' https://example.com/",
|
|
458
|
+
expectedObservation: { statusCode: 200, message: "ok" },
|
|
459
|
+
description: "Wait 30s and retry once; if it succeeds, throttling was the cause.",
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
case "BAD_CONFIG":
|
|
463
|
+
return {
|
|
464
|
+
kind: "fs-probe",
|
|
465
|
+
candidateId: candidate.id,
|
|
466
|
+
command: "cat ${CONFIG_PATH}",
|
|
467
|
+
expectedObservation: { errorCode: "OK", message: "config loaded" },
|
|
468
|
+
description: "Read and validate the configuration file.",
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
case "UNCLASSIFIED":
|
|
472
|
+
default:
|
|
473
|
+
return {
|
|
474
|
+
kind: "shell",
|
|
475
|
+
candidateId: candidate.id,
|
|
476
|
+
command: "echo 'no discriminating experiment defined'",
|
|
477
|
+
expectedObservation: { errorCode: "OK", message: "noop" },
|
|
478
|
+
description: "No targeted experiment available; manual investigation required.",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
484
|
+
// Cause assessment
|
|
485
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Assess whether an observed experiment result supports, contradicts, or is
|
|
489
|
+
* inconclusive with respect to a candidate cause.
|
|
490
|
+
*
|
|
491
|
+
* Decision logic:
|
|
492
|
+
* - If observation is missing/null → INCONCLUSIVE
|
|
493
|
+
* - If observation.errorCode / statusCode matches
|
|
494
|
+
* experiment.expectedObservation → SUPPORTED
|
|
495
|
+
* - If observation is the OPPOSITE of expected → CONTRADICTED
|
|
496
|
+
* - Otherwise → INCONCLUSIVE
|
|
497
|
+
*
|
|
498
|
+
* @param {CandidateCause} candidate
|
|
499
|
+
* @param {Experiment} experiment
|
|
500
|
+
* @param {Object|null} observationResult
|
|
501
|
+
* @returns {{result: string, reason: string}}
|
|
502
|
+
*/
|
|
503
|
+
export function assessCause(candidate, experiment, observationResult) {
|
|
504
|
+
if (observationResult === null || observationResult === undefined) {
|
|
505
|
+
return {
|
|
506
|
+
result: AssessmentResult.INCONCLUSIVE,
|
|
507
|
+
reason: "no observation collected yet",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const expected = experiment?.expectedObservation ?? {};
|
|
512
|
+
const observed = observationResult ?? {};
|
|
513
|
+
|
|
514
|
+
// Build two flat haystacks — one from expected, one from observed — and ask
|
|
515
|
+
// whether they overlap. We deliberately keep this lightweight: a real system
|
|
516
|
+
// would call createAssessment on the full object.
|
|
517
|
+
const expSignals = collectSignals(expected);
|
|
518
|
+
const obsSignals = collectSignals(observed);
|
|
519
|
+
|
|
520
|
+
const overlap = expSignals.filter((s) => obsSignals.includes(s));
|
|
521
|
+
const inverse = expSignals
|
|
522
|
+
.map((s) => inverseSignal(s))
|
|
523
|
+
.filter((s) => obsSignals.includes(s));
|
|
524
|
+
|
|
525
|
+
if (overlap.length > 0) {
|
|
526
|
+
return {
|
|
527
|
+
result: AssessmentResult.SUPPORTED,
|
|
528
|
+
reason: `observation matches expected signal(s): ${overlap.join(", ")}`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (inverse.length > 0) {
|
|
533
|
+
return {
|
|
534
|
+
result: AssessmentResult.CONTRADICTED,
|
|
535
|
+
reason: `observation contradicts expected signal(s): ${inverse.join(", ")}`,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
result: AssessmentResult.INCONCLUSIVE,
|
|
541
|
+
reason: "observation has no overlapping or inverse signal with expectation",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function collectSignals(obj) {
|
|
546
|
+
const out = [];
|
|
547
|
+
for (const [k, v] of Object.entries(obj ?? {})) {
|
|
548
|
+
if (v === undefined || v === null) continue;
|
|
549
|
+
out.push(`${k}:${String(v)}`);
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function inverseSignal(signal) {
|
|
555
|
+
const [k, v] = signal.split(":");
|
|
556
|
+
if (k === "errorCode" && v === "ENOENT") return "errorCode:PRESENT";
|
|
557
|
+
if (k === "errorCode" && v === "PRESENT") return "errorCode:ENOENT";
|
|
558
|
+
if (k === "statusCode") {
|
|
559
|
+
if (v === "200") return "statusCode:403";
|
|
560
|
+
if (v === "403") return "statusCode:200";
|
|
561
|
+
if (v === "401") return "statusCode:200";
|
|
562
|
+
}
|
|
563
|
+
return `__inverse_unknown__:${signal}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
567
|
+
// Orchestrator
|
|
568
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @typedef {Object} DiagnosisResult
|
|
572
|
+
* @property {string} failureClass
|
|
573
|
+
* @property {CandidateCause[]} candidates
|
|
574
|
+
* @property {Experiment|null} recommendedExperiment
|
|
575
|
+
* @property {string} summary
|
|
576
|
+
*/
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Run one iteration of the diagnostic causal loop.
|
|
580
|
+
*
|
|
581
|
+
* @param {Object} args
|
|
582
|
+
* @param {Object} args.observation - raw failure observation
|
|
583
|
+
* @param {Object} [args.previousDiagnosis] - prior diagnosis to reuse / refine
|
|
584
|
+
* @returns {DiagnosisResult}
|
|
585
|
+
*/
|
|
586
|
+
export function diagnose({ observation, previousDiagnosis = null } = {}) {
|
|
587
|
+
const failureClass = classifyObservation(observation);
|
|
588
|
+
const candidates = generateCandidateCauses(failureClass, observation);
|
|
589
|
+
|
|
590
|
+
// Re-rank candidates: prefer the most specific prior if available.
|
|
591
|
+
const ranked = rankCandidates(candidates, previousDiagnosis);
|
|
592
|
+
|
|
593
|
+
const recommendedExperiment = ranked.length > 0
|
|
594
|
+
? deriveDiscriminatingExperiment(ranked[0])
|
|
595
|
+
: null;
|
|
596
|
+
|
|
597
|
+
const summary =
|
|
598
|
+
`failure=${failureClass} candidates=${ranked.length} top=${ranked[0]?.id ?? "none"}`;
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
failureClass,
|
|
602
|
+
candidates: ranked,
|
|
603
|
+
recommendedExperiment,
|
|
604
|
+
summary,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function rankCandidates(candidates, previousDiagnosis) {
|
|
609
|
+
if (!previousDiagnosis || !Array.isArray(previousDiagnosis.candidates)) {
|
|
610
|
+
return [...candidates].sort((a, b) => b.prior - a.prior);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const priorById = new Map(
|
|
614
|
+
previousDiagnosis.candidates.map((c) => [c.id, c.prior ?? 0]),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
return [...candidates]
|
|
618
|
+
.map((c) => ({
|
|
619
|
+
...c,
|
|
620
|
+
prior: priorById.has(c.id)
|
|
621
|
+
? (c.prior ?? 0) + (priorById.get(c.id) ?? 0)
|
|
622
|
+
: c.prior ?? 0,
|
|
623
|
+
}))
|
|
624
|
+
.sort((a, b) => b.prior - a.prior);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
628
|
+
// Convenience export: anti-bot heuristic guard (used by golden test)
|
|
629
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Determine whether anti-bot / stealth measures are SUPPORTED for a given
|
|
633
|
+
* diagnosis. The rule: ANTI_BOT is only supported when at least one candidate
|
|
634
|
+
* has the WAF_BLOCK / GEO_BLOCK class AND the failure class itself is ANTI_BOT.
|
|
635
|
+
*
|
|
636
|
+
* If the failure class is BROWSER_RUNTIME / MISSING_EXECUTABLE, anti-bot is
|
|
637
|
+
* explicitly NOT supported (the agent must NOT assume stealth).
|
|
638
|
+
*
|
|
639
|
+
* @param {DiagnosisResult} diagnosis
|
|
640
|
+
* @returns {"SUPPORTED" | "NOT_SUPPORTED" | "INCONCLUSIVE"}
|
|
641
|
+
*/
|
|
642
|
+
export function antiBotSupport(diagnosis) {
|
|
643
|
+
if (!diagnosis || typeof diagnosis !== "object") return "INCONCLUSIVE";
|
|
644
|
+
|
|
645
|
+
if (diagnosis.failureClass === FailureClass.ANTI_BOT) {
|
|
646
|
+
return "SUPPORTED";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// If the actual evidence is browser-runtime (e.g. missing executable), the
|
|
650
|
+
// agent must NOT auto-assume stealth/anti-bot measures are needed.
|
|
651
|
+
if (
|
|
652
|
+
diagnosis.failureClass === FailureClass.BROWSER_RUNTIME ||
|
|
653
|
+
diagnosis.failureClass === FailureClass.MISSING_EXECUTABLE
|
|
654
|
+
) {
|
|
655
|
+
return "NOT_SUPPORTED";
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return "INCONCLUSIVE";
|
|
659
|
+
}
|