@entrorove/skill 0.1.32 → 0.1.34
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/CAPABILITY-MAP.md +20 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +31 -5
- package/automatic-feedback-client.mjs +43 -0
- package/cli.mjs +20 -1
- package/cloud-event-stream.mjs +27 -0
- package/cloud-task-client.mjs +34 -15
- package/cloud-watch-client.mjs +58 -0
- package/ide-presentation.mjs +58 -0
- package/local-cli.mjs +12 -1
- package/local-contract-types.mjs +9 -3
- package/local-contracts.mjs +31 -2
- package/local-error.mjs +19 -1
- package/local-execution.mjs +23 -1
- package/local-scope-config-imports.mjs +192 -0
- package/local-scope-repository.mjs +45 -9
- package/manifest.json +1 -1
- package/package.json +7 -1
- package/release-state.json +21 -15
- package/release.mjs +16 -6
- package/releases.json +12 -0
- package/remote-client.mjs +37 -9
- package/scope-runner-client.mjs +125 -0
package/remote-client.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import Ajv from 'ajv';
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
4
|
import { appendInvocation } from './local-invocations.mjs';
|
|
5
|
+
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
|
+
import { ideClientHeaders, presentInvocationNotices } from './ide-presentation.mjs';
|
|
5
7
|
|
|
6
8
|
export const apiUrl = 'https://entrorove.com/api/skill/invoke';
|
|
7
9
|
|
|
@@ -33,11 +35,17 @@ export function buildInvocation(operation, input) {
|
|
|
33
35
|
if (!['capabilities', 'task-describe', 'message-route', 'message-accept', 'task-resume', 'task-checkpoint',
|
|
34
36
|
'message-status', 'message-delivery-start', 'message-delivery-report', 'message-resolve', 'message-complete',
|
|
35
37
|
'handoff-prepare', 'handoff-release', 'handoff-claim', 'handoff-status', 'handoff-resume', 'handoff-execution-authority', 'mutation-snapshot-challenge', 'mutation-pass-issue',
|
|
38
|
+
'scope-runner-prepare', 'scope-runner-register', 'scope-runner-challenge', 'scope-runner-attest',
|
|
36
39
|
'start', 'continue'].includes(operation)) throw new Error('Invalid entrorove operation');
|
|
37
40
|
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Input must be a JSON object');
|
|
38
41
|
const id = requestId(operation, input);
|
|
39
42
|
return { url: apiUrl, requestId: id, body: { schemaVersion: 'entrorove.skill.request/2.0', requestId: id, operation, input } };
|
|
40
43
|
}
|
|
44
|
+
function requiresAutomaticFeedback(operation, input, capabilities) {
|
|
45
|
+
const actual = operation === 'continue' ? input.operation : operation;
|
|
46
|
+
return capabilities?.responseExtensions?.automaticFeedback === 'entrorove.automatic-feedback/1.0'
|
|
47
|
+
&& !capabilities.responseExtensions.automaticFeedbackObservations?.includes(actual);
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
export function validateDiscoveredChecks(input, checks = []) {
|
|
43
51
|
for (const check of checks) {
|
|
@@ -62,25 +70,35 @@ export async function invokeRemote(data, dependencies) {
|
|
|
62
70
|
process.exitCode = 2;
|
|
63
71
|
return;
|
|
64
72
|
}
|
|
65
|
-
const call = { ...buildInvocation(data.operation, data.input), url: `${trustedOrigin(apiOrigin())}/api/skill/invoke` };
|
|
66
73
|
const token = await readCredential(home);
|
|
74
|
+
const { response, result } = await invokeDiscovered(data, token, { home, apiOrigin });
|
|
75
|
+
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
|
|
76
|
+
if (!response.ok || result.output.status === 'blocked' || result.output.status === 'failed'
|
|
77
|
+
|| result.output.status === 'incomplete') process.exitCode = 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function invokeRemoteResult(data, dependencies) {
|
|
81
|
+
const token = await machineCredential(dependencies);
|
|
82
|
+
return invokeDiscovered(data, token, dependencies);
|
|
83
|
+
}
|
|
84
|
+
async function invokeDiscovered(data, token, { home, apiOrigin }) {
|
|
85
|
+
const call = { ...buildInvocation(data.operation, data.input), url: `${trustedOrigin(apiOrigin())}/api/skill/invoke` };
|
|
86
|
+
let feedbackRequired = false;
|
|
67
87
|
if (data.operation !== 'capabilities') {
|
|
68
88
|
const discovery = await postInvocation({ ...buildInvocation('capabilities', {}), url: call.url }, token, home);
|
|
69
89
|
if (!discovery.response.ok || discovery.result.output.status !== 'succeeded') throw new Error(`Capability discovery failed: HTTP ${discovery.response.status}`);
|
|
70
90
|
const capabilities = discovery.result.output.capabilities;
|
|
91
|
+
feedbackRequired = requiresAutomaticFeedback(data.operation,data.input,capabilities);
|
|
71
92
|
const schema = capabilities?.operationSchemas?.[data.operation];
|
|
72
93
|
if (!capabilities?.operations?.includes(data.operation) || !schema) throw new Error(`Capability discovery does not support ${data.operation}`);
|
|
73
94
|
const validate = discoveredAjv().compile(schema);
|
|
74
95
|
if (!validate(data.input)) throw new Error(`Invalid ${data.operation} input: ${validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ')}`);
|
|
75
96
|
validateDiscoveredChecks(data.input, capabilities.inputChecks?.[data.operation]);
|
|
76
97
|
}
|
|
77
|
-
|
|
78
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
79
|
-
if (!response.ok || result.output.status === 'blocked' || result.output.status === 'failed'
|
|
80
|
-
|| result.output.status === 'incomplete') process.exitCode = 2;
|
|
98
|
+
return postInvocation(call, token, home,feedbackRequired);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
async function postInvocation(call, token, home) {
|
|
101
|
+
async function postInvocation(call, token, home, feedbackRequired = false) {
|
|
84
102
|
const metadata = { kind: 'remote', operation: call.body.operation, requestId: call.requestId };
|
|
85
103
|
const logged = async value => {
|
|
86
104
|
try { return await appendInvocation({ ...metadata, ...value }, home); }
|
|
@@ -91,7 +109,7 @@ async function postInvocation(call, token, home) {
|
|
|
91
109
|
try {
|
|
92
110
|
response = await requestTransport(new URL(call.url).origin, '/api/skill/invoke', {
|
|
93
111
|
method: 'POST', timeoutMs: 135_000,
|
|
94
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}
|
|
112
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}`,...ideClientHeaders() }, body: JSON.stringify(call.body),
|
|
95
113
|
});
|
|
96
114
|
} catch (cause) {
|
|
97
115
|
if (cause.established) await logged({ event: 'outcome-unknown' });
|
|
@@ -113,6 +131,10 @@ async function postInvocation(call, token, home) {
|
|
|
113
131
|
await logged({ event: 'outcome-unknown' }); throw unknownInvocation(call.requestId);
|
|
114
132
|
}
|
|
115
133
|
await logged({ event: response.ok ? 'completed' : 'rejected', usage: result.usage ?? [] });
|
|
134
|
+
if (result.feedback) verifyAutomaticFeedback(result.feedback,call.requestId,call.requestId);
|
|
135
|
+
else if (feedbackRequired && !['running','pending'].includes(result.output.status)) {
|
|
136
|
+
throw new Error(`Automatic feedback receipt is missing; query feedback ${call.requestId}. Do not repeat the invocation.`);
|
|
137
|
+
}
|
|
116
138
|
return { response, result };
|
|
117
139
|
}
|
|
118
140
|
|
|
@@ -149,13 +171,16 @@ export function createTaskMessageClient(dependencies) {
|
|
|
149
171
|
async invoke(operation, input) {
|
|
150
172
|
const original = buildInvocation(operation, input);
|
|
151
173
|
const token = await machineCredential(dependencies); const origin = machineOrigin(dependencies);
|
|
174
|
+
let feedbackRequired = false;
|
|
152
175
|
if (operation !== 'capabilities') {
|
|
153
176
|
const discovery = buildInvocation('capabilities', {});
|
|
154
177
|
const discovered = await postInvocation({ ...discovery, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
|
|
155
178
|
if (!discovered.response.ok || discovered.result.output.status !== 'succeeded') throw new Error('Task message capability discovery failed');
|
|
156
179
|
validateMachineCapability(operation, input, discovered.result.output.capabilities);
|
|
180
|
+
feedbackRequired = requiresAutomaticFeedback(operation,input,discovered.result.output.capabilities);
|
|
157
181
|
}
|
|
158
|
-
const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
|
|
182
|
+
const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home,feedbackRequired);
|
|
183
|
+
presentInvocationNotices(result,dependencies.onIdeEvent);
|
|
159
184
|
if (stoppedOriginalResume(response, result.output, operation, input)) return result.output;
|
|
160
185
|
if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) {
|
|
161
186
|
throw new Error(`Task message invocation was rejected: HTTP ${response.status}; requestId=${original.requestId}`);
|
|
@@ -169,6 +194,8 @@ export function createTaskMessageClient(dependencies) {
|
|
|
169
194
|
{ authorization: `Bearer ${token}` }, 'original task message status');
|
|
170
195
|
const result = await readResponse(response, 'task message status');
|
|
171
196
|
if (!response.ok || !result?.output || result.output.requestId !== id) throw new Error('Original task message status is unavailable or invalid');
|
|
197
|
+
if (result.feedback) verifyAutomaticFeedback(result.feedback,id);
|
|
198
|
+
presentInvocationNotices(result,dependencies.onIdeEvent);
|
|
172
199
|
await appendInvocation({ kind: 'remote', operation: 'status', requestId: id, event: 'status', usage: result.usage ?? [] }, dependencies.home);
|
|
173
200
|
return result.output;
|
|
174
201
|
},
|
|
@@ -182,7 +209,8 @@ export async function queryStatus(requestId, dependencies) {
|
|
|
182
209
|
const result = await readResponse(response, 'status');
|
|
183
210
|
if (!response.ok || result?.output?.requestId !== requestId) throw new Error(`entrorove status failed: HTTP ${response.status}; requestId=${requestId}`);
|
|
184
211
|
await appendInvocation({ kind: 'remote', operation: 'status', requestId, event: 'status', usage: result.usage ?? [] }, dependencies.home);
|
|
185
|
-
|
|
212
|
+
if (result.feedback) verifyAutomaticFeedback(result.feedback,requestId);
|
|
213
|
+
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
|
|
186
214
|
}
|
|
187
215
|
|
|
188
216
|
export async function queryFlows(dependencies) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { constants, promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
5
|
+
import { scopeFail, scopePath, scopeRoot, scopeTarget } from './local-scope-files.mjs';
|
|
6
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
7
|
+
import { snapshotPlan, snapshotVerify } from './local-execution.mjs';
|
|
8
|
+
import { probe } from './local-probe.mjs';
|
|
9
|
+
import { invokeRemoteResult } from './remote-client.mjs';
|
|
10
|
+
|
|
11
|
+
async function privateDirectory(directory) {
|
|
12
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
13
|
+
const stat = await fs.lstat(directory);
|
|
14
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.getuid && stat.uid !== process.getuid()) {
|
|
15
|
+
scopeFail('ENTROROVE_SCOPE_STATE', 'Scope signer storage must belong to the current OS account');
|
|
16
|
+
}
|
|
17
|
+
if (process.platform !== 'win32') await fs.chmod(directory, 0o700);
|
|
18
|
+
}
|
|
19
|
+
async function readPrivate(file) {
|
|
20
|
+
const stat = await fs.lstat(file);
|
|
21
|
+
if (!stat.isFile() || stat.isSymbolicLink() || process.getuid && stat.uid !== process.getuid()
|
|
22
|
+
|| process.platform !== 'win32' && (stat.mode & 0o077)) scopeFail('ENTROROVE_SCOPE_STATE', 'Scope signer files must be private regular files');
|
|
23
|
+
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
24
|
+
try { return await handle.readFile('utf8'); } finally { await handle.close(); }
|
|
25
|
+
}
|
|
26
|
+
async function signer(directory, identity) {
|
|
27
|
+
const controls = path.join(directory, 'identity'), file = path.join(controls, 'binding.json');
|
|
28
|
+
let binding;
|
|
29
|
+
try { binding = JSON.parse(await readPrivate(file)); }
|
|
30
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
31
|
+
if (!binding) {
|
|
32
|
+
const keys = generateKeyPairSync('ed25519'), publicDer = keys.publicKey.export({ type: 'spki', format: 'der' });
|
|
33
|
+
binding = { schemaVersion: 'entrorove.local-scope-binding/1.0', ...identity, runnerId: `local-scope-${randomUUID()}`,
|
|
34
|
+
repositoryId: `local-repository-${sha256(identity.repositoryRoot).slice(0, 32)}`, publicKey: publicDer.toString('base64'), keyId: sha256(publicDer) };
|
|
35
|
+
const stage = path.join(directory, `.signer-${randomUUID()}`); await privateDirectory(stage);
|
|
36
|
+
try {
|
|
37
|
+
await fs.writeFile(path.join(stage, 'private.pem'), keys.privateKey.export({ type: 'pkcs8', format: 'pem' }), { flag: 'wx', mode: 0o600 });
|
|
38
|
+
await fs.writeFile(path.join(stage, 'signer.json'), `${JSON.stringify({ schemaVersion: 'entrorove.scope-signer/1.0',
|
|
39
|
+
privateKeyPath: path.join(controls, 'private.pem'), keyId: binding.keyId, receiptTtlMs: 60_000 })}\n`, { flag: 'wx', mode: 0o600 });
|
|
40
|
+
await fs.writeFile(path.join(stage, 'binding.json'), `${JSON.stringify(binding)}\n`, { flag: 'wx', mode: 0o600 });
|
|
41
|
+
await fs.rename(stage, controls);
|
|
42
|
+
} finally { await fs.rm(stage, { recursive: true, force: true }); }
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(identity).some(field => canonical(binding[field]) !== canonical(identity[field]))) {
|
|
45
|
+
scopeFail('ENTROROVE_SCOPE_REPLACEMENT', 'The original local task signer, repository and path set cannot be replaced');
|
|
46
|
+
}
|
|
47
|
+
await privateDirectory(controls);
|
|
48
|
+
const key = createPrivateKey(await readPrivate(path.join(controls, 'private.pem')));
|
|
49
|
+
if (key.asymmetricKeyType !== 'ed25519' || sha256(createPublicKey(key).export({ type: 'spki', format: 'der' })) !== binding.keyId) {
|
|
50
|
+
scopeFail('ENTROROVE_SCOPE_STATE', 'Original scope signer key identity changed');
|
|
51
|
+
}
|
|
52
|
+
return { binding, key, signerConfigPath: path.join(controls, 'signer.json') };
|
|
53
|
+
}
|
|
54
|
+
async function remote(operation, input, dependencies) {
|
|
55
|
+
const { response, result } = await invokeRemoteResult({ operation, input }, dependencies);
|
|
56
|
+
if (!response.ok || result.output.status !== 'succeeded') {
|
|
57
|
+
throw Object.assign(new Error(result.output.message ?? `Scope connection blocked at ${operation}`),
|
|
58
|
+
{ code: result.output.code ?? 'ENTROROVE_SCOPE_BLOCKED', output: result.output });
|
|
59
|
+
}
|
|
60
|
+
return result.output;
|
|
61
|
+
}
|
|
62
|
+
async function connect(identity, directory, dependencies) {
|
|
63
|
+
const { binding, key, signerConfigPath } = await signer(directory, identity);
|
|
64
|
+
const prepared = await remote('scope-runner-prepare', { flowId: identity.flowId, attemptId: randomUUID(), runnerId: binding.runnerId,
|
|
65
|
+
repositoryId: binding.repositoryId, publicKey: binding.publicKey, targetPaths: identity.targetPaths }, dependencies);
|
|
66
|
+
const enrollment = prepared.enrollment;
|
|
67
|
+
if (enrollment.flowId !== identity.flowId || enrollment.runnerId !== binding.runnerId || enrollment.keyId !== binding.keyId
|
|
68
|
+
|| canonical(enrollment.targetPaths) !== canonical(identity.targetPaths)) scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud enrollment differs from the original local task signer');
|
|
69
|
+
await remote('scope-runner-register', { flowId: identity.flowId, enrollmentId: enrollment.enrollmentId,
|
|
70
|
+
signature: sign(null, Buffer.from(canonical(enrollment)), key).toString('base64url') }, dependencies);
|
|
71
|
+
const issued = await remote('scope-runner-challenge', { flowId: identity.flowId, receiverId: binding.runnerId, attemptId: randomUUID() }, dependencies);
|
|
72
|
+
const challenge = issued.challenge;
|
|
73
|
+
if (challenge.flowId !== identity.flowId || challenge.ownerId !== enrollment.ownerId || challenge.runnerId !== binding.runnerId
|
|
74
|
+
|| challenge.repositoryId !== binding.repositoryId || challenge.taskDigest !== enrollment.taskDigest
|
|
75
|
+
|| challenge.chainDigest !== enrollment.chainDigest || canonical(challenge.targetPaths) !== canonical(identity.targetPaths)) {
|
|
76
|
+
scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud scope challenge differs from the original account and accepted task');
|
|
77
|
+
}
|
|
78
|
+
const input = { repositoryRoot: identity.repositoryRoot, home: dependencies.home, flowId: identity.flowId,
|
|
79
|
+
stepId: challenge.stepId, paths: identity.targetPaths, snapshotId: `snapshot-${randomUUID()}`, importContexts: identity.importContexts };
|
|
80
|
+
const planned = await snapshotPlan(input), verified = await snapshotVerify(input);
|
|
81
|
+
if (verified.verified !== true || verified.receipt.snapshotDigest !== planned.snapshotDigest) scopeFail('ENTROROVE_SCOPE_SNAPSHOT', 'Actual copied snapshot did not verify');
|
|
82
|
+
const observed = await probe({ ...input, ownerId: challenge.ownerId, task: challenge.task, taskDigest: challenge.taskDigest,
|
|
83
|
+
chainDigest: challenge.chainDigest, repositoryId: challenge.repositoryId, challenge: challenge.challenge, signerConfigPath });
|
|
84
|
+
const accepted = await remote('scope-runner-attest', { flowId: identity.flowId, attestation: observed.attestation }, dependencies);
|
|
85
|
+
return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'succeeded', flowId: identity.flowId,
|
|
86
|
+
runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
|
|
87
|
+
nextOperation: 'workflow-gate' };
|
|
88
|
+
}
|
|
89
|
+
export async function connectScopeRunner(repositoryRoot, input, dependencies) {
|
|
90
|
+
if (!input || Object.keys(input).some(field => !['flowId', 'paths', 'importContexts'].includes(field))
|
|
91
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(input.flowId ?? '') || !Array.isArray(input.paths) || !input.paths.length) {
|
|
92
|
+
scopeFail('ENTROROVE_SCOPE_INPUT', 'Expected {flowId,paths} for the original task and concrete existing source files');
|
|
93
|
+
}
|
|
94
|
+
const root = await scopeRoot(repositoryRoot), targetPaths = [...new Set(input.paths.map(scopePath))].sort();
|
|
95
|
+
if (targetPaths.length !== input.paths.length) scopeFail('ENTROROVE_SCOPE_INPUT', 'Scope paths must be unique');
|
|
96
|
+
for (const relative of targetPaths) {
|
|
97
|
+
const target = await scopeTarget(root, relative);
|
|
98
|
+
if (!target.stat.isFile()) scopeFail('ENTROROVE_SCOPE_INPUT', 'Scope connection requires concrete existing source files');
|
|
99
|
+
}
|
|
100
|
+
const importContexts = input.importContexts ?? [];
|
|
101
|
+
if (!Array.isArray(importContexts) || importContexts.length > 100 || new Set(importContexts.map(context => context?.sourcePrefix)).size !== importContexts.length) {
|
|
102
|
+
scopeFail('ENTROROVE_SCOPE_INPUT', 'Import contexts require unique source prefixes and at most 100 actual build configurations');
|
|
103
|
+
}
|
|
104
|
+
for (const context of importContexts) {
|
|
105
|
+
if (!context || Object.keys(context).sort().join(',') !== 'configPath,sourcePrefix') scopeFail('ENTROROVE_SCOPE_INPUT', 'Each import context requires sourcePrefix and configPath');
|
|
106
|
+
scopePath(context.sourcePrefix); scopePath(context.configPath);
|
|
107
|
+
}
|
|
108
|
+
const identity = { flowId: input.flowId, repositoryRoot: root, apiOrigin: dependencies.apiOrigin(), targetPaths, importContexts };
|
|
109
|
+
const parent = path.join(dependencies.home, '.entrorove');
|
|
110
|
+
await privateDirectory(parent);
|
|
111
|
+
const runners = path.join(parent, 'scope-runners'); await privateDirectory(runners);
|
|
112
|
+
const directory = path.join(runners, sha256(`${identity.apiOrigin}:${input.flowId}:${root}`)); await privateDirectory(directory);
|
|
113
|
+
return withStateLock(path.join(directory, 'connection'), () => connect(identity, directory, dependencies));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runScopeConnect(repositoryRoot, input, dependencies) {
|
|
117
|
+
await dependencies.install();
|
|
118
|
+
if (!await dependencies.agreementAccepted(dependencies.home)) return dependencies.agreementRequired(dependencies.home);
|
|
119
|
+
try { return await connectScopeRunner(repositoryRoot, input, dependencies); }
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.outcomeUnknown || !error.code?.startsWith('ENTROROVE_')) throw error;
|
|
122
|
+
return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'blocked', flowId: input?.flowId,
|
|
123
|
+
code: error.code, message: error.message, ...(error.output ? { output: error.output } : {}) };
|
|
124
|
+
}
|
|
125
|
+
}
|