@trucore/openclaw-atf 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -12
- package/examples/README.md +134 -0
- package/examples/disable-plugin.json +3 -0
- package/examples/full-config.json +12 -0
- package/examples/minimal-enable.json +5 -0
- package/examples/prefer-api.json +8 -0
- package/examples/prefer-cli.json +7 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
- package/src/adoption_advisor.mjs +425 -0
- package/src/backend.mjs +230 -0
- package/src/billing_claim.mjs +630 -0
- package/src/config.mjs +317 -0
- package/src/contracts/deny_codes.mjs +217 -0
- package/src/contracts/index.mjs +52 -0
- package/src/contracts/result_builder.mjs +132 -0
- package/src/contracts/schemas.mjs +402 -0
- package/src/contracts/status_codes.mjs +148 -0
- package/src/doctor.mjs +207 -0
- package/src/index.mjs +1168 -70
- package/src/tool_response.mjs +181 -0
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.mjs — Integration doctor / readiness check for ATF OpenClaw plugin
|
|
3
|
+
*
|
|
4
|
+
* Answers:
|
|
5
|
+
* - Is the plugin loaded under the canonical ID?
|
|
6
|
+
* - Is the ATF CLI available?
|
|
7
|
+
* - Is the ATF API reachable?
|
|
8
|
+
* - Which backend is preferred / effective?
|
|
9
|
+
* - Are expected native tools registered?
|
|
10
|
+
* - Are there configuration warnings?
|
|
11
|
+
* - What should the operator do next?
|
|
12
|
+
*
|
|
13
|
+
* Exports:
|
|
14
|
+
* - runDoctor(cfg, registeredToolNames) → DoctorReport
|
|
15
|
+
* - EXPECTED_TOOLS → canonical tool list
|
|
16
|
+
* - DOCTOR_STATUS → status enum
|
|
17
|
+
*
|
|
18
|
+
* Uses ONLY built-in Node modules + sibling backend.mjs. No external deps.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
resolveBackend,
|
|
23
|
+
probeCliAvailable,
|
|
24
|
+
probeApiAvailable,
|
|
25
|
+
BACKEND_STATUS,
|
|
26
|
+
} from "./backend.mjs";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
validateConfig,
|
|
30
|
+
CANONICAL_PLUGIN_ID,
|
|
31
|
+
} from "./config.mjs";
|
|
32
|
+
|
|
33
|
+
// Import constants at module level — avoids circular dependency since
|
|
34
|
+
// index.mjs imports doctor.mjs lazily (dynamic import inside handler).
|
|
35
|
+
// At call-time, index.mjs is already fully initialized.
|
|
36
|
+
|
|
37
|
+
// Canonical plugin identity — duplicated from index.mjs to avoid circular
|
|
38
|
+
// import at module level. Kept in sync by version consistency tests.
|
|
39
|
+
const PLUGIN_ID = "trucore-atf";
|
|
40
|
+
const PLUGIN_VERSION = "0.2.0";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Constants
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** Canonical set of expected native tools in this plugin version. */
|
|
47
|
+
export const EXPECTED_TOOLS = Object.freeze([
|
|
48
|
+
"atf_health",
|
|
49
|
+
"atf_discover",
|
|
50
|
+
"atf_bootstrap_plan",
|
|
51
|
+
"atf_bootstrap_execute_safe",
|
|
52
|
+
"atf_protect_intent",
|
|
53
|
+
"atf_verify_receipt",
|
|
54
|
+
"atf_report_savings",
|
|
55
|
+
"atf_integration_doctor",
|
|
56
|
+
"atf_bot_preflight",
|
|
57
|
+
"atf_tx_explain",
|
|
58
|
+
"atf_billing_info",
|
|
59
|
+
"atf_adoption_advisor",
|
|
60
|
+
"atf_billing_claim",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** Doctor overall status — stable finite set. */
|
|
64
|
+
export const DOCTOR_STATUS = Object.freeze({
|
|
65
|
+
OK: "ok",
|
|
66
|
+
DEGRADED: "degraded",
|
|
67
|
+
MISCONFIGURED: "misconfigured",
|
|
68
|
+
UNAVAILABLE: "unavailable",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Doctor report
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {object} DoctorReport
|
|
77
|
+
* @property {string} plugin_id
|
|
78
|
+
* @property {string} plugin_version
|
|
79
|
+
* @property {boolean} plugin_loaded
|
|
80
|
+
* @property {boolean} lifecycle_exports_present
|
|
81
|
+
* @property {boolean} cli_available
|
|
82
|
+
* @property {boolean} api_available
|
|
83
|
+
* @property {string} preferred_backend
|
|
84
|
+
* @property {string} effective_backend
|
|
85
|
+
* @property {boolean} fallback_occurred
|
|
86
|
+
* @property {string|null} fallback_reason
|
|
87
|
+
* @property {string[]} native_tools_expected
|
|
88
|
+
* @property {string[]} native_tools_available
|
|
89
|
+
* @property {string[]} native_tools_missing
|
|
90
|
+
* @property {string} status
|
|
91
|
+
* @property {string[]} warnings
|
|
92
|
+
* @property {string[]} remediation
|
|
93
|
+
* @property {object} config_validation
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run the integration doctor check.
|
|
98
|
+
*
|
|
99
|
+
* @param {Record<string, unknown>} cfg Resolved plugin config.
|
|
100
|
+
* @param {string[]} registeredToolNames Names of tools currently registered.
|
|
101
|
+
* @param {Record<string, unknown>} [rawUserConfig] Original user config (pre-resolve) for validation.
|
|
102
|
+
* @returns {Promise<DoctorReport>}
|
|
103
|
+
*/
|
|
104
|
+
export async function runDoctor(cfg, registeredToolNames = [], rawUserConfig) {
|
|
105
|
+
// 1. Probe backends
|
|
106
|
+
const cli = await probeCliAvailable(cfg?.atfCli ?? "atf");
|
|
107
|
+
const api = await probeApiAvailable(cfg?.atfBaseUrl);
|
|
108
|
+
const backend = await resolveBackend(cfg, { cli, api });
|
|
109
|
+
|
|
110
|
+
// 2. Check tool registration
|
|
111
|
+
const registeredSet = new Set(registeredToolNames);
|
|
112
|
+
const toolsMissing = EXPECTED_TOOLS.filter((t) => !registeredSet.has(t));
|
|
113
|
+
const toolsAvailable = EXPECTED_TOOLS.filter((t) => registeredSet.has(t));
|
|
114
|
+
|
|
115
|
+
// 3. Lifecycle exports presence
|
|
116
|
+
const lifecyclePresent = true; // verified by ES import resolution
|
|
117
|
+
|
|
118
|
+
// 4. Config validation
|
|
119
|
+
const configResult = validateConfig(rawUserConfig ?? cfg);
|
|
120
|
+
|
|
121
|
+
// 5. Build warnings + remediation
|
|
122
|
+
/** @type {string[]} */
|
|
123
|
+
const warnings = [...backend.warnings];
|
|
124
|
+
/** @type {string[]} */
|
|
125
|
+
const remediation = [];
|
|
126
|
+
|
|
127
|
+
// Merge config validation warnings/errors into doctor output
|
|
128
|
+
if (!configResult.valid) {
|
|
129
|
+
for (const err of configResult.errors) {
|
|
130
|
+
warnings.push(`Config error: ${err}`);
|
|
131
|
+
}
|
|
132
|
+
for (const rem of configResult.remediation) {
|
|
133
|
+
remediation.push(rem);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const w of configResult.warnings) {
|
|
137
|
+
warnings.push(`Config warning: ${w}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!cli.available) {
|
|
141
|
+
remediation.push("Install ATF CLI: npm install -g @trucore/atf");
|
|
142
|
+
}
|
|
143
|
+
if (cfg?.prefer === "api" && !cfg?.atfBaseUrl) {
|
|
144
|
+
remediation.push(
|
|
145
|
+
"Set atfBaseUrl in plugin config (e.g. 'https://api.trucore.xyz').",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (cfg?.prefer === "api" && cfg?.atfBaseUrl && !api.available) {
|
|
149
|
+
remediation.push(
|
|
150
|
+
`Check ATF API at ${cfg.atfBaseUrl}/health — currently unreachable.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (toolsMissing.length > 0) {
|
|
154
|
+
warnings.push(
|
|
155
|
+
`Missing tools: ${toolsMissing.join(", ")}. ` +
|
|
156
|
+
"Ensure the plugin loaded without errors.",
|
|
157
|
+
);
|
|
158
|
+
remediation.push(
|
|
159
|
+
"Restart the OpenClaw gateway and check logs for [trucore-atf] warnings.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 6. Determine overall status
|
|
164
|
+
let status = DOCTOR_STATUS.OK;
|
|
165
|
+
if (!configResult.valid) {
|
|
166
|
+
status = DOCTOR_STATUS.MISCONFIGURED;
|
|
167
|
+
} else if (backend.status === BACKEND_STATUS.UNAVAILABLE) {
|
|
168
|
+
status = DOCTOR_STATUS.UNAVAILABLE;
|
|
169
|
+
} else if (backend.status === BACKEND_STATUS.MISCONFIGURED) {
|
|
170
|
+
status = DOCTOR_STATUS.MISCONFIGURED;
|
|
171
|
+
} else if (
|
|
172
|
+
backend.status === BACKEND_STATUS.DEGRADED ||
|
|
173
|
+
toolsMissing.length > 0
|
|
174
|
+
) {
|
|
175
|
+
status = DOCTOR_STATUS.DEGRADED;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 7. Add "what to do next" if healthy
|
|
179
|
+
if (status === DOCTOR_STATUS.OK && remediation.length === 0) {
|
|
180
|
+
remediation.push("Integration is healthy. No action required.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
plugin_id: PLUGIN_ID,
|
|
185
|
+
plugin_version: PLUGIN_VERSION,
|
|
186
|
+
plugin_loaded: true,
|
|
187
|
+
lifecycle_exports_present: lifecyclePresent,
|
|
188
|
+
cli_available: cli.available,
|
|
189
|
+
api_available: api.available,
|
|
190
|
+
preferred_backend: backend.preferred_backend,
|
|
191
|
+
effective_backend: backend.effective_backend,
|
|
192
|
+
fallback_occurred: backend.fallback_occurred,
|
|
193
|
+
fallback_reason: backend.fallback_reason,
|
|
194
|
+
native_tools_expected: [...EXPECTED_TOOLS],
|
|
195
|
+
native_tools_available: toolsAvailable,
|
|
196
|
+
native_tools_missing: toolsMissing,
|
|
197
|
+
status,
|
|
198
|
+
warnings,
|
|
199
|
+
remediation,
|
|
200
|
+
config_validation: {
|
|
201
|
+
valid: configResult.valid,
|
|
202
|
+
errors: configResult.errors,
|
|
203
|
+
warnings: configResult.warnings,
|
|
204
|
+
unsupported_keys: configResult.unsupported_keys,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|