@principles/core 1.296.2 → 1.297.1
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.md +0 -1
- package/dist/runtime-v2/adapter/index.d.ts +0 -2
- package/dist/runtime-v2/adapter/index.d.ts.map +1 -1
- package/dist/runtime-v2/adapter/index.js +0 -1
- package/dist/runtime-v2/adapter/index.js.map +1 -1
- package/dist/runtime-v2/adapter/output-schema-registry.d.ts +3 -3
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +1 -2
- package/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/dist/runtime-v2/cli/probe.d.ts +4 -15
- package/dist/runtime-v2/cli/probe.d.ts.map +1 -1
- package/dist/runtime-v2/cli/probe.js +24 -44
- package/dist/runtime-v2/cli/probe.js.map +1 -1
- package/dist/runtime-v2/config/index.d.ts +1 -1
- package/dist/runtime-v2/config/index.d.ts.map +1 -1
- package/dist/runtime-v2/config/index.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-agent-binding.d.ts +10 -17
- package/dist/runtime-v2/config/pd-config-agent-binding.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-agent-binding.js +23 -27
- package/dist/runtime-v2/config/pd-config-agent-binding.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-defaults.d.ts +3 -5
- package/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-defaults.js +17 -20
- package/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-effective.js +4 -1
- package/dist/runtime-v2/config/pd-config-effective.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-redaction.js +4 -7
- package/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-types.d.ts +5 -1
- package/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-types.js +6 -0
- package/dist/runtime-v2/config/pd-config-types.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
- package/dist/runtime-v2/config/pd-config-validate.js +6 -0
- package/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
- package/dist/runtime-v2/golden-trace.d.ts.map +1 -1
- package/dist/runtime-v2/golden-trace.js +8 -1
- package/dist/runtime-v2/golden-trace.js.map +1 -1
- package/dist/runtime-v2/index.d.ts +4 -4
- package/dist/runtime-v2/index.d.ts.map +1 -1
- package/dist/runtime-v2/index.js +2 -3
- package/dist/runtime-v2/index.js.map +1 -1
- package/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/artificer-intent-preflight.js +26 -95
- package/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -1
- package/dist/runtime-v2/internalization/bash-risk.d.ts +32 -0
- package/dist/runtime-v2/internalization/bash-risk.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/bash-risk.js +785 -0
- package/dist/runtime-v2/internalization/bash-risk.js.map +1 -0
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +25 -2
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js +177 -14
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.js +14 -2
- package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts +47 -0
- package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.js +164 -3
- package/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +9 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts +39 -0
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.js +59 -0
- package/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/dist/runtime-v2/internalization/rule-source-correction.d.ts +60 -0
- package/dist/runtime-v2/internalization/rule-source-correction.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/rule-source-correction.js +214 -0
- package/dist/runtime-v2/internalization/rule-source-correction.js.map +1 -0
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.js +65 -0
- package/dist/runtime-v2/internalization/rulecode-verification-obligations.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-output.d.ts +14 -2
- package/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-output.js +57 -2
- package/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +13 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-prompt-builder.js +15 -3
- package/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/internalization/scribe-runner.d.ts +34 -0
- package/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/scribe-runner.js +128 -4
- package/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/textual-basis-anchor.d.ts +72 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.js +100 -0
- package/dist/runtime-v2/internalization/textual-basis-anchor.js.map +1 -0
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts +9 -7
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/dist/runtime-v2/pain-signal-runtime-factory.js +59 -121
- package/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/dist/runtime-v2/runtime-protocol.d.ts +2 -3
- package/dist/runtime-v2/runtime-protocol.d.ts.map +1 -1
- package/dist/runtime-v2/runtime-protocol.js +15 -10
- package/dist/runtime-v2/runtime-protocol.js.map +1 -1
- package/dist/runtime-v2/store/lifecycle/lease-manager.d.ts.map +1 -1
- package/dist/runtime-v2/store/lifecycle/lease-manager.js +11 -2
- package/dist/runtime-v2/store/lifecycle/lease-manager.js.map +1 -1
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.js +26 -0
- package/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
- package/dist/telemetry-event.d.ts +2 -2
- package/dist/telemetry-event.d.ts.map +1 -1
- package/dist/telemetry-event.js +5 -0
- package/dist/telemetry-event.js.map +1 -1
- package/dist/workflow-funnel-loader.d.ts +0 -2
- package/dist/workflow-funnel-loader.d.ts.map +1 -1
- package/dist/workflow-funnel-loader.js.map +1 -1
- package/package.json +5 -1
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts +0 -72
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts.map +0 -1
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js +0 -837
- package/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js.map +0 -1
- package/dist/runtime-v2/utils/cli-process-runner.d.ts +0 -35
- package/dist/runtime-v2/utils/cli-process-runner.d.ts.map +0 -1
- package/dist/runtime-v2/utils/cli-process-runner.js +0 -263
- package/dist/runtime-v2/utils/cli-process-runner.js.map +0 -1
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClawCliRuntimeAdapter — PDRuntimeAdapter implementation for openclaw-cli.
|
|
3
|
-
*
|
|
4
|
-
* One-shot run: startRun() spawns 'openclaw agent', blocks until CLI exits,
|
|
5
|
-
* stores CliOutput in memory. pollRun()/fetchOutput() operate on stored state.
|
|
6
|
-
*
|
|
7
|
-
* Error mapping (D-04):
|
|
8
|
-
* ENOENT (binary not found) → runtime_unavailable
|
|
9
|
-
* timedOut → timeout
|
|
10
|
-
* non-zero exit code → execution_failed
|
|
11
|
-
* JSON parse failure → output_invalid
|
|
12
|
-
* DiagnosticianOutputV1Schema validation failure → output_invalid
|
|
13
|
-
*/
|
|
14
|
-
import { Value } from '@sinclair/typebox/value';
|
|
15
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
16
|
-
import { tmpdir } from 'node:os';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { runCliProcess } from '../utils/cli-process-runner.js';
|
|
19
|
-
import { PDRuntimeError } from '../error-categories.js';
|
|
20
|
-
import { DiagnosticianOutputV1Schema } from '../diagnostician-output.js';
|
|
21
|
-
import { resolveOutputSchema } from './output-schema-registry.js';
|
|
22
|
-
import { storeEmitter } from '../store/event-emitter.js';
|
|
23
|
-
import { safeStringifyPreview, truncatePreview } from './output-repair-contract.js';
|
|
24
|
-
import { redactTelemetryString } from '../feedback/redact-sensitive.js';
|
|
25
|
-
import { redactSensitiveFields } from '../feedback/redact-sensitive.js';
|
|
26
|
-
import { repairMalformedJson } from './json-extractor.js';
|
|
27
|
-
/**
|
|
28
|
-
* Persist an inter-process message payload so the runtime CLI can read it via
|
|
29
|
-
* --message-file. When no workspaceDir is configured, the payload is written
|
|
30
|
-
* inside a fresh mkdtemp root owned solely by this call; on any write failure
|
|
31
|
-
* that root is removed here so failed writes cannot leak temp dirs
|
|
32
|
-
* (PRI-827 / js/insecure-temporary-file). Workspace-owned directories are
|
|
33
|
-
* never removed: they are shared, not exclusive to this call.
|
|
34
|
-
*/
|
|
35
|
-
async function writeMessageFile(message, workspaceDir) {
|
|
36
|
-
// Keep the payload in the PD workspace so cleanup and workspace ownership remain explicit.
|
|
37
|
-
const baseDir = workspaceDir
|
|
38
|
-
? join(workspaceDir, '.pd', 'tmp')
|
|
39
|
-
: await mkdtemp(join(tmpdir(), 'pd-msg-'));
|
|
40
|
-
const cleanupDir = workspaceDir ? undefined : baseDir;
|
|
41
|
-
try {
|
|
42
|
-
await mkdir(baseDir, { recursive: true });
|
|
43
|
-
const filePath = join(baseDir, `msg-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.json`);
|
|
44
|
-
await writeFile(filePath, message, 'utf8');
|
|
45
|
-
return { filePath, cleanupDir };
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (cleanupDir !== undefined) {
|
|
49
|
-
try {
|
|
50
|
-
await rm(cleanupDir, { recursive: true, force: true });
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Best effort: preserve the original error even if cleanup also fails.
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Remove a message file created by writeMessageFile, including its unique temp
|
|
61
|
-
* root when one was created. Best effort on both success and failure paths.
|
|
62
|
-
*/
|
|
63
|
-
async function cleanupMessageFile(ref) {
|
|
64
|
-
if (!ref)
|
|
65
|
-
return;
|
|
66
|
-
try {
|
|
67
|
-
if (ref.cleanupDir !== undefined) {
|
|
68
|
-
await rm(ref.cleanupDir, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
await rm(ref.filePath, { force: true });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// File may already be gone; ignore cleanup errors
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Parse CliOutput stdout into the runtime's native result structure.
|
|
80
|
-
*
|
|
81
|
-
* OCRA-03 (revised): OpenClaw `--json` produces a CliOutput object (from openclaw's own
|
|
82
|
-
* output.ts), NOT DiagnosticianOutputV1 directly. The LLM's diagnosis JSON lives inside
|
|
83
|
-
* CliOutput.text as a string that may be:
|
|
84
|
-
*
|
|
85
|
-
* 1. Bare DiagnosticianOutputV1 JSON — { "valid": true, ... }
|
|
86
|
-
* 2. Local envelope — { payloads: [{ text: "<json>" }], meta: ... }
|
|
87
|
-
* 3. Gateway envelope — { result: { payloads: [{ text: "<json>" }] }, ... }
|
|
88
|
-
*
|
|
89
|
-
* This function normalises all three into a raw JSON value (string or object) that
|
|
90
|
-
* DiagnosticianOutputV1Schema can validate.
|
|
91
|
-
*
|
|
92
|
-
* @returns The parsed JSON value, or null if stdout could not be parsed at all.
|
|
93
|
-
*/
|
|
94
|
-
function extractPayloadFromCliOutput(stdout, stderr) {
|
|
95
|
-
for (const source of [stdout, stderr ?? '']) {
|
|
96
|
-
if (!source)
|
|
97
|
-
continue;
|
|
98
|
-
// Attempt 1: direct parse — covers case 1 (bare JSON)
|
|
99
|
-
try {
|
|
100
|
-
return JSON.parse(source);
|
|
101
|
-
}
|
|
102
|
-
catch { /* fall through */ }
|
|
103
|
-
// Attempt 2: balanced-bracket extraction — handles text that surrounds JSON.
|
|
104
|
-
// When multiple JSON objects are present (e.g. probe message + agent response in
|
|
105
|
-
// stderr), return the LAST complete object — the agent's response comes after the
|
|
106
|
-
// probe message that was injected into the session.
|
|
107
|
-
const objects = [];
|
|
108
|
-
let depth = 0;
|
|
109
|
-
let start = -1;
|
|
110
|
-
for (let i = 0; i < source.length; i++) {
|
|
111
|
-
const ch = source[i];
|
|
112
|
-
if (!ch)
|
|
113
|
-
continue;
|
|
114
|
-
if (ch === '{') {
|
|
115
|
-
if (start === -1)
|
|
116
|
-
start = i;
|
|
117
|
-
depth++;
|
|
118
|
-
}
|
|
119
|
-
else if (ch === '}') {
|
|
120
|
-
depth--;
|
|
121
|
-
if (depth === 0 && start !== -1) {
|
|
122
|
-
try {
|
|
123
|
-
objects.push(JSON.parse(source.slice(start, i + 1)));
|
|
124
|
-
}
|
|
125
|
-
catch { /* keep scanning */ }
|
|
126
|
-
start = -1;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (objects.length > 0)
|
|
131
|
-
return objects[objects.length - 1];
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Search for a single JSON object literal inside `text` using a balanced-bracket
|
|
137
|
-
* scan. Returns the parsed object, or null if no valid object is found.
|
|
138
|
-
*/
|
|
139
|
-
function extractJsonObject(text) {
|
|
140
|
-
let depth = 0;
|
|
141
|
-
let start = -1;
|
|
142
|
-
for (let i = 0; i < text.length; i++) {
|
|
143
|
-
const ch = text[i];
|
|
144
|
-
if (ch === '{') {
|
|
145
|
-
if (start === -1)
|
|
146
|
-
start = i;
|
|
147
|
-
depth++;
|
|
148
|
-
}
|
|
149
|
-
else if (ch === '}') {
|
|
150
|
-
depth--;
|
|
151
|
-
if (depth === 0 && start !== -1) {
|
|
152
|
-
try {
|
|
153
|
-
return JSON.parse(text.slice(start, i + 1));
|
|
154
|
-
}
|
|
155
|
-
catch { /* keep scanning */ }
|
|
156
|
-
start = -1;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Navigate the three possible envelope shapes to find the payload text that
|
|
164
|
-
* contains the DiagnosticianOutputV1 JSON string.
|
|
165
|
-
*
|
|
166
|
-
* Returns the parsed JSON object (not a string) if extraction succeeds,
|
|
167
|
-
* or null if the envelope structure doesn't match any known shape.
|
|
168
|
-
*/
|
|
169
|
-
function extractDiagnosisFromEnvelope(parsed) {
|
|
170
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
// Case 2: local envelope — { payloads: [{ text: "<json>" }] }
|
|
174
|
-
const asLocal = parsed;
|
|
175
|
-
if (Array.isArray(asLocal.payloads) && asLocal.payloads.length > 0) {
|
|
176
|
-
const [first] = asLocal.payloads;
|
|
177
|
-
if (first && typeof first.text === 'string') {
|
|
178
|
-
const inner = first.text.trim();
|
|
179
|
-
try {
|
|
180
|
-
return JSON.parse(inner);
|
|
181
|
-
}
|
|
182
|
-
catch { /* fall through — try prose extraction */ }
|
|
183
|
-
const extracted = extractJsonObject(inner);
|
|
184
|
-
if (extracted !== null)
|
|
185
|
-
return extracted;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Case 3: gateway envelope — { result: { payloads: [{ text: "<json>" }] } }
|
|
189
|
-
const asGateway = parsed;
|
|
190
|
-
if (asGateway.result &&
|
|
191
|
-
typeof asGateway.result === 'object' &&
|
|
192
|
-
Array.isArray(asGateway.result.payloads) &&
|
|
193
|
-
asGateway.result.payloads.length > 0) {
|
|
194
|
-
const [first] = asGateway.result.payloads;
|
|
195
|
-
if (first && typeof first.text === 'string') {
|
|
196
|
-
const inner = first.text.trim();
|
|
197
|
-
try {
|
|
198
|
-
return JSON.parse(inner);
|
|
199
|
-
}
|
|
200
|
-
catch { /* fall through */ }
|
|
201
|
-
const extracted = extractJsonObject(inner);
|
|
202
|
-
if (extracted !== null)
|
|
203
|
-
return extracted;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// No matching envelope shape
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Extract the inner result object from an OpenClaw agent response envelope.
|
|
211
|
-
*
|
|
212
|
-
* OpenClaw `--json --local` wraps the agent's response text inside an envelope:
|
|
213
|
-
* Case A — local envelope: { payloads: [{ text: "<json or prose>" }] }
|
|
214
|
-
* Case B — gateway envelope: { result: { payloads: [{ text: "<json or prose>" }] } }
|
|
215
|
-
* Case C — bare object: { ok: true } (no envelope, direct response)
|
|
216
|
-
*
|
|
217
|
-
* The inner text may be bare JSON (`{"ok":true}`) or wrapped in a small amount
|
|
218
|
-
* of prose that surrounds the JSON. We balance-bracket extract to find the JSON
|
|
219
|
-
* object within the text, then return the parsed result.
|
|
220
|
-
*
|
|
221
|
-
* Returns the parsed inner object on success, or null if extraction fails.
|
|
222
|
-
*/
|
|
223
|
-
function extractProbeResult(parsed) {
|
|
224
|
-
// Case A: local envelope — { payloads: [{ text: "..." }] }
|
|
225
|
-
const asLocal = parsed;
|
|
226
|
-
if (Array.isArray(asLocal.payloads) && asLocal.payloads.length > 0) {
|
|
227
|
-
const [first] = asLocal.payloads;
|
|
228
|
-
if (first && typeof first.text === 'string') {
|
|
229
|
-
const inner = first.text.trim();
|
|
230
|
-
try {
|
|
231
|
-
return JSON.parse(inner);
|
|
232
|
-
}
|
|
233
|
-
catch { /* fall through — try prose extraction */ }
|
|
234
|
-
const extracted = extractJsonObject(inner);
|
|
235
|
-
if (extracted !== null)
|
|
236
|
-
return extracted;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// Case B: gateway envelope — { result: { payloads: [{ text: "..." }] } }
|
|
240
|
-
const asGateway = parsed;
|
|
241
|
-
if (asGateway.result &&
|
|
242
|
-
typeof asGateway.result === 'object' &&
|
|
243
|
-
Array.isArray(asGateway.result.payloads) &&
|
|
244
|
-
asGateway.result.payloads.length > 0) {
|
|
245
|
-
const [first] = asGateway.result.payloads;
|
|
246
|
-
if (first && typeof first.text === 'string') {
|
|
247
|
-
const inner = first.text.trim();
|
|
248
|
-
try {
|
|
249
|
-
return JSON.parse(inner);
|
|
250
|
-
}
|
|
251
|
-
catch { /* fall through */ }
|
|
252
|
-
const extracted = extractJsonObject(inner);
|
|
253
|
-
if (extracted !== null)
|
|
254
|
-
return extracted;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Case C: bare object — already the result
|
|
258
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
259
|
-
return parsed;
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
export class OpenClawCliRuntimeAdapter {
|
|
264
|
-
runStateMap = new Map();
|
|
265
|
-
runtimeMode;
|
|
266
|
-
workspaceDir;
|
|
267
|
-
agentId;
|
|
268
|
-
eventEmitter;
|
|
269
|
-
cliProcessRunner;
|
|
270
|
-
constructor(options) {
|
|
271
|
-
this.runtimeMode = options.runtimeMode;
|
|
272
|
-
this.workspaceDir = options.workspaceDir;
|
|
273
|
-
this.agentId = options.agentId ?? 'main';
|
|
274
|
-
this.eventEmitter = options.eventEmitter ?? storeEmitter;
|
|
275
|
-
this.cliProcessRunner = options.cliProcessRunner ?? runCliProcess;
|
|
276
|
-
}
|
|
277
|
-
// OCRA-01: kind() returns RuntimeKind literal 'openclaw-cli'
|
|
278
|
-
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
279
|
-
kind() {
|
|
280
|
-
return 'openclaw-cli';
|
|
281
|
-
}
|
|
282
|
-
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
283
|
-
async getCapabilities() {
|
|
284
|
-
return {
|
|
285
|
-
supportsStructuredJsonOutput: true,
|
|
286
|
-
supportsToolUse: false,
|
|
287
|
-
supportsWorkingDirectory: false,
|
|
288
|
-
supportsModelSelection: false,
|
|
289
|
-
supportsLongRunningSessions: false,
|
|
290
|
-
supportsCancellation: true,
|
|
291
|
-
supportsArtifactWriteBack: false,
|
|
292
|
-
supportsConcurrentRuns: false,
|
|
293
|
-
supportsStreaming: false,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Probe runtime health by running a minimal openclaw command.
|
|
298
|
-
*
|
|
299
|
-
* Verifies:
|
|
300
|
-
* 1. openclaw binary is executable (run `openclaw --version`)
|
|
301
|
-
* 2. The configured agent is available (run `openclaw agents list --json`)
|
|
302
|
-
* 3. The agent can actually respond (one-shot minimal probe with --agent)
|
|
303
|
-
*
|
|
304
|
-
* Returns healthy=false with appropriate error category when checks fail.
|
|
305
|
-
* Per DPB-09: No silent fallback — failures are reported accurately.
|
|
306
|
-
* Per P1 hard gate: Binary-only check is insufficient; must verify real invocation.
|
|
307
|
-
*/
|
|
308
|
-
async healthCheck() {
|
|
309
|
-
const warnings = [];
|
|
310
|
-
let healthy = true;
|
|
311
|
-
let degraded = false;
|
|
312
|
-
// Probe 1: verify openclaw binary is executable
|
|
313
|
-
try {
|
|
314
|
-
const versionResult = await this.cliProcessRunner({
|
|
315
|
-
command: 'openclaw',
|
|
316
|
-
args: ['--version'],
|
|
317
|
-
cwd: this.workspaceDir,
|
|
318
|
-
timeoutMs: 10_000,
|
|
319
|
-
});
|
|
320
|
-
if (versionResult.spawnError === 'ENOENT') {
|
|
321
|
-
return {
|
|
322
|
-
healthy: false,
|
|
323
|
-
degraded: false,
|
|
324
|
-
warnings: ['openclaw binary not found'],
|
|
325
|
-
lastCheckedAt: new Date().toISOString(),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if (versionResult.spawnError === 'EACCES' || versionResult.spawnError === 'EMFILE') {
|
|
329
|
-
return {
|
|
330
|
-
healthy: false,
|
|
331
|
-
degraded: false,
|
|
332
|
-
warnings: [`openclaw binary not executable: ${versionResult.spawnError}`],
|
|
333
|
-
lastCheckedAt: new Date().toISOString(),
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (versionResult.timedOut) {
|
|
337
|
-
return {
|
|
338
|
-
healthy: false,
|
|
339
|
-
degraded: false,
|
|
340
|
-
warnings: ['openclaw --version timed out'],
|
|
341
|
-
lastCheckedAt: new Date().toISOString(),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
if (versionResult.exitCode !== null && versionResult.exitCode !== 0) {
|
|
345
|
-
degraded = true;
|
|
346
|
-
warnings.push(`openclaw --version exited with code ${versionResult.exitCode}`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch (err) {
|
|
350
|
-
return {
|
|
351
|
-
healthy: false,
|
|
352
|
-
degraded: false,
|
|
353
|
-
warnings: [err instanceof Error ? err.message : String(err)],
|
|
354
|
-
lastCheckedAt: new Date().toISOString(),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
// Probe 2: verify the configured agent is available via agents list
|
|
358
|
-
// P1 fix: Must verify agent exists, not just the binary
|
|
359
|
-
// Using `openclaw agents list --json` to check if agentId is registered
|
|
360
|
-
try {
|
|
361
|
-
const listArgs = ['agents', 'list', '--json'];
|
|
362
|
-
if (this.runtimeMode === 'local') {
|
|
363
|
-
listArgs.push('--local');
|
|
364
|
-
}
|
|
365
|
-
const listResult = await this.cliProcessRunner({
|
|
366
|
-
command: 'openclaw',
|
|
367
|
-
args: listArgs,
|
|
368
|
-
cwd: this.workspaceDir,
|
|
369
|
-
timeoutMs: 15_000,
|
|
370
|
-
});
|
|
371
|
-
if (listResult.spawnError === 'ENOENT') {
|
|
372
|
-
return {
|
|
373
|
-
healthy: false,
|
|
374
|
-
degraded: false,
|
|
375
|
-
warnings: ['openclaw binary not found'],
|
|
376
|
-
lastCheckedAt: new Date().toISOString(),
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
if (listResult.spawnError === 'EACCES' || listResult.spawnError === 'EMFILE') {
|
|
380
|
-
return {
|
|
381
|
-
healthy: false,
|
|
382
|
-
degraded: false,
|
|
383
|
-
warnings: [`openclaw binary not executable: ${listResult.spawnError}`],
|
|
384
|
-
lastCheckedAt: new Date().toISOString(),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
if (listResult.timedOut) {
|
|
388
|
-
degraded = true;
|
|
389
|
-
warnings.push(`openclaw agents list timed out`);
|
|
390
|
-
}
|
|
391
|
-
else if (listResult.exitCode !== null && listResult.exitCode !== 0) {
|
|
392
|
-
// agents list failed — degraded state
|
|
393
|
-
degraded = true;
|
|
394
|
-
warnings.push(`openclaw agents list exited with code ${listResult.exitCode}`);
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
// Parse agent list and check if target agent exists
|
|
398
|
-
// The output may contain config warnings/ANSI codes before the JSON array
|
|
399
|
-
try {
|
|
400
|
-
const jsonStart = listResult.stdout.indexOf('[');
|
|
401
|
-
if (jsonStart === -1)
|
|
402
|
-
throw new Error('No JSON array found');
|
|
403
|
-
const jsonStr = listResult.stdout.slice(jsonStart);
|
|
404
|
-
const parsed = JSON.parse(jsonStr);
|
|
405
|
-
// agents list returns an array of agent objects (each with an `id` field)
|
|
406
|
-
const agentIds = parsed.map((a) => typeof a === 'string' ? a : a.id);
|
|
407
|
-
if (!agentIds.includes(this.agentId)) {
|
|
408
|
-
healthy = false;
|
|
409
|
-
warnings.push(`agent '${this.agentId}' not found in agents list`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
// JSON parse failed — cannot verify agent, treat as degraded
|
|
414
|
-
degraded = true;
|
|
415
|
-
warnings.push(`failed to parse agents list output`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
// Any error in the agent probe is a hard failure
|
|
421
|
-
return {
|
|
422
|
-
healthy: false,
|
|
423
|
-
degraded: false,
|
|
424
|
-
warnings: [err instanceof Error ? err.message : String(err)],
|
|
425
|
-
lastCheckedAt: new Date().toISOString(),
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
// Probe 3: One-shot minimal agent invocation (P1 hard gate)
|
|
429
|
-
// Verifies the agent can actually respond, not just that it exists in the list.
|
|
430
|
-
// This catches plugin loading failures, permission issues, etc.
|
|
431
|
-
let probeMessageRef = undefined;
|
|
432
|
-
try {
|
|
433
|
-
// Unique session ID for probe to avoid lock conflicts with concurrent runs
|
|
434
|
-
const probeSessionId = `pd-runtime-probe-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
435
|
-
probeMessageRef = await writeMessageFile(JSON.stringify({ probe: 'pd-runtime-v2', instruction: 'reply with {"ok":true} only' }), this.workspaceDir);
|
|
436
|
-
const probeArgs = [
|
|
437
|
-
'agent',
|
|
438
|
-
'--agent', this.agentId,
|
|
439
|
-
'--message-file', probeMessageRef.filePath,
|
|
440
|
-
'--session-id', probeSessionId,
|
|
441
|
-
'--timeout', '240',
|
|
442
|
-
'--json',
|
|
443
|
-
];
|
|
444
|
-
if (this.runtimeMode === 'local') {
|
|
445
|
-
probeArgs.push('--local');
|
|
446
|
-
}
|
|
447
|
-
const probeResult = await this.cliProcessRunner({
|
|
448
|
-
command: 'openclaw',
|
|
449
|
-
args: probeArgs,
|
|
450
|
-
cwd: this.workspaceDir,
|
|
451
|
-
timeoutMs: 240_000, // 240s — agent init + bootstrap + skill loading can take >60s on cold start
|
|
452
|
-
});
|
|
453
|
-
if (probeResult.spawnError === 'ENOENT') {
|
|
454
|
-
return {
|
|
455
|
-
healthy: false,
|
|
456
|
-
degraded: false,
|
|
457
|
-
warnings: ['openclaw binary not found'],
|
|
458
|
-
lastCheckedAt: new Date().toISOString(),
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
if (probeResult.spawnError === 'EACCES' || probeResult.spawnError === 'EMFILE') {
|
|
462
|
-
return {
|
|
463
|
-
healthy: false,
|
|
464
|
-
degraded: false,
|
|
465
|
-
warnings: [`openclaw binary not executable: ${probeResult.spawnError}`],
|
|
466
|
-
lastCheckedAt: new Date().toISOString(),
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
if (probeResult.timedOut) {
|
|
470
|
-
return {
|
|
471
|
-
healthy: false,
|
|
472
|
-
degraded: false,
|
|
473
|
-
warnings: ['openclaw agent probe timed out'],
|
|
474
|
-
lastCheckedAt: new Date().toISOString(),
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
if (probeResult.exitCode !== null && probeResult.exitCode !== 0) {
|
|
478
|
-
return {
|
|
479
|
-
healthy: false,
|
|
480
|
-
degraded: false,
|
|
481
|
-
warnings: [
|
|
482
|
-
`openclaw agent '${this.agentId}' probe failed with exit code ${probeResult.exitCode}`,
|
|
483
|
-
...(probeResult.stderr ? [`stderr: ${probeResult.stderr.substring(0, 2000)}`] : []),
|
|
484
|
-
],
|
|
485
|
-
lastCheckedAt: new Date().toISOString(),
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
// exit code 0 — verify the output is parseable JSON
|
|
489
|
-
const rawParsed = extractPayloadFromCliOutput(probeResult.stdout, probeResult.stderr);
|
|
490
|
-
if (rawParsed === null) {
|
|
491
|
-
return {
|
|
492
|
-
healthy: false,
|
|
493
|
-
degraded: false,
|
|
494
|
-
warnings: ['openclaw agent probe produced unparseable output'],
|
|
495
|
-
lastCheckedAt: new Date().toISOString(),
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
// Extract the inner result from the OpenClaw envelope and verify {ok: true}
|
|
499
|
-
const result = extractProbeResult(rawParsed);
|
|
500
|
-
if (typeof result !== 'object' ||
|
|
501
|
-
result === null ||
|
|
502
|
-
!Object.hasOwn(result, 'ok') ||
|
|
503
|
-
result.ok !== true) {
|
|
504
|
-
const excerpt = rawParsed && typeof rawParsed === 'object'
|
|
505
|
-
? JSON.stringify(rawParsed).substring(0, 2000)
|
|
506
|
-
: String(rawParsed).substring(0, 2000);
|
|
507
|
-
return {
|
|
508
|
-
healthy: false,
|
|
509
|
-
degraded: false,
|
|
510
|
-
warnings: [
|
|
511
|
-
`openclaw agent '${this.agentId}' probe returned unexpected result: ${excerpt}`,
|
|
512
|
-
],
|
|
513
|
-
lastCheckedAt: new Date().toISOString(),
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
catch (err) {
|
|
518
|
-
return {
|
|
519
|
-
healthy: false,
|
|
520
|
-
degraded: false,
|
|
521
|
-
warnings: [err instanceof Error ? err.message : String(err)],
|
|
522
|
-
lastCheckedAt: new Date().toISOString(),
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
finally {
|
|
526
|
-
await cleanupMessageFile(probeMessageRef);
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
healthy,
|
|
530
|
-
degraded,
|
|
531
|
-
warnings,
|
|
532
|
-
lastCheckedAt: new Date().toISOString(),
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Map CLI output to telemetry event type and error category.
|
|
537
|
-
*/
|
|
538
|
-
static mapCliResultToTelemetry(cliOutput) {
|
|
539
|
-
// spawnError is set by runCliProcess on ENOENT/EACCES/EMFILE — use it
|
|
540
|
-
if (cliOutput.spawnError === 'ENOENT') {
|
|
541
|
-
return { telemetryEventType: 'runtime_invocation_failed', errorCategory: 'runtime_unavailable', stderrExcerpt: cliOutput.stderr };
|
|
542
|
-
}
|
|
543
|
-
if (cliOutput.spawnError === 'EACCES' || cliOutput.spawnError === 'EMFILE') {
|
|
544
|
-
return { telemetryEventType: 'runtime_invocation_failed', errorCategory: 'execution_failed', stderrExcerpt: cliOutput.stderr };
|
|
545
|
-
}
|
|
546
|
-
if (cliOutput.timedOut) {
|
|
547
|
-
return { telemetryEventType: 'runtime_invocation_failed', errorCategory: 'timeout', stderrExcerpt: cliOutput.stderr };
|
|
548
|
-
}
|
|
549
|
-
if (cliOutput.exitCode !== null && cliOutput.exitCode !== 0) {
|
|
550
|
-
return { telemetryEventType: 'runtime_invocation_failed', errorCategory: 'execution_failed', stderrExcerpt: cliOutput.stderr };
|
|
551
|
-
}
|
|
552
|
-
return { telemetryEventType: 'runtime_invocation_succeeded', errorCategory: undefined, stderrExcerpt: undefined };
|
|
553
|
-
}
|
|
554
|
-
// OCRA-02: startRun synchronously invokes openclaw agent with correct CLI args
|
|
555
|
-
async startRun(input) {
|
|
556
|
-
// PRI-966 G2 P2 contract (declared capability boundary): StartRunInput.
|
|
557
|
-
// maxTokens is a per-request model output budget. This adapter delegates
|
|
558
|
-
// to the OpenClaw host CLI process, which exposes NO per-request
|
|
559
|
-
// output-token control — honoring the field here is impossible, and
|
|
560
|
-
// silently ignoring it is forbidden (rc-9). Fail loud as input_invalid
|
|
561
|
-
// BEFORE any CLI process starts.
|
|
562
|
-
if (input.maxTokens !== undefined) {
|
|
563
|
-
throw new PDRuntimeError('input_invalid', `OpenClawCliRuntimeAdapter does not support StartRunInput.maxTokens (received ${String(input.maxTokens)}): the host CLI owns its own model calls and exposes no per-request output-token control.`, {
|
|
564
|
-
maxTokens: input.maxTokens,
|
|
565
|
-
nextAction: 'Use a PiAi/L2 runtime adapter for per-request output budgets, or omit maxTokens for this runtime.',
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
const runId = crypto.randomUUID();
|
|
569
|
-
const startedAt = new Date().toISOString();
|
|
570
|
-
// Initialize state — cliOutput null until CLI completes.
|
|
571
|
-
// Capture outputSchemaRef so fetchOutput can validate against the runner's
|
|
572
|
-
// expected schema (e.g. diag-rootcause-output-v1 for Stage A) instead of
|
|
573
|
-
// hardcoding DiagnosticianOutputV1Schema.
|
|
574
|
-
const state = { runId, startedAt, cliOutput: null, completed: false, outputSchemaRef: input.outputSchemaRef };
|
|
575
|
-
this.runStateMap.set(runId, state);
|
|
576
|
-
// Build CLI args per D-02.
|
|
577
|
-
// PRI-633: the run's base-layer systemPrompt (agent role + protocol) is
|
|
578
|
-
// folded back into the message file. The OpenClaw host keeps full
|
|
579
|
-
// ownership of its own agent system prompt (DPB-07), so PD continues to
|
|
580
|
-
// deliver its instructions through the message channel; only the
|
|
581
|
-
// placement changed (appended section after the payload JSON instead of
|
|
582
|
-
// an instruction field inside it). Absent systemPrompt → unchanged bytes.
|
|
583
|
-
const payloadJson = typeof input.inputPayload === 'string'
|
|
584
|
-
? input.inputPayload
|
|
585
|
-
: JSON.stringify(input.inputPayload);
|
|
586
|
-
const jsonPayload = input.systemPrompt
|
|
587
|
-
? payloadJson + '\n\n' + input.systemPrompt
|
|
588
|
-
: payloadJson;
|
|
589
|
-
const timeoutSeconds = Math.ceil((input.timeoutMs ?? 600000) / 1000);
|
|
590
|
-
const agentId = input.agentSpec?.agentId ?? 'main';
|
|
591
|
-
const messageRef = await writeMessageFile(jsonPayload, this.workspaceDir);
|
|
592
|
-
// DPB-09 (LOCKED): --local only passed when runtimeMode === 'local'
|
|
593
|
-
// HG-03 (HARD GATE): No silent fallback — must be explicit
|
|
594
|
-
// Unique session ID per run — avoids session lock conflicts and context pollution
|
|
595
|
-
// between concurrent or sequential PD diagnostician runs on the same agent.
|
|
596
|
-
const sessionId = `pd-runtime-${runId}`;
|
|
597
|
-
const args = [
|
|
598
|
-
'agent',
|
|
599
|
-
'--agent', agentId,
|
|
600
|
-
'--message-file', messageRef.filePath,
|
|
601
|
-
'--session-id', sessionId,
|
|
602
|
-
'--json',
|
|
603
|
-
];
|
|
604
|
-
if (this.runtimeMode === 'local') {
|
|
605
|
-
args.push('--local');
|
|
606
|
-
}
|
|
607
|
-
args.push('--timeout', String(timeoutSeconds));
|
|
608
|
-
// DPB-08 (LOCKED): Pass workspaceDir as cwd for workspace boundary control
|
|
609
|
-
// HG-02 (HARD GATE): Two distinct boundaries — cwd controls PD workspace
|
|
610
|
-
// TELE-02: runtime_invocation_started — CLI process about to spawn
|
|
611
|
-
this.eventEmitter.emitTelemetry({
|
|
612
|
-
eventType: 'runtime_invocation_started',
|
|
613
|
-
traceId: input.taskRef?.taskId ?? runId,
|
|
614
|
-
timestamp: new Date().toISOString(),
|
|
615
|
-
sessionId: 'openclaw-cli-adapter',
|
|
616
|
-
agentId: 'openclaw-cli-adapter',
|
|
617
|
-
payload: {
|
|
618
|
-
runId,
|
|
619
|
-
runtimeKind: 'openclaw-cli',
|
|
620
|
-
runtimeMode: this.runtimeMode,
|
|
621
|
-
timeoutMs: input.timeoutMs,
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
const cliOutput = await (async () => {
|
|
625
|
-
try {
|
|
626
|
-
return await this.cliProcessRunner({
|
|
627
|
-
command: 'openclaw',
|
|
628
|
-
args,
|
|
629
|
-
cwd: this.workspaceDir,
|
|
630
|
-
timeoutMs: input.timeoutMs,
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
finally {
|
|
634
|
-
await cleanupMessageFile(messageRef);
|
|
635
|
-
}
|
|
636
|
-
})();
|
|
637
|
-
// Store result in memory
|
|
638
|
-
state.cliOutput = cliOutput;
|
|
639
|
-
state.completed = true;
|
|
640
|
-
// TELE-03: runtime_invocation_succeeded/failed — CLI process completed
|
|
641
|
-
const endedAt = new Date().toISOString();
|
|
642
|
-
const { telemetryEventType, errorCategory, stderrExcerpt } = OpenClawCliRuntimeAdapter.mapCliResultToTelemetry(cliOutput);
|
|
643
|
-
this.eventEmitter.emitTelemetry({
|
|
644
|
-
eventType: telemetryEventType,
|
|
645
|
-
traceId: input.taskRef?.taskId ?? runId,
|
|
646
|
-
timestamp: endedAt,
|
|
647
|
-
sessionId: 'openclaw-cli-adapter',
|
|
648
|
-
agentId: 'openclaw-cli-adapter',
|
|
649
|
-
payload: {
|
|
650
|
-
runId,
|
|
651
|
-
runtimeKind: 'openclaw-cli',
|
|
652
|
-
runtimeMode: this.runtimeMode,
|
|
653
|
-
exitCode: cliOutput.exitCode,
|
|
654
|
-
timedOut: cliOutput.timedOut,
|
|
655
|
-
...(errorCategory ? { errorCategory } : {}),
|
|
656
|
-
...(stderrExcerpt ? { stderrExcerpt } : {}),
|
|
657
|
-
},
|
|
658
|
-
});
|
|
659
|
-
return { runId, runtimeKind: 'openclaw-cli', startedAt };
|
|
660
|
-
}
|
|
661
|
-
async pollRun(runId) {
|
|
662
|
-
const state = this.runStateMap.get(runId);
|
|
663
|
-
if (!state) {
|
|
664
|
-
throw new PDRuntimeError('output_invalid', `Run ${runId} not found`);
|
|
665
|
-
}
|
|
666
|
-
if (!state.completed || !state.cliOutput) {
|
|
667
|
-
return { runId, status: 'running', startedAt: state.startedAt };
|
|
668
|
-
}
|
|
669
|
-
const { cliOutput } = state;
|
|
670
|
-
if (cliOutput.timedOut) {
|
|
671
|
-
return {
|
|
672
|
-
runId,
|
|
673
|
-
status: 'timed_out',
|
|
674
|
-
startedAt: state.startedAt,
|
|
675
|
-
endedAt: new Date().toISOString(),
|
|
676
|
-
reason: 'CLI timeout exceeded',
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
if (cliOutput.exitCode !== null && cliOutput.exitCode !== 0) {
|
|
680
|
-
const stderrExcerpt = cliOutput.stderr
|
|
681
|
-
? cliOutput.stderr.substring(0, 2000)
|
|
682
|
-
: undefined;
|
|
683
|
-
return {
|
|
684
|
-
runId,
|
|
685
|
-
status: 'failed',
|
|
686
|
-
startedAt: state.startedAt,
|
|
687
|
-
endedAt: new Date().toISOString(),
|
|
688
|
-
reason: stderrExcerpt
|
|
689
|
-
? `CLI exited with code ${cliOutput.exitCode}. stderr: ${stderrExcerpt}`
|
|
690
|
-
: `CLI exited with code ${cliOutput.exitCode}`,
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
return {
|
|
694
|
-
runId,
|
|
695
|
-
status: 'succeeded',
|
|
696
|
-
startedAt: state.startedAt,
|
|
697
|
-
endedAt: new Date().toISOString(),
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
// OCRA-03: fetchOutput parses CliOutput.stdout and returns DiagnosticianOutputV1
|
|
701
|
-
// OCRA-05: Malformed JSON repair + evidence persistence on output_invalid
|
|
702
|
-
async fetchOutput(runId) {
|
|
703
|
-
const state = this.runStateMap.get(runId);
|
|
704
|
-
if (!state || !state.completed || !state.cliOutput) {
|
|
705
|
-
throw new PDRuntimeError('output_invalid', `Run ${runId} not completed`);
|
|
706
|
-
}
|
|
707
|
-
const { cliOutput } = state;
|
|
708
|
-
// OCRA-04: Map ENOENT (binary not found) to runtime_unavailable
|
|
709
|
-
// spawnError is set explicitly by runCliProcess when spawn fails
|
|
710
|
-
if (cliOutput.spawnError === 'ENOENT') {
|
|
711
|
-
throw new PDRuntimeError('runtime_unavailable', 'openclaw binary not found');
|
|
712
|
-
}
|
|
713
|
-
// OCRA-04: Map timeout to timeout
|
|
714
|
-
if (cliOutput.timedOut) {
|
|
715
|
-
throw new PDRuntimeError('timeout', 'CLI timeout exceeded');
|
|
716
|
-
}
|
|
717
|
-
// OCRA-04: Map non-zero exit to execution_failed
|
|
718
|
-
if (cliOutput.exitCode !== null && cliOutput.exitCode !== 0) {
|
|
719
|
-
const stderrExcerpt = cliOutput.stderr
|
|
720
|
-
? cliOutput.stderr.substring(0, 2000)
|
|
721
|
-
: undefined;
|
|
722
|
-
throw new PDRuntimeError('execution_failed', stderrExcerpt
|
|
723
|
-
? `CLI exited with code ${cliOutput.exitCode}. stderr: ${stderrExcerpt}`
|
|
724
|
-
: `CLI exited with code ${cliOutput.exitCode}`);
|
|
725
|
-
}
|
|
726
|
-
// OCRA-03: Parse CliOutput — the stdout may be raw JSON, or wrapped in an OpenClaw envelope
|
|
727
|
-
// (local or gateway). First extract the JSON value, then navigate to the diagnosis JSON.
|
|
728
|
-
let parsed;
|
|
729
|
-
const rawParsed = extractPayloadFromCliOutput(cliOutput.stdout, cliOutput.stderr);
|
|
730
|
-
if (rawParsed !== null) {
|
|
731
|
-
// Try envelope extraction first (cases 2 and 3)
|
|
732
|
-
const fromEnvelope = extractDiagnosisFromEnvelope(rawParsed);
|
|
733
|
-
parsed = fromEnvelope ?? rawParsed;
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
parsed = null;
|
|
737
|
-
}
|
|
738
|
-
// OCRA-05: When initial JSON parse fails, attempt bounded repair via extractJsonObject
|
|
739
|
-
// and repairMalformedJson on the raw stdout/stderr text before giving up.
|
|
740
|
-
if (parsed === null) {
|
|
741
|
-
// Repair step 1: scan raw text for a JSON object using balanced-bracket extraction
|
|
742
|
-
const repairSources = [cliOutput.stdout, cliOutput.stderr ?? ''];
|
|
743
|
-
for (const source of repairSources) {
|
|
744
|
-
if (!source)
|
|
745
|
-
continue;
|
|
746
|
-
const repaired = extractJsonObject(source);
|
|
747
|
-
if (repaired !== null) {
|
|
748
|
-
parsed = repaired;
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
// Repair step 2: attempt syntactic repair of malformed JSON (e.g., unescaped quotes)
|
|
753
|
-
if (parsed === null) {
|
|
754
|
-
for (const source of repairSources) {
|
|
755
|
-
if (!source)
|
|
756
|
-
continue;
|
|
757
|
-
const repaired = repairMalformedJson(source);
|
|
758
|
-
if (repaired !== null) {
|
|
759
|
-
parsed = repaired;
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (parsed === null) {
|
|
765
|
-
// OCRA-05: Evidence persistence on output_invalid — no silent fallback (ERR-002)
|
|
766
|
-
// ERR-055/056: redact tokens/paths from raw output before persisting preview
|
|
767
|
-
const rawText = cliOutput.stdout || cliOutput.stderr || '';
|
|
768
|
-
const redacted = redactTelemetryString(rawText.substring(0, 2000));
|
|
769
|
-
const boundedPreview = truncatePreview(typeof redacted === 'string' ? redacted : '', 500);
|
|
770
|
-
throw new PDRuntimeError('output_invalid', 'Failed to parse CLI output as JSON', {
|
|
771
|
-
parseFailureReason: 'no_json_object_found',
|
|
772
|
-
boundedRawOutputPreview: boundedPreview,
|
|
773
|
-
nextAction: 'Check openclaw agent output format; ensure agent returns valid JSON',
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
// OCRA-03: Validate against the schema named by outputSchemaRef.
|
|
778
|
-
// The split diagnostician pipeline runs 3 stages with DIFFERENT output
|
|
779
|
-
// shapes: Stage A (diag_rootcause) → DiagRootCauseOutputV1, Stage B
|
|
780
|
-
// (diag_distiller) → DiagDistillerOutputV1, Stage C (diag_router) →
|
|
781
|
-
// DiagnosticianOutputV1. Each runner passes outputSchemaRef to name its
|
|
782
|
-
// expected shape. Previously this hardcoded DiagnosticianOutputV1Schema,
|
|
783
|
-
// which rejected every valid Stage A/B output → 0 candidates.
|
|
784
|
-
// Resolution rules (rc-3-fail-loud-missing):
|
|
785
|
-
// - outputSchemaRef present + in registry → validate against that schema
|
|
786
|
-
// - outputSchemaRef present + NOT in registry → fail loud (unknown ref)
|
|
787
|
-
// - outputSchemaRef absent → fall back to DiagnosticianOutputV1Schema
|
|
788
|
-
// (backward compat for legacy callers / monolithic diagnostician path)
|
|
789
|
-
let schema;
|
|
790
|
-
if (state.outputSchemaRef) {
|
|
791
|
-
const resolved = resolveOutputSchema(state.outputSchemaRef);
|
|
792
|
-
if (!resolved) {
|
|
793
|
-
throw new PDRuntimeError('output_invalid', `Unknown outputSchemaRef: ${state.outputSchemaRef}`, {
|
|
794
|
-
parseFailureReason: 'unknown_output_schema_ref',
|
|
795
|
-
outputSchemaRef: state.outputSchemaRef,
|
|
796
|
-
nextAction: `Register the schema in OUTPUT_SCHEMA_REGISTRY or pass a known ref (e.g. 'diag-rootcause-output-v1')`,
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
schema = resolved;
|
|
800
|
-
}
|
|
801
|
-
else {
|
|
802
|
-
schema = DiagnosticianOutputV1Schema;
|
|
803
|
-
}
|
|
804
|
-
// OCRA-05: On schema mismatch, include evidence with bounded preview (ERR-014/016/017)
|
|
805
|
-
// ERR-055/056: redact sensitive fields from parsed payload before persisting preview
|
|
806
|
-
if (!Value.Check(schema, parsed)) {
|
|
807
|
-
const schemaErrors = [...Value.Errors(schema, parsed)]
|
|
808
|
-
.slice(0, 10)
|
|
809
|
-
.map(e => ({ path: e.path, message: e.message }));
|
|
810
|
-
const redacted = redactSensitiveFields(parsed);
|
|
811
|
-
const boundedPreview = redacted.ok
|
|
812
|
-
? safeStringifyPreview(redacted.value, 500)
|
|
813
|
-
: safeStringifyPreview(parsed, 500);
|
|
814
|
-
throw new PDRuntimeError('output_invalid', `CLI output does not match ${state.outputSchemaRef ?? 'diagnostician-output-v1'} schema`, {
|
|
815
|
-
parseFailureReason: 'schema_validation_failed',
|
|
816
|
-
outputSchemaRef: state.outputSchemaRef ?? 'diagnostician-output-v1',
|
|
817
|
-
boundedRawOutputPreview: boundedPreview,
|
|
818
|
-
schemaErrors,
|
|
819
|
-
nextAction: 'Review schema errors; check LLM output format and required fields',
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
return { runId, payload: parsed };
|
|
823
|
-
}
|
|
824
|
-
async cancelRun(runId) {
|
|
825
|
-
// For one-shot run: if still in-flight, mark as cancelled
|
|
826
|
-
const state = this.runStateMap.get(runId);
|
|
827
|
-
if (state && !state.completed) {
|
|
828
|
-
state.completed = true;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
832
|
-
async fetchArtifacts(_runId) {
|
|
833
|
-
// No artifact refs produced by openclaw-cli adapter directly
|
|
834
|
-
return [];
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
//# sourceMappingURL=openclaw-cli-runtime-adapter.js.map
|