meguro-mcp 0.2.10 → 0.2.13
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/CHANGELOG.md +26 -0
- package/README.md +35 -22
- package/package.json +1 -1
- package/scripts/generate-readme-tools.mjs +67 -12
- package/src/docs.mjs +3191 -70
- package/src/protocol.mjs +6 -0
- package/src/server.mjs +3 -3
- package/src/tools.mjs +1155 -480
package/src/tools.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Every control-plane call uses an account-scoped MEGURO_API_TOKEN (meg_sk_...).
|
|
4
4
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
-
import {
|
|
6
|
+
import { documentationToolContract, resolveDocumentationRead } from './docs.mjs';
|
|
7
7
|
import {
|
|
8
8
|
ADMIN_API_DEFAULT_VERSION,
|
|
9
9
|
ADMIN_API_SUPPORTED_VERSION_LABEL,
|
|
@@ -15,6 +15,8 @@ const SECRET_VALUE = /\bmeg_(?:pw|sk)_[a-z0-9_-]+\b|\b(?:Bearer|Basic)\s+[A-Za-z
|
|
|
15
15
|
const SECRET_HEADER = /\b(?:authorization|cookie|set-cookie|x-shopify-access-token|signed-snapshot-token)\s*[:=]\s*[^\s,;]+/giu;
|
|
16
16
|
const SECRET_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|signedsnapshot)/iu;
|
|
17
17
|
const PRIVATE_RESPONSE_KEY = /^(?:raw|rawBody|rawBodyPreview|responsePayload|requestHeaders|responseHeaders|headers|cookies)$/iu;
|
|
18
|
+
const MCP_TRUNCATION_SCHEMA_VERSION = 'meguro.mcp-truncation.v1';
|
|
19
|
+
const MCP_TRUNCATION_KEY = 'mcpTruncation';
|
|
18
20
|
const RESERVED_WORLD_IDS = new Set(['api', 'api-dev', 'www', 'dev', 'hooks', 'control']);
|
|
19
21
|
const PRACTICE_ATTEMPT_ID = /^pa-[a-z0-9][a-z0-9-]{0,124}$/u;
|
|
20
22
|
export const OPERATION_COMMIT_BOUNDARY_META_KEY = 'meguro/operationCommitBoundary';
|
|
@@ -25,15 +27,15 @@ const AUTH_TEACHING = Object.freeze({
|
|
|
25
27
|
const DOCUMENTATION_TOOL_CONTRACT = documentationToolContract();
|
|
26
28
|
const DOCUMENTATION_TOPICS = new Set(DOCUMENTATION_TOOL_CONTRACT.topics);
|
|
27
29
|
const FLEET_TOOL_NAMES = new Set([
|
|
28
|
-
'templates_list', 'stores_list', 'store_create', 'store_delete', 'store_passport',
|
|
30
|
+
'templates_list', 'template_get', 'stores_list', 'store_create', 'store_delete', 'store_passport',
|
|
29
31
|
'practice_runs_list',
|
|
30
32
|
'workspaces_list', 'workspace_create', 'workspace_archive', 'workspace_unarchive',
|
|
31
|
-
'share_create', 'shares_list', 'share_status', 'share_publish', 'share_revoke',
|
|
32
33
|
'catalog_slice_read', 'catalog_slice_snapshot', 'catalog_slices_saved',
|
|
33
34
|
'store_claim_by_code', 'store_claim',
|
|
34
35
|
]);
|
|
35
36
|
const TOOL_PRESENTATION = Object.freeze({
|
|
36
37
|
templates_list: { title: 'List practice-store templates', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
38
|
+
template_get: { title: 'Read one practice-store template', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
37
39
|
stores_list: { title: 'List practice stores', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
38
40
|
store_create: { title: 'Create a practice store', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
39
41
|
store_delete: { title: 'Delete a practice store', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
@@ -42,11 +44,6 @@ const TOOL_PRESENTATION = Object.freeze({
|
|
|
42
44
|
workspace_create: { title: 'Create a workspace', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
43
45
|
workspace_archive: { title: 'Archive a workspace', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
44
46
|
workspace_unarchive: { title: 'Restore a workspace', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
45
|
-
share_create: { title: 'Create or update a private share draft', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
46
|
-
shares_list: { title: 'List evidence shares', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
47
|
-
share_status: { title: 'Read an evidence share', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
48
|
-
share_publish: { title: 'Publish evidence to client viewers', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
49
|
-
share_revoke: { title: 'Revoke client-viewer evidence access', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
50
47
|
catalog_slice_read: { title: 'Read a live Shopify catalog slice', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
51
48
|
catalog_slice_snapshot: { title: 'Mint a catalog slice snapshot', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
52
49
|
catalog_slices_saved: { title: 'Manage saved catalog slices', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
@@ -58,9 +55,9 @@ const TOOL_PRESENTATION = Object.freeze({
|
|
|
58
55
|
run_report: { title: 'Read run receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
59
56
|
run_resume: { title: 'Resume a Shopify dev-store run', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
60
57
|
runs_diff: { title: 'Compare history-run receipts', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
61
|
-
gate_configure: { title: 'Configure
|
|
62
|
-
gate_evaluate: { title: 'Evaluate
|
|
63
|
-
gate_verdict: { title: 'Read
|
|
58
|
+
gate_configure: { title: 'Configure Configured Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
59
|
+
gate_evaluate: { title: 'Evaluate Configured Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
60
|
+
gate_verdict: { title: 'Read Configured Receipt Gate verdict', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
64
61
|
runs_list: { title: 'List history runs', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
65
62
|
usage_read: { title: 'Read usage headroom', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
66
63
|
twin_diff: { title: 'Read a twin impact receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
@@ -80,12 +77,23 @@ const TOOL_PRESENTATION = Object.freeze({
|
|
|
80
77
|
get_connection_details: { title: 'Get practice-store connection details', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
81
78
|
admin_probe: { title: 'Run an Admin API probe', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
82
79
|
admin_schema: { title: 'Look up the Admin API schema', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
80
|
+
admin_recipes_list: { title: 'List Admin API recipes', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
81
|
+
plan_validate: { title: 'Check an intended Admin operation plan against Meguro', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
function textResult(value) {
|
|
86
85
|
return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
|
|
87
86
|
}
|
|
88
87
|
|
|
88
|
+
function configuredReceiptGateResult(value, action) {
|
|
89
|
+
const result = textResult(value);
|
|
90
|
+
result.content.push({
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `Configured Receipt Gate ${action}. This optional configured policy is separate from the receipt's automatic recorded API behavior; the receipt's recorded API behavior remains available.`,
|
|
93
|
+
});
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
function errorResult(message) {
|
|
90
98
|
return { content: [{ type: 'text', text: scrubString(message) }], isError: true };
|
|
91
99
|
}
|
|
@@ -108,19 +116,60 @@ export function redactSecrets(value) {
|
|
|
108
116
|
return scrubString(value);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
function
|
|
119
|
+
function secretSafePath(parent, key, arrayIndex = false) {
|
|
120
|
+
if (arrayIndex) return `${parent}[${key}]`;
|
|
121
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key)
|
|
122
|
+
? `${parent}.${key}`
|
|
123
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function secretSafeValue(value, depth, path, truncations) {
|
|
112
127
|
if (depth > 12) return '[TRUNCATED]';
|
|
113
|
-
if (typeof value === 'string')
|
|
114
|
-
|
|
128
|
+
if (typeof value === 'string') {
|
|
129
|
+
const scrubbed = scrubString(value);
|
|
130
|
+
const returned = scrubbed.slice(0, 10_000);
|
|
131
|
+
if (returned.length < scrubbed.length) {
|
|
132
|
+
truncations.push({ path, kind: 'string', originalSize: scrubbed.length, returnedSize: returned.length });
|
|
133
|
+
}
|
|
134
|
+
return returned;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
const returned = value.slice(0, 100);
|
|
138
|
+
if (returned.length < value.length) {
|
|
139
|
+
truncations.push({ path, kind: 'array', originalSize: value.length, returnedSize: returned.length });
|
|
140
|
+
}
|
|
141
|
+
return returned.map((child, index) => secretSafeValue(
|
|
142
|
+
child,
|
|
143
|
+
depth + 1,
|
|
144
|
+
secretSafePath(path, index, true),
|
|
145
|
+
truncations,
|
|
146
|
+
));
|
|
147
|
+
}
|
|
115
148
|
if (value && typeof value === 'object') {
|
|
116
|
-
|
|
117
|
-
.filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key))
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
const visibleEntries = Object.entries(value)
|
|
150
|
+
.filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key));
|
|
151
|
+
const returned = visibleEntries.slice(0, 100);
|
|
152
|
+
if (returned.length < visibleEntries.length) {
|
|
153
|
+
truncations.push({ path, kind: 'object', originalSize: visibleEntries.length, returnedSize: returned.length });
|
|
154
|
+
}
|
|
155
|
+
return Object.fromEntries(returned.map(([key, child]) => [
|
|
156
|
+
key,
|
|
157
|
+
secretSafeValue(child, depth + 1, secretSafePath(path, key), truncations),
|
|
158
|
+
]));
|
|
120
159
|
}
|
|
121
160
|
return value;
|
|
122
161
|
}
|
|
123
162
|
|
|
163
|
+
function secretSafe(value, depth = 0) {
|
|
164
|
+
const truncations = [];
|
|
165
|
+
const safe = secretSafeValue(value, depth, '$', truncations);
|
|
166
|
+
if (truncations.length === 0) return safe;
|
|
167
|
+
const evidence = { schemaVersion: MCP_TRUNCATION_SCHEMA_VERSION, entries: truncations };
|
|
168
|
+
return safe && typeof safe === 'object' && !Array.isArray(safe)
|
|
169
|
+
? { ...safe, [MCP_TRUNCATION_KEY]: evidence }
|
|
170
|
+
: { value: safe, [MCP_TRUNCATION_KEY]: evidence };
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
function authenticationErrorResult(status, value) {
|
|
125
174
|
if (status !== 401 && status !== 403) return null;
|
|
126
175
|
const body = value && typeof value === 'object' ? value : {};
|
|
@@ -176,8 +225,55 @@ function catalogSnapshotProjection(value) {
|
|
|
176
225
|
: safe;
|
|
177
226
|
}
|
|
178
227
|
|
|
228
|
+
function enforcementProjection(body) {
|
|
229
|
+
if (!body || typeof body !== 'object') return {};
|
|
230
|
+
const hasStructuredEnforcement = typeof body.blockedCapability === 'string'
|
|
231
|
+
|| typeof body.tier === 'string'
|
|
232
|
+
|| Number.isFinite(body.allowance)
|
|
233
|
+
|| Number.isFinite(body.currentUsage)
|
|
234
|
+
|| Number.isFinite(body.requestedAmount)
|
|
235
|
+
|| (body.sameTierAction && typeof body.sameTierAction === 'object')
|
|
236
|
+
|| typeof body.targetTier === 'string'
|
|
237
|
+
|| typeof body.upgradePath === 'string';
|
|
238
|
+
if (!hasStructuredEnforcement) return {};
|
|
239
|
+
return {
|
|
240
|
+
...(typeof body.code === 'string' ? { code: body.code } : {}),
|
|
241
|
+
...(typeof body.blockedCapability === 'string' ? { blockedCapability: body.blockedCapability } : {}),
|
|
242
|
+
...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
|
|
243
|
+
...(Number.isFinite(body.allowance) ? { allowance: body.allowance } : {}),
|
|
244
|
+
...(Number.isFinite(body.currentUsage) ? { currentUsage: body.currentUsage } : {}),
|
|
245
|
+
...(Number.isFinite(body.requestedAmount) ? { requestedAmount: body.requestedAmount } : {}),
|
|
246
|
+
...(body.sameTierAction && typeof body.sameTierAction === 'object'
|
|
247
|
+
? { sameTierAction: secretSafe(body.sameTierAction) }
|
|
248
|
+
: {}),
|
|
249
|
+
...(typeof body.targetTier === 'string' ? { targetTier: body.targetTier } : {}),
|
|
250
|
+
...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* MEG-1058: the compact list tier. `templates_list` carries only what a cold agent needs to CHOOSE;
|
|
256
|
+
* everything it needs to USE a template is one `template_get` hop away. Both are projected from the
|
|
257
|
+
* same compiled catalog, so there is no second template contract.
|
|
258
|
+
*/
|
|
259
|
+
const STORE_TEMPLATE_LIST_FIELDS = Object.freeze([
|
|
260
|
+
'key',
|
|
261
|
+
'label',
|
|
262
|
+
'category',
|
|
263
|
+
'recommended',
|
|
264
|
+
'bestFor',
|
|
265
|
+
'recommendedRunDays',
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* MEG-1058: the detail tier's allowlist — the complete public-safe record `template_get` returns.
|
|
270
|
+
* `revision` and `evaluationContext` join it here: both are ordinary template declarations the
|
|
271
|
+
* detail hop is meant to answer with, and a record missing them is not the complete one.
|
|
272
|
+
*/
|
|
179
273
|
const STORE_TEMPLATE_FIELDS = Object.freeze([
|
|
180
274
|
'key',
|
|
275
|
+
'revision',
|
|
276
|
+
'evaluationContext',
|
|
181
277
|
'label',
|
|
182
278
|
'category',
|
|
183
279
|
'recommended',
|
|
@@ -192,47 +288,204 @@ const STORE_TEMPLATE_FIELDS = Object.freeze([
|
|
|
192
288
|
'extendedRunDays',
|
|
193
289
|
'whatAgentGets',
|
|
194
290
|
'supportedReads',
|
|
291
|
+
'modeledEvidence',
|
|
195
292
|
'liveRun',
|
|
196
293
|
'goodFirstAgents',
|
|
197
294
|
'supportedWrites',
|
|
295
|
+
'operationContracts',
|
|
198
296
|
'knownUnsupported',
|
|
297
|
+
'unsupportedContracts',
|
|
298
|
+
'capabilityBoundaries',
|
|
299
|
+
'modeledPhysics',
|
|
199
300
|
'goodAgentShould',
|
|
200
301
|
'avoid',
|
|
201
302
|
'reportGrades',
|
|
202
303
|
]);
|
|
203
304
|
|
|
305
|
+
/**
|
|
306
|
+
* MEG-1058: the detail tier's projection. Same allowlist the list used to carry, applied to the one
|
|
307
|
+
* record the server selected — the catalog is never fetched or returned as this tool's result.
|
|
308
|
+
*/
|
|
309
|
+
function storeTemplateDetailProjection(value) {
|
|
310
|
+
const storeTemplate = value?.storeTemplate;
|
|
311
|
+
if (!storeTemplate || typeof storeTemplate !== 'object' || Array.isArray(storeTemplate) || typeof storeTemplate.key !== 'string') {
|
|
312
|
+
throw new Error('Meguro template detail response did not contain a storeTemplate');
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
storeTemplate: Object.fromEntries(STORE_TEMPLATE_FIELDS
|
|
316
|
+
.filter((field) => Object.hasOwn(storeTemplate, field))
|
|
317
|
+
.map((field) => [field, secretSafe(storeTemplate[field], 1)])),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
204
321
|
function storeTemplatesProjection(value) {
|
|
205
322
|
if (!Array.isArray(value?.storeTemplates)) {
|
|
206
323
|
throw new Error('Meguro template catalog response did not contain storeTemplates');
|
|
207
324
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
325
|
+
const storeTemplates = value.storeTemplates.map((template) => {
|
|
326
|
+
if (!template || typeof template !== 'object' || Array.isArray(template) || typeof template.key !== 'string') {
|
|
327
|
+
throw new Error('Meguro template catalog contained an invalid template entry');
|
|
328
|
+
}
|
|
329
|
+
return Object.fromEntries(STORE_TEMPLATE_LIST_FIELDS
|
|
330
|
+
.filter((field) => Object.hasOwn(template, field))
|
|
331
|
+
.map((field) => [field, secretSafe(template[field], 1)]));
|
|
332
|
+
});
|
|
333
|
+
const catalogTemplateKeys = storeTemplates.map((template) => template.key);
|
|
334
|
+
const declaredTemplateKeys = value?.starterRecommendation?.templateKeys;
|
|
335
|
+
const starterTemplateKeys = Array.isArray(declaredTemplateKeys)
|
|
336
|
+
&& declaredTemplateKeys.every((key) => typeof key === 'string' && catalogTemplateKeys.includes(key))
|
|
337
|
+
? [...declaredTemplateKeys]
|
|
338
|
+
: [];
|
|
339
|
+
let invalidRecommendation = starterTemplateKeys.length !== (declaredTemplateKeys?.length ?? -1);
|
|
211
340
|
const declaredStarterBasis = typeof value?.starterRecommendation?.basis === 'string'
|
|
212
341
|
&& /^[a-z0-9-]{1,64}$/u.test(value.starterRecommendation.basis)
|
|
213
342
|
? value.starterRecommendation.basis
|
|
214
343
|
: 'starter-for-first-simulation';
|
|
344
|
+
const projectStarterPlan = (starterPlan) => {
|
|
345
|
+
const positiveInteger = (candidate) => Number.isSafeInteger(candidate) && candidate > 0;
|
|
346
|
+
const nonNegativeInteger = (candidate) => Number.isSafeInteger(candidate) && candidate >= 0;
|
|
347
|
+
const valid = starterPlan
|
|
348
|
+
&& typeof starterPlan === 'object'
|
|
349
|
+
&& !Array.isArray(starterPlan)
|
|
350
|
+
&& starterPlan.schemaVersion === 'meguro.practice-run-starter-plan.v1'
|
|
351
|
+
&& ['developer', 'solo', 'builder'].includes(starterPlan.tier)
|
|
352
|
+
&& typeof starterPlan.templateKey === 'string'
|
|
353
|
+
&& catalogTemplateKeys.includes(starterPlan.templateKey)
|
|
354
|
+
&& positiveInteger(starterPlan.idealRunDays)
|
|
355
|
+
&& positiveInteger(starterPlan.scenarioArcDays)
|
|
356
|
+
&& positiveInteger(starterPlan.firstSegmentDays)
|
|
357
|
+
&& starterPlan.practiceRunStart
|
|
358
|
+
&& typeof starterPlan.practiceRunStart === 'object'
|
|
359
|
+
&& starterPlan.practiceRunStart.clock
|
|
360
|
+
&& typeof starterPlan.practiceRunStart.clock === 'object'
|
|
361
|
+
&& positiveInteger(starterPlan.practiceRunStart.clock.simulationDays)
|
|
362
|
+
&& starterPlan.continuation
|
|
363
|
+
&& typeof starterPlan.continuation === 'object'
|
|
364
|
+
&& typeof starterPlan.continuation.available === 'boolean'
|
|
365
|
+
&& nonNegativeInteger(starterPlan.continuation.remainingArcDaysAfterFirstSegment)
|
|
366
|
+
&& (starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment === undefined
|
|
367
|
+
|| nonNegativeInteger(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment))
|
|
368
|
+
&& positiveInteger(starterPlan.continuation.maxCanvasWorldDays)
|
|
369
|
+
&& (starterPlan.authorization === undefined || (
|
|
370
|
+
starterPlan.authorization
|
|
371
|
+
&& ['account-onboarding-grant', 'ordinary-tier'].includes(starterPlan.authorization.source)
|
|
372
|
+
&& positiveInteger(starterPlan.authorization.idealHorizonDays)
|
|
373
|
+
))
|
|
374
|
+
&& starterPlan.usage
|
|
375
|
+
&& typeof starterPlan.usage === 'object'
|
|
376
|
+
&& typeof starterPlan.usage.runStartAllowed === 'boolean'
|
|
377
|
+
&& ((starterPlan.usage.runStartAllowedMeaning === undefined && starterPlan.usage.runStartAllowance === undefined) || (
|
|
378
|
+
starterPlan.usage.runStartAllowedMeaning === 'metering-allowance-only'
|
|
379
|
+
&& starterPlan.usage.runStartAllowance
|
|
380
|
+
&& typeof starterPlan.usage.runStartAllowance === 'object'
|
|
381
|
+
&& starterPlan.usage.runStartAllowance.schemaVersion === 'meguro.run-start-allowance.v1'
|
|
382
|
+
&& starterPlan.usage.runStartAllowance.permitsChargedRun === starterPlan.usage.runStartAllowed
|
|
383
|
+
&& ['metering-allowance-available', 'metering-allowance-exhausted'].includes(starterPlan.usage.runStartAllowance.reasonCode)
|
|
384
|
+
&& starterPlan.usage.runStartAllowance.meaning === 'metering-allowance-only'
|
|
385
|
+
&& starterPlan.usage.runStartAllowance.detail === 'Whether current tier metering allowance permits one charged simulation run; this is not overall store/root lifecycle eligibility.'
|
|
386
|
+
))
|
|
387
|
+
&& nonNegativeInteger(starterPlan.usage.remainingSimulationRuns);
|
|
388
|
+
if (!valid) throw new Error('Meguro template catalog contained an invalid starterPlan');
|
|
389
|
+
return {
|
|
390
|
+
schemaVersion: starterPlan.schemaVersion,
|
|
391
|
+
tier: starterPlan.tier,
|
|
392
|
+
templateKey: starterPlan.templateKey,
|
|
393
|
+
idealRunDays: starterPlan.idealRunDays,
|
|
394
|
+
scenarioArcDays: starterPlan.scenarioArcDays,
|
|
395
|
+
firstSegmentDays: starterPlan.firstSegmentDays,
|
|
396
|
+
...(starterPlan.authorization ? { authorization: {
|
|
397
|
+
source: starterPlan.authorization.source,
|
|
398
|
+
idealHorizonDays: starterPlan.authorization.idealHorizonDays,
|
|
399
|
+
} } : {}),
|
|
400
|
+
practiceRunStart: { clock: { simulationDays: starterPlan.practiceRunStart.clock.simulationDays } },
|
|
401
|
+
continuation: {
|
|
402
|
+
available: starterPlan.continuation.available,
|
|
403
|
+
remainingArcDaysAfterFirstSegment: starterPlan.continuation.remainingArcDaysAfterFirstSegment,
|
|
404
|
+
...(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment !== undefined
|
|
405
|
+
? { remainingAuthorizedDaysAfterFirstSegment: starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment }
|
|
406
|
+
: {}),
|
|
407
|
+
maxCanvasWorldDays: starterPlan.continuation.maxCanvasWorldDays,
|
|
408
|
+
},
|
|
409
|
+
usage: {
|
|
410
|
+
runStartAllowed: starterPlan.usage.runStartAllowed,
|
|
411
|
+
...(starterPlan.usage.runStartAllowedMeaning ? {
|
|
412
|
+
runStartAllowedMeaning: starterPlan.usage.runStartAllowedMeaning,
|
|
413
|
+
runStartAllowance: {
|
|
414
|
+
schemaVersion: starterPlan.usage.runStartAllowance.schemaVersion,
|
|
415
|
+
permitsChargedRun: starterPlan.usage.runStartAllowance.permitsChargedRun,
|
|
416
|
+
reasonCode: starterPlan.usage.runStartAllowance.reasonCode,
|
|
417
|
+
meaning: starterPlan.usage.runStartAllowance.meaning,
|
|
418
|
+
detail: starterPlan.usage.runStartAllowance.detail,
|
|
419
|
+
},
|
|
420
|
+
} : {}),
|
|
421
|
+
remainingSimulationRuns: starterPlan.usage.remainingSimulationRuns,
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
const starterPlan = value?.starterRecommendation?.starterPlan;
|
|
426
|
+
const starterPlans = value?.starterRecommendation?.starterPlans;
|
|
427
|
+
let projectedStarterPlan;
|
|
428
|
+
let projectedStarterPlans;
|
|
429
|
+
try {
|
|
430
|
+
projectedStarterPlans = starterPlans === undefined
|
|
431
|
+
? undefined
|
|
432
|
+
: Array.isArray(starterPlans) ? starterPlans.map(projectStarterPlan) : (() => { throw new Error('invalid starterPlans'); })();
|
|
433
|
+
if (projectedStarterPlans) {
|
|
434
|
+
const planKeys = projectedStarterPlans.map((plan) => plan.templateKey);
|
|
435
|
+
if (new Set(planKeys).size !== planKeys.length
|
|
436
|
+
|| planKeys.some((key) => !catalogTemplateKeys.includes(key))) {
|
|
437
|
+
throw new Error('invalid starterPlans');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
projectedStarterPlan = starterPlan === undefined ? undefined : projectStarterPlan(starterPlan);
|
|
441
|
+
if (projectedStarterPlan && !starterTemplateKeys.includes(projectedStarterPlan.templateKey)) {
|
|
442
|
+
throw new Error('invalid selected starterPlan');
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
projectedStarterPlan = undefined;
|
|
446
|
+
projectedStarterPlans = undefined;
|
|
447
|
+
invalidRecommendation = true;
|
|
448
|
+
}
|
|
449
|
+
const serverUnavailable = value?.starterRecommendation?.recommendationUnavailable;
|
|
450
|
+
const projectedUnavailable = serverUnavailable
|
|
451
|
+
&& typeof serverUnavailable === 'object'
|
|
452
|
+
&& serverUnavailable.schemaVersion === 'meguro.practice-starter-recommendation-unavailable.v1'
|
|
453
|
+
&& ['run-start-unavailable', 'usage-unavailable'].includes(serverUnavailable.code)
|
|
454
|
+
? {
|
|
455
|
+
schemaVersion: serverUnavailable.schemaVersion,
|
|
456
|
+
code: serverUnavailable.code,
|
|
457
|
+
}
|
|
458
|
+
: undefined;
|
|
459
|
+
const recommendationUnavailable = invalidRecommendation
|
|
460
|
+
? {
|
|
461
|
+
schemaVersion: 'meguro.practice-starter-recommendation-unavailable.v1',
|
|
462
|
+
code: 'invalid-server-recommendation',
|
|
463
|
+
}
|
|
464
|
+
: projectedUnavailable;
|
|
215
465
|
return {
|
|
216
466
|
starterRecommendation: {
|
|
217
467
|
basis: declaredStarterBasis,
|
|
218
468
|
templateKeys: starterTemplateKeys,
|
|
469
|
+
...(projectedStarterPlan ? { starterPlan: projectedStarterPlan } : {}),
|
|
470
|
+
...(projectedStarterPlans ? { starterPlans: projectedStarterPlans } : {}),
|
|
471
|
+
...(recommendationUnavailable ? { recommendationUnavailable } : {}),
|
|
219
472
|
},
|
|
220
|
-
storeTemplates
|
|
221
|
-
if (!template || typeof template !== 'object' || Array.isArray(template)) {
|
|
222
|
-
throw new Error('Meguro template catalog contained an invalid template entry');
|
|
223
|
-
}
|
|
224
|
-
return Object.fromEntries(STORE_TEMPLATE_FIELDS
|
|
225
|
-
.filter((field) => Object.hasOwn(template, field))
|
|
226
|
-
.map((field) => [field, secretSafe(template[field], 1)]));
|
|
227
|
-
}),
|
|
473
|
+
storeTemplates,
|
|
228
474
|
};
|
|
229
475
|
}
|
|
230
476
|
|
|
231
|
-
function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
|
|
477
|
+
function practiceErrorResult(status, value, retryAfterSeconds, teaching, declaredBoundary) {
|
|
232
478
|
const authenticationError = authenticationErrorResult(status, value);
|
|
233
479
|
if (authenticationError) return authenticationError;
|
|
234
480
|
const body = value && typeof value === 'object' ? value : {};
|
|
235
|
-
const stable = { httpStatus: status };
|
|
481
|
+
const stable = { httpStatus: status, ...enforcementProjection(body) };
|
|
482
|
+
if (body.code === 'terminal-advance-confirmation-required') {
|
|
483
|
+
stable.code = body.code;
|
|
484
|
+
stable.terminalAdvance = secretSafe(body.terminalAdvance);
|
|
485
|
+
}
|
|
486
|
+
if (body.advanceCursor && typeof body.advanceCursor === 'object') {
|
|
487
|
+
stable.advanceCursor = secretSafe(body.advanceCursor);
|
|
488
|
+
}
|
|
236
489
|
if (['requested-exceeds-cap', 'continuation-unavailable', 'limit-reached'].includes(body.reason)) {
|
|
237
490
|
stable.reason = body.reason;
|
|
238
491
|
}
|
|
@@ -253,7 +506,16 @@ function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
|
|
|
253
506
|
else if (Number.isFinite(bodyRetryAfter) && bodyRetryAfter >= 0) stable.retryAfterSeconds = bodyRetryAfter;
|
|
254
507
|
if (typeof body.retryable === 'boolean') stable.retryable = body.retryable;
|
|
255
508
|
if (teaching) stable.teaching = secretSafe(teaching);
|
|
256
|
-
|
|
509
|
+
const result = { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
|
|
510
|
+
// MEG-1057: an attempt-identity 404 is pre-commit by construction — nothing can have applied
|
|
511
|
+
// against an attempt that does not exist. Without this the mutating routes fell through to the
|
|
512
|
+
// caller-facing default of `unknown`, which asks the agent to go read state before retrying an
|
|
513
|
+
// operation that never had a target.
|
|
514
|
+
if (isPracticeRunUnavailable(status, body)) return withOperationCommitBoundary(result, 'pre-commit');
|
|
515
|
+
if (declaredBoundary) return withOperationCommitBoundary(result, declaredBoundary);
|
|
516
|
+
return ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
|
|
517
|
+
? withOperationCommitBoundary(result, body.operationCommitBoundary)
|
|
518
|
+
: result;
|
|
257
519
|
}
|
|
258
520
|
|
|
259
521
|
function practiceRunUnavailableTeaching(toolName, attemptId) {
|
|
@@ -266,22 +528,30 @@ function practiceRunUnavailableTeaching(toolName, attemptId) {
|
|
|
266
528
|
|
|
267
529
|
function gateReceiptUnavailableTeaching(toolName, receiptId) {
|
|
268
530
|
return {
|
|
269
|
-
meaning: `No completed practice simulation receipt is available for receiptId ${receiptId}. Receipt Gate uses the pa-* practice-run attempt identity, not a public receipt digest or a history-run id.`,
|
|
531
|
+
meaning: `No completed practice simulation receipt is available for receiptId ${receiptId}. Configured Receipt Gate uses the pa-* practice-run attempt identity, not a public receipt digest or a history-run id.`,
|
|
270
532
|
nextStep: `Call stores_list({}), choose an exact storeId, call practice_run_start({ storeId: "<exact storeId>" }), complete the simulation, call practice_run_finish({ attemptId: "<returned attemptId>" }), then call practice_run_report({ attemptId: "<returned attemptId>" }). Use that same returned attemptId as receiptId and call ${toolName}({ receiptId: "<returned attemptId>" }).`,
|
|
271
|
-
stopCondition: 'If practice_run_report does not return a completed receipt for that attemptId, stop. Do not retry Receipt Gate with an unavailable receiptId.',
|
|
533
|
+
stopCondition: 'If practice_run_report does not return a completed receipt for that attemptId, stop. Do not retry Configured Receipt Gate with an unavailable receiptId.',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function practiceStoreUnavailableTeaching(storeId) {
|
|
538
|
+
return {
|
|
539
|
+
meaning: `No practice store with storeId ${storeId} is available to this account, so no practice run was started.`,
|
|
540
|
+
nextStep: 'Call stores_list({}), choose an exact current storeId, then call practice_run_start({ storeId: "<exact storeId>" }).',
|
|
541
|
+
stopCondition: `If stores_list({}) does not return ${storeId}, stop. Do not retry practice_run_start with the unavailable storeId.`,
|
|
272
542
|
};
|
|
273
543
|
}
|
|
274
544
|
|
|
275
545
|
function isPracticeRunUnavailable(status, value) {
|
|
276
|
-
return status === 404
|
|
546
|
+
return status === 404;
|
|
277
547
|
}
|
|
278
548
|
|
|
279
549
|
function isGateReceiptUnavailable(status, value) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return
|
|
550
|
+
return status === 404;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isPracticeStoreUnavailable(status, value) {
|
|
554
|
+
return status === 404;
|
|
285
555
|
}
|
|
286
556
|
|
|
287
557
|
function requiredString(args, key) {
|
|
@@ -311,11 +581,11 @@ function requiredWorkspaceId(args) {
|
|
|
311
581
|
return workspaceId;
|
|
312
582
|
}
|
|
313
583
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
584
|
+
const OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA = Object.freeze({
|
|
585
|
+
type: 'string',
|
|
586
|
+
minLength: 1,
|
|
587
|
+
description: 'Optional account-owned workspace selector. Omit for Default.',
|
|
588
|
+
});
|
|
319
589
|
|
|
320
590
|
function optionalStringList(args, key, options = {}) {
|
|
321
591
|
const value = args?.[key];
|
|
@@ -351,6 +621,115 @@ function requiredSavedSliceId(args) {
|
|
|
351
621
|
return sliceId;
|
|
352
622
|
}
|
|
353
623
|
|
|
624
|
+
const CATALOG_SNAPSHOT_INPUT_PROPERTIES = Object.freeze({
|
|
625
|
+
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
626
|
+
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
|
|
627
|
+
variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Exact Shopify variant ids returned by catalog_slice_read. Required with shopDomain.' },
|
|
628
|
+
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const CATALOG_SAVED_SLICES_INPUT_PROPERTIES = Object.freeze({
|
|
632
|
+
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
633
|
+
action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
|
|
634
|
+
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
|
|
635
|
+
label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
|
|
636
|
+
variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
|
|
637
|
+
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
function catalogConditionalBranch({ id, action, mutates, required, forbidden, example }) {
|
|
641
|
+
return Object.freeze({
|
|
642
|
+
id,
|
|
643
|
+
action,
|
|
644
|
+
mutates,
|
|
645
|
+
required: Object.freeze([...required]),
|
|
646
|
+
example: Object.freeze(example),
|
|
647
|
+
schema: Object.freeze({
|
|
648
|
+
type: 'object',
|
|
649
|
+
...(action ? { properties: { action: { const: action } } } : {}),
|
|
650
|
+
required: [...required],
|
|
651
|
+
...(forbidden.length > 0
|
|
652
|
+
? { not: { anyOf: forbidden.map((key) => ({ required: [key] })) } }
|
|
653
|
+
: {}),
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function conditionalToolInputSchema(properties, branches) {
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
type: 'object',
|
|
661
|
+
additionalProperties: false,
|
|
662
|
+
properties,
|
|
663
|
+
required: [],
|
|
664
|
+
oneOf: branches.map((branch) => branch.schema),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Each branch owns the published shape, dispatch selection, and its teaching example. Keeping
|
|
669
|
+
// these together prevents the schema from admitting a selector combination that the handler will
|
|
670
|
+
// later refuse or silently ignore.
|
|
671
|
+
const CATALOG_SNAPSHOT_BRANCHES = Object.freeze([
|
|
672
|
+
catalogConditionalBranch({
|
|
673
|
+
id: 'saved',
|
|
674
|
+
mutates: true,
|
|
675
|
+
required: ['sliceId'],
|
|
676
|
+
forbidden: ['shopDomain', 'variantIds'],
|
|
677
|
+
example: { sliceId: 'csl_saved_01234567' },
|
|
678
|
+
}),
|
|
679
|
+
catalogConditionalBranch({
|
|
680
|
+
id: 'live',
|
|
681
|
+
mutates: false,
|
|
682
|
+
required: ['shopDomain', 'variantIds'],
|
|
683
|
+
forbidden: ['sliceId'],
|
|
684
|
+
example: { shopDomain: 'merchant.myshopify.com', variantIds: ['gid://shopify/ProductVariant/1'] },
|
|
685
|
+
}),
|
|
686
|
+
]);
|
|
687
|
+
|
|
688
|
+
const CATALOG_SAVED_SLICES_BRANCHES = Object.freeze([
|
|
689
|
+
catalogConditionalBranch({
|
|
690
|
+
id: 'list',
|
|
691
|
+
action: 'list',
|
|
692
|
+
mutates: false,
|
|
693
|
+
required: ['action'],
|
|
694
|
+
forbidden: ['shopDomain', 'label', 'variantIds', 'sliceId'],
|
|
695
|
+
example: { action: 'list' },
|
|
696
|
+
}),
|
|
697
|
+
catalogConditionalBranch({
|
|
698
|
+
id: 'save',
|
|
699
|
+
action: 'save',
|
|
700
|
+
mutates: true,
|
|
701
|
+
required: ['action', 'shopDomain', 'label', 'variantIds'],
|
|
702
|
+
forbidden: ['sliceId'],
|
|
703
|
+
example: {
|
|
704
|
+
action: 'save',
|
|
705
|
+
shopDomain: 'merchant.myshopify.com',
|
|
706
|
+
label: 'Agent watchlist',
|
|
707
|
+
variantIds: ['gid://shopify/ProductVariant/1'],
|
|
708
|
+
},
|
|
709
|
+
}),
|
|
710
|
+
...['get', 'delete', 'refresh', 'changes'].map((action) => catalogConditionalBranch({
|
|
711
|
+
id: action,
|
|
712
|
+
action,
|
|
713
|
+
mutates: action === 'delete' || action === 'refresh',
|
|
714
|
+
required: ['action', 'sliceId'],
|
|
715
|
+
forbidden: ['shopDomain', 'label', 'variantIds'],
|
|
716
|
+
example: { action, sliceId: 'csl_saved_01234567' },
|
|
717
|
+
})),
|
|
718
|
+
]);
|
|
719
|
+
|
|
720
|
+
const CATALOG_SNAPSHOT_INPUT_SCHEMA = conditionalToolInputSchema(
|
|
721
|
+
CATALOG_SNAPSHOT_INPUT_PROPERTIES,
|
|
722
|
+
CATALOG_SNAPSHOT_BRANCHES,
|
|
723
|
+
);
|
|
724
|
+
const CATALOG_SAVED_SLICES_INPUT_SCHEMA = conditionalToolInputSchema(
|
|
725
|
+
CATALOG_SAVED_SLICES_INPUT_PROPERTIES,
|
|
726
|
+
CATALOG_SAVED_SLICES_BRANCHES,
|
|
727
|
+
);
|
|
728
|
+
const CONDITIONAL_TOOL_BRANCHES = new Map([
|
|
729
|
+
['catalog_slice_snapshot', CATALOG_SNAPSHOT_BRANCHES],
|
|
730
|
+
['catalog_slices_saved', CATALOG_SAVED_SLICES_BRANCHES],
|
|
731
|
+
]);
|
|
732
|
+
|
|
354
733
|
const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
|
|
355
734
|
type: 'object',
|
|
356
735
|
additionalProperties: false,
|
|
@@ -366,26 +745,6 @@ const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
|
|
|
366
745
|
required: ['schemaVersion', 'publicReceiptId', 'kind', 'revision', 'issuedAt', 'sha256', 'canonicalPath'],
|
|
367
746
|
});
|
|
368
747
|
|
|
369
|
-
const SHARE_ARTIFACT_REF_INPUT_SCHEMA = Object.freeze({
|
|
370
|
-
oneOf: [
|
|
371
|
-
PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA,
|
|
372
|
-
{ type: 'object', additionalProperties: false, properties: { attemptId: { type: 'string', minLength: 1 } }, required: ['attemptId'] },
|
|
373
|
-
{ type: 'object', additionalProperties: false, properties: { runId: { type: 'string', minLength: 1 } }, required: ['runId'] },
|
|
374
|
-
{ type: 'object', additionalProperties: false, properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$' } }, required: ['examId'] },
|
|
375
|
-
{
|
|
376
|
-
type: 'object',
|
|
377
|
-
additionalProperties: false,
|
|
378
|
-
properties: {
|
|
379
|
-
probeSetRunId: { type: 'string', minLength: 1 },
|
|
380
|
-
evaluationId: { type: 'string', minLength: 1 },
|
|
381
|
-
visibility: { type: 'string', const: 'public' },
|
|
382
|
-
},
|
|
383
|
-
required: ['probeSetRunId', 'evaluationId', 'visibility'],
|
|
384
|
-
},
|
|
385
|
-
],
|
|
386
|
-
description: 'Use the complete publicReceipt or impactPublicReceipt object returned by practice_run_report, publicReceipt from practice_run_impact, or one exact legacy reference: {attemptId}, {runId}, {examId}, or Gate {probeSetRunId,evaluationId,visibility:"public"}.',
|
|
387
|
-
});
|
|
388
|
-
|
|
389
748
|
const PRACTICE_STORE_ID_INPUT_SCHEMA = Object.freeze({
|
|
390
749
|
type: 'string',
|
|
391
750
|
minLength: 1,
|
|
@@ -455,8 +814,7 @@ const PRACTICE_RUN_EXTERNAL_AGENT_INPUT_SCHEMA = Object.freeze({
|
|
|
455
814
|
name: { type: 'string', maxLength: 80 },
|
|
456
815
|
source: { type: 'string', maxLength: 80 },
|
|
457
816
|
version: { type: 'string', maxLength: 80 },
|
|
458
|
-
commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.
|
|
459
|
-
commit: { type: 'string', maxLength: 80, description: 'Legacy alias for commitSha.' },
|
|
817
|
+
commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.' },
|
|
460
818
|
testCommand: { type: 'string', maxLength: 300 },
|
|
461
819
|
ciStatus: { type: 'string', enum: ['unknown', 'passed', 'failed'] },
|
|
462
820
|
examRole: { type: 'string', enum: ['candidate', 'negative-control'] },
|
|
@@ -493,6 +851,7 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
|
|
|
493
851
|
}, ['sku', 'threshold']),
|
|
494
852
|
practiceRunUntilConditionInputSchema('order-recorded'),
|
|
495
853
|
practiceRunUntilConditionInputSchema('return-recorded'),
|
|
854
|
+
practiceRunUntilConditionInputSchema('return-request-pending'),
|
|
496
855
|
practiceRunUntilConditionInputSchema('incoming-inventory-landed', {
|
|
497
856
|
sku: { type: 'string', minLength: 1, maxLength: 100 },
|
|
498
857
|
}, ['sku']),
|
|
@@ -502,10 +861,27 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
|
|
|
502
861
|
required: ['schemaVersion', 'maxDays', 'condition'],
|
|
503
862
|
});
|
|
504
863
|
|
|
864
|
+
const PRACTICE_RUN_ADVANCE_CURSOR_INPUT_SCHEMA = Object.freeze({
|
|
865
|
+
type: 'object',
|
|
866
|
+
additionalProperties: false,
|
|
867
|
+
description: 'Copy this object unchanged from practiceRun.advanceCursor in the latest server response.',
|
|
868
|
+
properties: {
|
|
869
|
+
expectedDay: { type: 'integer', minimum: 0 },
|
|
870
|
+
expectedCallSeq: { type: 'integer', minimum: -1 },
|
|
871
|
+
},
|
|
872
|
+
required: ['expectedDay', 'expectedCallSeq'],
|
|
873
|
+
});
|
|
874
|
+
|
|
505
875
|
const SCHEMA_VALIDATED_TOOL_NAMES = new Set([
|
|
876
|
+
// MEG-1164: admin_schema publishes two exclusive modes, so the oneOf has to be enforced locally —
|
|
877
|
+
// otherwise an ambiguous call reaches the route instead of being corrected before transport.
|
|
878
|
+
'admin_schema',
|
|
879
|
+
'plan_validate',
|
|
506
880
|
'get_connection_details',
|
|
881
|
+
'workspace_create',
|
|
507
882
|
'practice_run_start',
|
|
508
883
|
'practice_run_advance',
|
|
884
|
+
...CONDITIONAL_TOOL_BRANCHES.keys(),
|
|
509
885
|
]);
|
|
510
886
|
|
|
511
887
|
function schemaValidationError(schema, value, path = 'arguments') {
|
|
@@ -570,18 +946,32 @@ function schemaValidationError(schema, value, path = 'arguments') {
|
|
|
570
946
|
return null;
|
|
571
947
|
}
|
|
572
948
|
|
|
573
|
-
|
|
949
|
+
function conditionalToolBranch(name, args) {
|
|
950
|
+
const branches = CONDITIONAL_TOOL_BRANCHES.get(name);
|
|
951
|
+
if (!branches) return null;
|
|
952
|
+
const matches = branches.filter((branch) => schemaValidationError(branch.schema, args) === null);
|
|
953
|
+
return matches.length === 1 ? matches[0] : null;
|
|
954
|
+
}
|
|
574
955
|
|
|
575
|
-
function
|
|
576
|
-
const
|
|
577
|
-
if (!
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
956
|
+
function conditionalToolFallbackBranch(name, args) {
|
|
957
|
+
const branches = CONDITIONAL_TOOL_BRANCHES.get(name);
|
|
958
|
+
if (!branches) return null;
|
|
959
|
+
const action = typeof args?.action === 'string' ? args.action : undefined;
|
|
960
|
+
return branches.find((candidate) => candidate.action === action)
|
|
961
|
+
?? branches.find((candidate) => candidate.required.some((key) => args?.[key] !== undefined))
|
|
962
|
+
?? branches[0];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export function toolInvocationMutates(name, args) {
|
|
966
|
+
if (!CONDITIONAL_TOOL_BRANCHES.has(name)) return TOOL_PRESENTATION[name]?.readOnlyHint === false;
|
|
967
|
+
const branch = conditionalToolBranch(name, args) ?? conditionalToolFallbackBranch(name, args);
|
|
968
|
+
return branch?.mutates === true;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function conditionalToolCorrection(name, args) {
|
|
972
|
+
const branch = conditionalToolFallbackBranch(name, args);
|
|
973
|
+
if (!branch) return null;
|
|
974
|
+
return `${name}(${JSON.stringify(branch.example)})`;
|
|
585
975
|
}
|
|
586
976
|
|
|
587
977
|
function queryPath(path, values) {
|
|
@@ -597,17 +987,8 @@ function requiredWorldId(args) {
|
|
|
597
987
|
return validatedStoreId(requiredString(args, 'worldId'), 'worldId');
|
|
598
988
|
}
|
|
599
989
|
|
|
600
|
-
// MEG-256: `storeId` is the one canonical public practice-store identifier across the assistant
|
|
601
|
-
// workflow (connection discovery → run start → receipt). `worldId` names the same identifier and
|
|
602
|
-
// stays accepted as a bounded legacy alias so existing callers keep working.
|
|
603
990
|
function requiredPracticeStoreId(args) {
|
|
604
|
-
|
|
605
|
-
if (provided.length === 0) throw new Error('storeId is required (worldId is accepted as a legacy alias for the same practice-store id)');
|
|
606
|
-
const canonical = requiredString(args, provided[0]);
|
|
607
|
-
if (provided.length === 2 && requiredString(args, provided[1]) !== canonical) {
|
|
608
|
-
throw new Error('storeId and worldId name the same practice store; they were both provided with different values');
|
|
609
|
-
}
|
|
610
|
-
return validatedStoreId(canonical, provided[0]);
|
|
991
|
+
return validatedStoreId(requiredString(args, 'storeId'), 'storeId');
|
|
611
992
|
}
|
|
612
993
|
|
|
613
994
|
function requiredPracticeAttemptId(args, key = 'attemptId') {
|
|
@@ -730,49 +1111,17 @@ function receiptProjection(value) {
|
|
|
730
1111
|
const practiceRun = attempt.practiceRun ?? value?.practiceRun ?? null;
|
|
731
1112
|
const calls = Array.isArray(value?.events?.calls) ? value.events.calls : [];
|
|
732
1113
|
const actions = Array.isArray(value?.events?.actions) ? value.events.actions : [];
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (webhookCapability?.availability === 'available') {
|
|
745
|
-
return { id: assertion.id, status: assertion.status, label: assertion.label, detail: assertion.detail };
|
|
746
|
-
}
|
|
747
|
-
if (webhookCapability?.availability === 'unavailable'
|
|
748
|
-
&& webhookCapability.statusLabel === 'not available in this run mode'
|
|
749
|
-
&& typeof webhookCapability.detail === 'string'
|
|
750
|
-
&& webhookCapability.detail.trim()) {
|
|
751
|
-
return {
|
|
752
|
-
id: assertion.id,
|
|
753
|
-
status: webhookCapability.statusLabel,
|
|
754
|
-
label: assertion.label,
|
|
755
|
-
detail: webhookCapability.detail,
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
if (webhookCapability?.availability === 'unknown'
|
|
759
|
-
&& webhookCapability.statusLabel === 'availability unknown'
|
|
760
|
-
&& typeof webhookCapability.detail === 'string'
|
|
761
|
-
&& webhookCapability.detail.trim()) {
|
|
762
|
-
return {
|
|
763
|
-
id: assertion.id,
|
|
764
|
-
status: webhookCapability.statusLabel,
|
|
765
|
-
label: assertion.label,
|
|
766
|
-
detail: webhookCapability.detail,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
return {
|
|
770
|
-
id: assertion.id,
|
|
771
|
-
status: 'availability unknown',
|
|
772
|
-
label: assertion.label,
|
|
773
|
-
detail: 'Webhook assertion availability is unknown because this receipt has no capability metadata.',
|
|
774
|
-
};
|
|
775
|
-
})
|
|
1114
|
+
const assertions = Array.isArray(value?.receiptEvidence?.effectiveAssertions)
|
|
1115
|
+
? value.receiptEvidence.effectiveAssertions.slice(0, 50).map((assertion) => ({
|
|
1116
|
+
id: assertion.id,
|
|
1117
|
+
status: assertion.statusLabel,
|
|
1118
|
+
observedStatus: assertion.observedStatus,
|
|
1119
|
+
evaluationSubject: assertion.evaluationSubject,
|
|
1120
|
+
rejectionResponsibleLayers: assertion.rejectionResponsibleLayers ?? [],
|
|
1121
|
+
ownershipStatement: assertion.ownershipStatement ?? null,
|
|
1122
|
+
label: assertion.label,
|
|
1123
|
+
detail: assertion.detail,
|
|
1124
|
+
}))
|
|
776
1125
|
: [];
|
|
777
1126
|
const verdict = value?.report?.verdict && typeof value.report.verdict === 'object'
|
|
778
1127
|
? Object.fromEntries(['status', 'outcome', 'grade', 'score', 'headline', 'horizonDays']
|
|
@@ -816,6 +1165,12 @@ function receiptProjection(value) {
|
|
|
816
1165
|
// disagree with the headline sitting beside it.
|
|
817
1166
|
const footprint = m?.footprint ?? null;
|
|
818
1167
|
const defaultVerdict = value?.defaultVerdict ?? value?.report?.defaultVerdict ?? value?.attempt?.defaultVerdict;
|
|
1168
|
+
const resultSemantics = value?.resultSemantics ?? value?.report?.resultSemantics;
|
|
1169
|
+
const gate = value?.gate ?? value?.report?.gate;
|
|
1170
|
+
const rejectionAttribution = value?.rejectionAttribution ?? value?.report?.rejectionAttribution;
|
|
1171
|
+
const reportBack = value?.reportBack ?? value?.report?.reportBack;
|
|
1172
|
+
const takeHome = value?.takeHome ?? value?.report?.takeHome;
|
|
1173
|
+
const eventResolution = value?.eventResolution ?? value?.report?.eventResolution;
|
|
819
1174
|
return secretSafe({
|
|
820
1175
|
schemaVersion: value?.schemaVersion,
|
|
821
1176
|
storeIdentity: value.storeIdentity,
|
|
@@ -835,8 +1190,16 @@ function receiptProjection(value) {
|
|
|
835
1190
|
receipt: {
|
|
836
1191
|
summary: value?.receiptEvidence?.summary,
|
|
837
1192
|
assertions,
|
|
1193
|
+
...(value?.receiptEvidence?.expectedRefusals ? { expectedRefusals: value.receiptEvidence.expectedRefusals } : {}),
|
|
838
1194
|
},
|
|
1195
|
+
changes: Array.isArray(value?.changes) ? value.changes : [],
|
|
839
1196
|
...(defaultVerdict && typeof defaultVerdict === 'object' ? { defaultVerdict } : {}),
|
|
1197
|
+
...(resultSemantics && typeof resultSemantics === 'object' ? { resultSemantics } : {}),
|
|
1198
|
+
...(gate && typeof gate === 'object' ? { gate } : {}),
|
|
1199
|
+
...(rejectionAttribution && typeof rejectionAttribution === 'object' ? { rejectionAttribution } : {}),
|
|
1200
|
+
...(reportBack?.schemaVersion === 'meguro.receipt-report-back.v1' ? { reportBack } : {}),
|
|
1201
|
+
...(takeHome?.schemaVersion === 'meguro.receipt-take-home.v1' ? { takeHome } : {}),
|
|
1202
|
+
...(eventResolution?.schemaVersion === 'meguro.template-event-resolution.v1' ? { eventResolution } : {}),
|
|
840
1203
|
...(verdict ? { verdict } : {}),
|
|
841
1204
|
...(measurement ? { measurement } : {}),
|
|
842
1205
|
...(value?.publicReceipts?.run ? {
|
|
@@ -849,12 +1212,15 @@ function receiptProjection(value) {
|
|
|
849
1212
|
|
|
850
1213
|
function runsListProjection(value, lifecycle) {
|
|
851
1214
|
const runs = Array.isArray(value?.runs) ? value.runs : [];
|
|
852
|
-
|
|
1215
|
+
if (!Number.isSafeInteger(value?.total) || value.total < 0) {
|
|
1216
|
+
throw new Error('Meguro runs response did not contain a valid total');
|
|
1217
|
+
}
|
|
853
1218
|
return secretSafe({
|
|
854
1219
|
runFamily: 'Shopify dev-store history runs created by run_start.',
|
|
855
1220
|
excludes: 'Practice simulation attempts (pa-*) returned by practice_run_start are a separate family and are not included.',
|
|
856
1221
|
lifecycleFilter: lifecycle ?? 'all',
|
|
857
|
-
|
|
1222
|
+
total: value.total,
|
|
1223
|
+
runs: runs.map((run) => {
|
|
858
1224
|
const {
|
|
859
1225
|
startedAt,
|
|
860
1226
|
updatedAt,
|
|
@@ -873,7 +1239,7 @@ function runsListProjection(value, lifecycle) {
|
|
|
873
1239
|
},
|
|
874
1240
|
};
|
|
875
1241
|
}),
|
|
876
|
-
...(
|
|
1242
|
+
...(runs.length === 0 ? {
|
|
877
1243
|
emptyState: {
|
|
878
1244
|
message: `No ${lifecycle ?? 'matching'} Shopify dev-store history runs were found. This says nothing about practice simulation attempts.`,
|
|
879
1245
|
practiceAttemptRecovery: 'Call practice_runs_list to discover practice simulation attempts, then pass one exact attemptId to practice_run_report or practice_run_impact.',
|
|
@@ -940,17 +1306,20 @@ function practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedSto
|
|
|
940
1306
|
};
|
|
941
1307
|
}
|
|
942
1308
|
|
|
943
|
-
function practiceRunsProjection(value, storeId, storeSelection) {
|
|
1309
|
+
function practiceRunsProjection(value, storeId, storeSelection, workspaceId) {
|
|
944
1310
|
const attempts = Array.isArray(value?.playbacks) ? value.playbacks : [];
|
|
1311
|
+
if (!Number.isSafeInteger(value?.total) || value.total < 0) {
|
|
1312
|
+
throw new Error('Meguro practice runs response did not contain a valid total');
|
|
1313
|
+
}
|
|
945
1314
|
return secretSafe({
|
|
946
1315
|
schemaVersion: 'meguro.practice-run-list.v1',
|
|
947
1316
|
storeId,
|
|
948
1317
|
storeSelection,
|
|
1318
|
+
total: value.total,
|
|
949
1319
|
attempts: attempts
|
|
950
|
-
.filter((attempt) => attempt?.storeId === storeId || attempt?.worldId === storeId)
|
|
951
1320
|
.map((attempt) => ({
|
|
952
1321
|
attemptId: attempt.attemptId,
|
|
953
|
-
state: attempt.
|
|
1322
|
+
state: typeof attempt.practiceRun?.state === 'string' ? attempt.practiceRun.state : null,
|
|
954
1323
|
clock: {
|
|
955
1324
|
mode: attempt.clock?.mode ?? null,
|
|
956
1325
|
baselineDay: attempt.baselineDay ?? null,
|
|
@@ -959,7 +1328,10 @@ function practiceRunsProjection(value, storeId, storeSelection) {
|
|
|
959
1328
|
elapsedSimulationDays: attempt.elapsedSimulationDays ?? null,
|
|
960
1329
|
remainingSimulationDays: attempt.remainingSimulationDays ?? null,
|
|
961
1330
|
},
|
|
962
|
-
receiptRef: {
|
|
1331
|
+
receiptRef: {
|
|
1332
|
+
attemptId: attempt.attemptId,
|
|
1333
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
1334
|
+
},
|
|
963
1335
|
})),
|
|
964
1336
|
});
|
|
965
1337
|
}
|
|
@@ -975,8 +1347,10 @@ function practiceAttemptComparisonGuidanceFor(toolName, a, b) {
|
|
|
975
1347
|
type: 'text',
|
|
976
1348
|
text: JSON.stringify({
|
|
977
1349
|
code: 'practice-attempt-comparator-unavailable',
|
|
978
|
-
message: `${toolName} compares
|
|
1350
|
+
message: `${toolName} compares history-run identities that are not exposed by hosted MCP. A pa-* id names a practice simulation attempt, and Meguro has no cross-attempt comparator today.`,
|
|
979
1351
|
attemptIds,
|
|
1352
|
+
nextAction: calls.join('; '),
|
|
1353
|
+
stopCondition: `Use the listed practice receipt reads instead. Do not retry ${toolName} with pa-* practice attempt ids.`,
|
|
980
1354
|
workingMethod: {
|
|
981
1355
|
calls,
|
|
982
1356
|
receiptFacts: 'Read both immutable run receipts and impact receipts. Align like-named impact effectLines by key, preserve each receipt moneyBasis, and subtract the recorded values only after checking the comparison basis below.',
|
|
@@ -1040,14 +1414,32 @@ function practiceRunsStoreNotFoundGuidance(storeId) {
|
|
|
1040
1414
|
};
|
|
1041
1415
|
}
|
|
1042
1416
|
|
|
1417
|
+
function workspaceLifecycleIdentityGuidance(toolName, workspaceId, message) {
|
|
1418
|
+
const refusedId = typeof workspaceId === 'string' && workspaceId.trim()
|
|
1419
|
+
? JSON.stringify(workspaceId.trim())
|
|
1420
|
+
: 'the missing workspaceId';
|
|
1421
|
+
return withOperationCommitBoundary({
|
|
1422
|
+
content: [{
|
|
1423
|
+
type: 'text',
|
|
1424
|
+
text: JSON.stringify({
|
|
1425
|
+
code: 'workspace-id-invalid',
|
|
1426
|
+
message: scrubString(message),
|
|
1427
|
+
nextAction: `Call workspaces_list({}) and use an exact current workspaceId with ${toolName}.`,
|
|
1428
|
+
stopCondition: `Do not retry ${toolName} with ${refusedId}; no workspace request was sent.`,
|
|
1429
|
+
}, null, 2),
|
|
1430
|
+
}],
|
|
1431
|
+
isError: true,
|
|
1432
|
+
}, 'pre-commit');
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1043
1435
|
function legacyGateArtifactNotFoundGuidance(runId) {
|
|
1044
1436
|
return {
|
|
1045
1437
|
content: [{
|
|
1046
1438
|
type: 'text',
|
|
1047
1439
|
text: JSON.stringify({
|
|
1048
1440
|
code: 'legacy-gate-artifact-not-found',
|
|
1049
|
-
message: `No retained legacy Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
|
|
1050
|
-
nextAction: 'Call gate_verdict({}) to read the latest Receipt Gate verdict.',
|
|
1441
|
+
message: `No retained legacy Configured Receipt Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
|
|
1442
|
+
nextAction: 'Call gate_verdict({}) to read the latest Configured Receipt Gate verdict.',
|
|
1051
1443
|
stopCondition: `Do not retry ${JSON.stringify(runId)}. The legacy artifact cannot be created from a customer surface; stop after gate_verdict({}).`,
|
|
1052
1444
|
}, null, 2),
|
|
1053
1445
|
}],
|
|
@@ -1055,7 +1447,7 @@ function legacyGateArtifactNotFoundGuidance(runId) {
|
|
|
1055
1447
|
};
|
|
1056
1448
|
}
|
|
1057
1449
|
|
|
1058
|
-
function usageProjection(value) {
|
|
1450
|
+
function usageProjection(value, evaluatedRunStartEligibility) {
|
|
1059
1451
|
const totalRuns = Number(value?.totalRuns ?? 0);
|
|
1060
1452
|
const nonBillableRuns = Number(value?.nonBillableRuns ?? 0);
|
|
1061
1453
|
const activityBillableRuns = Math.max(0, totalRuns - nonBillableRuns);
|
|
@@ -1066,6 +1458,17 @@ function usageProjection(value) {
|
|
|
1066
1458
|
const activeStoreLimit = Number(meter.activeStoreLimit ?? 0);
|
|
1067
1459
|
const activeWorkspaces = Number(meter.activeWorkspaces ?? 0);
|
|
1068
1460
|
const activeWorkspaceLimit = Number(meter.activeWorkspaceLimit ?? 0);
|
|
1461
|
+
const allowance = value?.runStartAllowance;
|
|
1462
|
+
const hasDeclaredRunStartAllowance = allowance !== undefined;
|
|
1463
|
+
if (hasDeclaredRunStartAllowance && (
|
|
1464
|
+
!allowance
|
|
1465
|
+
|| typeof allowance !== 'object'
|
|
1466
|
+
|| allowance.schemaVersion !== 'meguro.run-start-allowance.v1'
|
|
1467
|
+
|| allowance.permitsChargedRun !== meter.runStartAllowed
|
|
1468
|
+
|| !['metering-allowance-available', 'metering-allowance-exhausted'].includes(allowance.reasonCode)
|
|
1469
|
+
|| allowance.meaning !== 'metering-allowance-only'
|
|
1470
|
+
|| allowance.detail !== 'Whether current tier metering allowance permits one charged simulation run; this is not overall store/root lifecycle eligibility.'
|
|
1471
|
+
)) throw new Error('Meguro usage response carried inconsistent run-start allowance semantics');
|
|
1069
1472
|
const activityCoverageComplete = value?.coverage?.complete === true;
|
|
1070
1473
|
const activityMatchesMeter = activityCoverageComplete && activityBillableRuns === consumedSimulationRuns;
|
|
1071
1474
|
const reconciliationStatus = !activityCoverageComplete
|
|
@@ -1107,17 +1510,23 @@ function usageProjection(value) {
|
|
|
1107
1510
|
activeWorkspaces,
|
|
1108
1511
|
activeWorkspaceLimit,
|
|
1109
1512
|
runStartAllowed: meter.runStartAllowed,
|
|
1513
|
+
runStartAllowedMeaning: allowance?.meaning ?? null,
|
|
1514
|
+
runStartAllowance: allowance ?? null,
|
|
1110
1515
|
enforcement: meter.enforcement,
|
|
1111
1516
|
},
|
|
1517
|
+
runStartEligibility: evaluatedRunStartEligibility ?? value?.runStartEligibility ?? null,
|
|
1112
1518
|
coverage: value?.coverage ?? null,
|
|
1519
|
+
trial: value?.trial ?? null,
|
|
1113
1520
|
});
|
|
1114
1521
|
}
|
|
1115
1522
|
|
|
1116
1523
|
function twinDiffProjection(value) {
|
|
1117
1524
|
if (!value || typeof value !== 'object') return secretSafe(value);
|
|
1118
|
-
const { generatedAt, markdown: _markdown, ...impactReceipt } = value;
|
|
1525
|
+
const { generatedAt, markdown: _markdown, pair, sources, ...impactReceipt } = value;
|
|
1119
1526
|
return secretSafe({
|
|
1120
1527
|
schemaVersion: 'meguro.mcp-twin-diff.v1',
|
|
1528
|
+
pair,
|
|
1529
|
+
sources,
|
|
1121
1530
|
runClock: {
|
|
1122
1531
|
generatedAtWallClock: generatedAt ?? null,
|
|
1123
1532
|
note: 'This is when the impact receipt was generated; it is not Store-time.',
|
|
@@ -1129,6 +1538,34 @@ function twinDiffProjection(value) {
|
|
|
1129
1538
|
});
|
|
1130
1539
|
}
|
|
1131
1540
|
|
|
1541
|
+
function historyReadErrorResult(response) {
|
|
1542
|
+
const body = response?.json && typeof response.json === 'object' ? response.json : {};
|
|
1543
|
+
const unavailable = response?.status === 404;
|
|
1544
|
+
const meaning = typeof body.message === 'string'
|
|
1545
|
+
? body.message
|
|
1546
|
+
: unavailable
|
|
1547
|
+
? 'The requested history run is missing, expired, or not owned by this workspace.'
|
|
1548
|
+
: 'The history read did not complete.';
|
|
1549
|
+
const nextStep = typeof body.nextStep === 'string'
|
|
1550
|
+
? body.nextStep
|
|
1551
|
+
: 'Call runs_list({ lifecycle: "completed" }) and use an exact available server-returned run ID.';
|
|
1552
|
+
const stopCondition = typeof body.stopCondition === 'string'
|
|
1553
|
+
? body.stopCondition
|
|
1554
|
+
: 'Stop if runs_list does not return the requested run or both roles of one available twinPair.';
|
|
1555
|
+
const stable = {
|
|
1556
|
+
httpStatus: response?.status,
|
|
1557
|
+
code: typeof body.code === 'string'
|
|
1558
|
+
? body.code
|
|
1559
|
+
: unavailable ? 'history-run-unavailable' : 'history-read-failed',
|
|
1560
|
+
meaning,
|
|
1561
|
+
nextStep,
|
|
1562
|
+
stopCondition,
|
|
1563
|
+
teaching: { meaning, nextStep, stopCondition },
|
|
1564
|
+
...(body.retention ? { retention: body.retention } : {}),
|
|
1565
|
+
};
|
|
1566
|
+
return { content: [{ type: 'text', text: JSON.stringify(secretSafe(stable), null, 2) }], isError: true };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1132
1569
|
const SHOPIFY_EXAM_ID = /^pex-[0-9a-f]{24}$/u;
|
|
1133
1570
|
const SHOPIFY_DEV_DOMAIN = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/u;
|
|
1134
1571
|
const SHOPIFY_EXAM_ACTIVE_STATUSES = new Set(['confirmed', 'materializing', 'replaying', 'comparing']);
|
|
@@ -1143,6 +1580,7 @@ const SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE = Object.freeze({
|
|
|
1143
1580
|
'temporal-clock-context-mismatch': 'Use the target practice store clock and rerun the bounded Exam source.',
|
|
1144
1581
|
'temporal-clock-revision-mismatch': 'Capture one bounded burst at one Store-time coordinate; after advancing time, start a new practice run.',
|
|
1145
1582
|
'interleaved-simulation-state-change': 'Rerun one bounded agent burst without stepping the store between captured calls.',
|
|
1583
|
+
'target-preflight-unavailable': 'Use an exact connected Shopify development-store domain available to this workspace.',
|
|
1146
1584
|
});
|
|
1147
1585
|
|
|
1148
1586
|
function requiredExamId(args) {
|
|
@@ -1163,10 +1601,15 @@ function requiredShopifyDevDomain(args) {
|
|
|
1163
1601
|
|
|
1164
1602
|
function examNextAction(exam, receipt) {
|
|
1165
1603
|
const status = String(exam?.status ?? '');
|
|
1166
|
-
if (
|
|
1167
|
-
return
|
|
1168
|
-
|
|
1169
|
-
|
|
1604
|
+
if (status === 'cleaned' || status === 'failed') {
|
|
1605
|
+
return { tool: 'exam_report', args: { examId: exam.examId }, reason: `exam-${status}` };
|
|
1606
|
+
}
|
|
1607
|
+
if (status === 'completed') {
|
|
1608
|
+
return {
|
|
1609
|
+
tool: 'exam_start',
|
|
1610
|
+
args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain },
|
|
1611
|
+
reason: 'cleanup-required',
|
|
1612
|
+
};
|
|
1170
1613
|
}
|
|
1171
1614
|
if (status === 'blocked') {
|
|
1172
1615
|
if (receipt?.privateAvailable === true && receipt?.publicAvailable === true) {
|
|
@@ -1175,7 +1618,11 @@ function examNextAction(exam, receipt) {
|
|
|
1175
1618
|
return { tool: 'exam_preflight', args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain }, reason: 'preview-blocked' };
|
|
1176
1619
|
}
|
|
1177
1620
|
if (status === 'cleaning') {
|
|
1178
|
-
return {
|
|
1621
|
+
return {
|
|
1622
|
+
tool: 'exam_start',
|
|
1623
|
+
args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain },
|
|
1624
|
+
reason: 'cleanup-checkpoint-complete',
|
|
1625
|
+
};
|
|
1179
1626
|
}
|
|
1180
1627
|
return {
|
|
1181
1628
|
tool: 'exam_start',
|
|
@@ -1247,6 +1694,7 @@ export function createTools(config) {
|
|
|
1247
1694
|
const headers = { accept: 'application/json' };
|
|
1248
1695
|
const needsControlCredential = Boolean(options.auth || method !== 'GET');
|
|
1249
1696
|
if (needsControlCredential) Object.assign(headers, controlAuthHeaders());
|
|
1697
|
+
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1250
1698
|
if (method !== 'GET') {
|
|
1251
1699
|
headers['content-type'] = 'application/json';
|
|
1252
1700
|
}
|
|
@@ -1264,7 +1712,11 @@ export function createTools(config) {
|
|
|
1264
1712
|
const authenticationError = authenticationErrorResult(response.status, json);
|
|
1265
1713
|
if (authenticationError) throw new ToolResultError(authenticationError);
|
|
1266
1714
|
const message = json.errors?.[0]?.message ?? json.error ?? text.slice(0, 300);
|
|
1267
|
-
|
|
1715
|
+
const failure = new Error(`${response.status} ${path}: ${message}`);
|
|
1716
|
+
// MEG-1054: the control plane is the authority on what a failed mutation did to committed
|
|
1717
|
+
// state. Carry the classification it declared instead of re-deriving one at this boundary.
|
|
1718
|
+
if (typeof json.operationCommitBoundary === 'string') failure.operationCommitBoundary = json.operationCommitBoundary;
|
|
1719
|
+
throw failure;
|
|
1268
1720
|
}
|
|
1269
1721
|
return json;
|
|
1270
1722
|
}
|
|
@@ -1275,7 +1727,10 @@ export function createTools(config) {
|
|
|
1275
1727
|
const headers = { accept: 'application/json', ...controlAuthHeaders() };
|
|
1276
1728
|
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1277
1729
|
if (method !== 'GET') headers['content-type'] = 'application/json';
|
|
1278
|
-
const
|
|
1730
|
+
const bust = method === 'GET' && options.cacheBustRead === true
|
|
1731
|
+
? `${path.includes('?') ? '&' : '?'}_=${Date.now()}`
|
|
1732
|
+
: '';
|
|
1733
|
+
const response = await fetchImpl(`${apiBaseUrl}${path}${bust}`, {
|
|
1279
1734
|
method,
|
|
1280
1735
|
headers,
|
|
1281
1736
|
...(body ? { body: JSON.stringify(body) } : {}),
|
|
@@ -1326,12 +1781,47 @@ export function createTools(config) {
|
|
|
1326
1781
|
return { schemaVersion: 'meguro.support-code.v1', buildSha, requestIds: [safeRequestId], occurredAt, route: path, line };
|
|
1327
1782
|
}
|
|
1328
1783
|
|
|
1329
|
-
|
|
1784
|
+
function supportRouteForFleetFailure(toolName, path) {
|
|
1785
|
+
if (toolName === 'catalog_slice_read') return '/stores/{shopDomain}/catalog-slice';
|
|
1786
|
+
if (toolName === 'catalog_slice_snapshot' && /^\/stores\/[^/]+\/catalog-slice\/snapshot(?:\?|$)/u.test(path)) {
|
|
1787
|
+
return '/stores/{shopDomain}/catalog-slice/snapshot';
|
|
1788
|
+
}
|
|
1789
|
+
if (toolName === 'catalog_slices_saved' && /^\/stores\/[^/]+\/catalog-slices(?:\?|$)/u.test(path)) {
|
|
1790
|
+
return '/stores/{shopDomain}/catalog-slices';
|
|
1791
|
+
}
|
|
1792
|
+
return path;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function catalogConnectionTeaching(toolName, path, allowedShopDomains, hasAllowedShopDomainAuthority) {
|
|
1796
|
+
if (!hasAllowedShopDomainAuthority) return null;
|
|
1797
|
+
const liveCatalogStoreAction = toolName === 'catalog_slice_read'
|
|
1798
|
+
|| (toolName === 'catalog_slice_snapshot' && /^\/stores\/[^/]+\/catalog-slice\/snapshot(?:\?|$)/u.test(path))
|
|
1799
|
+
|| (toolName === 'catalog_slices_saved' && /^\/stores\/[^/]+\/catalog-slices(?:\?|$)/u.test(path));
|
|
1800
|
+
if (!liveCatalogStoreAction) return null;
|
|
1801
|
+
if (!allowedShopDomains.length) {
|
|
1802
|
+
return {
|
|
1803
|
+
meaning: 'No catalog-readable Shopify development-store domain is connected for this workspace, so the requested operation was not applied.',
|
|
1804
|
+
nextStep: `The allowedShopDomains field below is empty. Stop: no catalog-readable Shopify development-store domain is connected; connect one in Console before trying ${toolName} again.`,
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
return {
|
|
1808
|
+
meaning: 'The requested Shopify development-store domain is not connected or allowed for this workspace, so the requested operation was not applied.',
|
|
1809
|
+
nextStep: toolName === 'catalog_slice_read'
|
|
1810
|
+
? `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call catalog_slice_read({ shopDomain: ${JSON.stringify(allowedShopDomains[0])} }) with one exact value from that field. Do not retry the refused domain.`
|
|
1811
|
+
: `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call ${toolName} again with shopDomain: ${JSON.stringify(allowedShopDomains[0])} and the same valid non-domain arguments. Do not retry the refused domain.`,
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
async function fleetErrorResult(toolName, path, status, value, requestId, invocationMutates) {
|
|
1330
1816
|
const body = value && typeof value === 'object' ? value : {};
|
|
1817
|
+
const declaredBoundary = ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
|
|
1818
|
+
? body.operationCommitBoundary
|
|
1819
|
+
: null;
|
|
1331
1820
|
const sourceErrors = Array.isArray(body.errors) ? body.errors.slice(0, 5) : [];
|
|
1332
1821
|
const allowedShopDomains = Array.isArray(body.allowed)
|
|
1333
1822
|
? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
|
|
1334
1823
|
: [];
|
|
1824
|
+
const catalogConnection = catalogConnectionTeaching(toolName, path, allowedShopDomains, Array.isArray(body.allowed));
|
|
1335
1825
|
const workspaceLimit = body.code === 'active_workspace_limit_reached';
|
|
1336
1826
|
const rawError = typeof body.error === 'string' ? body.error : '';
|
|
1337
1827
|
const routeCode = typeof body.code === 'string'
|
|
@@ -1343,31 +1833,80 @@ export function createTools(config) {
|
|
|
1343
1833
|
const claimCodeError = toolName === 'store_claim_by_code'
|
|
1344
1834
|
&& ['invalid-claim-code', 'claim-code-unavailable', 'claim-code-busy'].includes(routeCode);
|
|
1345
1835
|
const sourceErrorText = sourceErrors.map((error) => String(error?.message ?? '')).join(' ');
|
|
1346
|
-
const
|
|
1836
|
+
const settledFleetMutation = new Set([
|
|
1837
|
+
'store_create',
|
|
1838
|
+
'store_delete',
|
|
1839
|
+
'workspace_create',
|
|
1840
|
+
'workspace_archive',
|
|
1841
|
+
'workspace_unarchive',
|
|
1842
|
+
'store_claim_by_code',
|
|
1843
|
+
]);
|
|
1844
|
+
const settledPreCommitRefusal = status >= 400 && status < 500
|
|
1845
|
+
&& settledFleetMutation.has(toolName);
|
|
1846
|
+
// These route/code pairs are authority-bearing pre-write refusals: template resolution,
|
|
1847
|
+
// target lookup, and claim-code resolution all finish before their write-capable phase. A
|
|
1848
|
+
// transport failure or any unrecognized response remains undeclared and reaches the hosted
|
|
1849
|
+
// resource's single `unknown` fallback.
|
|
1850
|
+
const authoritativeBoundary = declaredBoundary ?? (settledPreCommitRefusal ? 'pre-commit' : null);
|
|
1851
|
+
const effectiveBoundary = authoritativeBoundary ?? (invocationMutates ? 'unknown' : null);
|
|
1852
|
+
const hasInvocationBoundaryAuthority = invocationMutates !== undefined || authoritativeBoundary !== null;
|
|
1853
|
+
const operationMeaning = effectiveBoundary === 'pre-commit'
|
|
1854
|
+
? 'The requested fleet operation was not applied.'
|
|
1855
|
+
: effectiveBoundary === 'committed'
|
|
1856
|
+
? 'The requested fleet operation applied to committed state, but the response did not complete.'
|
|
1857
|
+
: effectiveBoundary === 'unknown'
|
|
1858
|
+
? 'The requested fleet operation did not complete with a confirmed commit outcome.'
|
|
1859
|
+
: invocationMutates === false
|
|
1860
|
+
? 'The requested fleet read did not complete.'
|
|
1861
|
+
: 'The requested fleet operation was not applied.';
|
|
1862
|
+
const workspaceLifecycleTool = toolName === 'workspace_archive' || toolName === 'workspace_unarchive';
|
|
1863
|
+
const resourceTeaching = toolName === 'workspace_archive' && status === 400 && routeCode === 'default-workspace'
|
|
1864
|
+
? {
|
|
1865
|
+
meaning: 'The default workspace cannot be archived. The requested lifecycle operation was not applied.',
|
|
1866
|
+
nextStep: 'Call workspaces_list({}), select an active workspace with isDefault: false, then call workspace_archive({ workspaceId: "<exact active non-default workspaceId>" }). If no active non-default workspace exists, stop and do not retry the default workspaceId.',
|
|
1867
|
+
}
|
|
1868
|
+
: workspaceLifecycleTool && status === 409 && routeCode === 'workspace-deleting'
|
|
1869
|
+
? {
|
|
1870
|
+
meaning: 'The workspace lifecycle transition is already underway. Do not retry the mutation.',
|
|
1871
|
+
nextStep: `Wait for the lifecycle transition to finish, then call workspaces_list({}) to read the terminal workspace state. Stop and do not retry ${toolName} while the workspace is deleting.`,
|
|
1872
|
+
}
|
|
1873
|
+
: workspaceLifecycleTool && status === 404
|
|
1347
1874
|
? {
|
|
1348
1875
|
meaning: 'The requested workspace is not available in this account. Workspace lifecycle tools require a workspaceId; a storeId names a different resource and is never a substitute.',
|
|
1349
1876
|
nextStep: `Call workspaces_list({}), choose an exact workspaceId from the returned account workspaces, then retry ${toolName}({ workspaceId: "<exact workspaceId>" }). If the intended workspace is absent, stop and do not retry.`,
|
|
1350
1877
|
}
|
|
1878
|
+
: toolName === 'template_get' && status === 404
|
|
1879
|
+
? {
|
|
1880
|
+
// MEG-1058: templates_list is the complete catalog, so an unknown key is settled, not
|
|
1881
|
+
// transient. Teach the discovery step and stop rather than inviting a retry.
|
|
1882
|
+
meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
|
|
1883
|
+
nextStep: 'Call templates_list({}), choose an exact key from the returned storeTemplates, then call template_get({ templateKey: "<exact key>" }). If the intended template is absent, stop and do not retry the same key.',
|
|
1884
|
+
}
|
|
1351
1885
|
: toolName === 'store_create' && status === 400 && /template/iu.test(`${rawError} ${routeMessage ?? ''} ${sourceErrorText}`)
|
|
1352
1886
|
? {
|
|
1353
1887
|
meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
|
|
1354
1888
|
nextStep: 'Call templates_list({}), choose an exact key from the returned storeTemplates, then call store_create({ templateKey: "<exact key>" }). If the catalog is empty or the intended template is absent, stop and do not retry.',
|
|
1355
1889
|
}
|
|
1356
|
-
|
|
1890
|
+
: toolName === 'catalog_slice_snapshot' && status === 404
|
|
1891
|
+
&& /^\/catalog-slices\/[^/]+\/snapshot(?:\?|$)/u.test(path)
|
|
1892
|
+
? {
|
|
1893
|
+
meaning: 'The requested saved catalog slice is not in the current workspace.',
|
|
1894
|
+
nextStep: 'Call catalog_slices_saved({ action: "list" }), choose an exact sliceId from that result, and retry only the failed action. If the intended slice is absent, stop and do not retry the unavailable sliceId.',
|
|
1895
|
+
}
|
|
1896
|
+
: toolName === 'catalog_slices_saved' && status === 404
|
|
1897
|
+
? {
|
|
1898
|
+
meaning: 'The requested saved catalog slice is not in the current workspace.',
|
|
1899
|
+
nextStep: 'Call catalog_slices_saved({ action: "list" }), choose an exact sliceId from that result, and retry only the failed action. If the intended slice is absent, stop and do not retry the unavailable sliceId.',
|
|
1900
|
+
}
|
|
1901
|
+
: null;
|
|
1357
1902
|
const defaultNextStep = resourceTeaching
|
|
1358
1903
|
? resourceTeaching.nextStep
|
|
1359
1904
|
: claimCodeError
|
|
1360
1905
|
? routeCode === 'claim-code-busy'
|
|
1361
1906
|
? 'Wait for the current connection update to finish, then reopen Meguro in Shopify Admin before retrying once.'
|
|
1362
1907
|
: 'Reopen Meguro in Shopify Admin and use the current one-time claim code shown for the intended development store.'
|
|
1363
|
-
: toolName === '
|
|
1364
|
-
|
|
1365
|
-
: toolName.startsWith('share_') || toolName === 'shares_list'
|
|
1366
|
-
? 'Call shares_list or share_status, inspect the current private/outward lifecycle state, and retry only the permitted transition.'
|
|
1367
|
-
: toolName === 'catalog_slice_read'
|
|
1368
|
-
? allowedShopDomains.length
|
|
1369
|
-
? `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call catalog_slice_read({ shopDomain: ${JSON.stringify(allowedShopDomains[0])} }) with one exact value from that field. Do not retry the refused domain.`
|
|
1370
|
-
: 'The allowedShopDomains field below is empty. Stop: no catalog-readable Shopify development-store domain is connected; connect one in Console before trying catalog_slice_read again.'
|
|
1908
|
+
: toolName === 'catalog_slice_read'
|
|
1909
|
+
? catalogConnection?.nextStep ?? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
|
|
1371
1910
|
: toolName.startsWith('catalog_')
|
|
1372
1911
|
? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
|
|
1373
1912
|
: toolName === 'store_claim'
|
|
@@ -1375,47 +1914,51 @@ export function createTools(config) {
|
|
|
1375
1914
|
: 'Call stores_list, inspect the current fleet, and retry with a current store id.';
|
|
1376
1915
|
const errors = sourceErrors.length ? sourceErrors.map((error) => ({
|
|
1377
1916
|
message: scrubString(error?.message ?? 'Meguro fleet request failed.'),
|
|
1378
|
-
meaning: scrubString(resourceTeaching?.meaning
|
|
1379
|
-
|
|
1917
|
+
meaning: scrubString(resourceTeaching?.meaning
|
|
1918
|
+
?? catalogConnection?.meaning
|
|
1919
|
+
?? (hasInvocationBoundaryAuthority ? operationMeaning : error?.meaning ?? operationMeaning)),
|
|
1920
|
+
nextStep: scrubString(resourceTeaching?.nextStep ?? catalogConnection?.nextStep ?? error?.nextStep ?? defaultNextStep),
|
|
1380
1921
|
})) : [{
|
|
1381
1922
|
message: scrubString(routeMessage ?? (typeof value === 'string' ? value : 'Meguro fleet request failed.')),
|
|
1382
1923
|
meaning: resourceTeaching
|
|
1383
1924
|
? resourceTeaching.meaning
|
|
1925
|
+
: catalogConnection
|
|
1926
|
+
? catalogConnection.meaning
|
|
1384
1927
|
: workspaceLimit
|
|
1385
1928
|
? 'The account is at its active-workspace limit, so the requested workspace operation was not applied.'
|
|
1386
1929
|
: claimCodeError
|
|
1387
1930
|
? 'The one-time Shopify development-store claim was not applied.'
|
|
1388
|
-
:
|
|
1931
|
+
: operationMeaning,
|
|
1389
1932
|
nextStep: resourceTeaching
|
|
1390
1933
|
? resourceTeaching.nextStep
|
|
1934
|
+
: catalogConnection
|
|
1935
|
+
? catalogConnection.nextStep
|
|
1391
1936
|
: workspaceLimit
|
|
1392
|
-
?
|
|
1937
|
+
? String(body.sameTierAction?.label ?? routeMessage ?? defaultNextStep)
|
|
1393
1938
|
: defaultNextStep,
|
|
1394
1939
|
}];
|
|
1395
1940
|
const stable = secretSafe({
|
|
1396
1941
|
tool: toolName,
|
|
1397
1942
|
httpStatus: status,
|
|
1398
1943
|
...(routeCode ? { code: routeCode } : {}),
|
|
1399
|
-
...(
|
|
1400
|
-
...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
|
|
1944
|
+
...enforcementProjection(body),
|
|
1401
1945
|
...(Number.isFinite(body.activeStores) ? { activeStores: body.activeStores } : {}),
|
|
1402
1946
|
...(Number.isFinite(body.activeStoreLimit) ? { activeStoreLimit: body.activeStoreLimit } : {}),
|
|
1403
1947
|
...(Number.isFinite(body.activeWorkspaces) ? { activeWorkspaces: body.activeWorkspaces } : {}),
|
|
1404
1948
|
...(Number.isFinite(body.activeWorkspaceLimit) ? { activeWorkspaceLimit: body.activeWorkspaceLimit } : {}),
|
|
1405
|
-
...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
|
|
1406
1949
|
errors,
|
|
1407
|
-
...(
|
|
1950
|
+
...(catalogConnection ? { allowedShopDomains } : {}),
|
|
1408
1951
|
...(Array.isArray(body.fleet) ? { fleet: body.fleet } : {}),
|
|
1409
|
-
supportCode: await fleetSupportCode(path, requestId),
|
|
1952
|
+
supportCode: await fleetSupportCode(supportRouteForFleetFailure(toolName, path), requestId),
|
|
1410
1953
|
});
|
|
1411
|
-
|
|
1954
|
+
const result = { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
|
|
1955
|
+
return authoritativeBoundary ? withOperationCommitBoundary(result, authoritativeBoundary) : result;
|
|
1412
1956
|
}
|
|
1413
1957
|
|
|
1414
1958
|
async function historyRunTeachingErrorResult(toolName, path, response, requestedId) {
|
|
1415
1959
|
const body = response.json && typeof response.json === 'object' ? response.json : {};
|
|
1416
1960
|
const rawMessage = String(body.errors?.[0]?.message ?? body.error ?? body.message ?? 'Meguro history-run request failed.');
|
|
1417
1961
|
if (toolName === 'run_start' && response.status === 400 && /not allowed|not connected/iu.test(rawMessage)) {
|
|
1418
|
-
const requestedDomain = String(body.requestedShopDomain ?? requestedId ?? '').trim().toLowerCase();
|
|
1419
1962
|
const allowedShopDomains = Array.isArray(body.allowed)
|
|
1420
1963
|
? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
|
|
1421
1964
|
: [];
|
|
@@ -1428,10 +1971,10 @@ export function createTools(config) {
|
|
|
1428
1971
|
error: { message: rawMessage },
|
|
1429
1972
|
allowedShopDomains,
|
|
1430
1973
|
teaching: {
|
|
1431
|
-
meaning:
|
|
1974
|
+
meaning: 'The requested shop domain is not connected or allowed for this account, so no Shopify dev-store history run was started.',
|
|
1432
1975
|
nextStep,
|
|
1433
1976
|
stopCondition: allowedShopDomains.length
|
|
1434
|
-
?
|
|
1977
|
+
? 'Do not retry the refused domain; use only an exact value from allowedShopDomains.'
|
|
1435
1978
|
: 'Stop until Console exposes a connected Shopify development-store domain.',
|
|
1436
1979
|
},
|
|
1437
1980
|
supportCode: await fleetSupportCode(path, response.requestId),
|
|
@@ -1459,14 +2002,22 @@ export function createTools(config) {
|
|
|
1459
2002
|
const response = await apiRaw(method, path, body, options);
|
|
1460
2003
|
if (!response.ok) {
|
|
1461
2004
|
const authenticationError = authenticationErrorResult(response.status, response.json);
|
|
1462
|
-
return { error: authenticationError ?? await fleetErrorResult(
|
|
2005
|
+
return { error: authenticationError ?? await fleetErrorResult(
|
|
2006
|
+
toolName,
|
|
2007
|
+
path,
|
|
2008
|
+
response.status,
|
|
2009
|
+
response.json,
|
|
2010
|
+
response.requestId,
|
|
2011
|
+
options.invocationMutates,
|
|
2012
|
+
) };
|
|
1463
2013
|
}
|
|
1464
2014
|
return { value: response.json };
|
|
1465
2015
|
}
|
|
1466
2016
|
|
|
1467
|
-
async function practiceApi(method, path, body) {
|
|
2017
|
+
async function practiceApi(method, path, body, options = {}) {
|
|
1468
2018
|
if (!practiceApiToken) throw new Error('MEGURO_API_TOKEN is required for practice-run tools');
|
|
1469
2019
|
const headers = { accept: 'application/json', authorization: `Bearer ${practiceApiToken}` };
|
|
2020
|
+
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1470
2021
|
if (method !== 'GET') headers['content-type'] = 'application/json';
|
|
1471
2022
|
let response;
|
|
1472
2023
|
try {
|
|
@@ -1486,23 +2037,20 @@ export function createTools(config) {
|
|
|
1486
2037
|
return { ok: response.ok, status: response.status, json, retryAfterSeconds };
|
|
1487
2038
|
}
|
|
1488
2039
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
?
|
|
1494
|
-
|
|
1495
|
-
if (!response.ok || !eligibility || typeof eligibility !== 'object') {
|
|
2040
|
+
function examEligibility(source) {
|
|
2041
|
+
if (source.code !== 'exam-ineligible') return null;
|
|
2042
|
+
const codes = Array.isArray(source.blockerCodes)
|
|
2043
|
+
? source.blockerCodes.map(String)
|
|
2044
|
+
: typeof source.blockerCode === 'string' ? [source.blockerCode] : [];
|
|
2045
|
+
if (source.retryable === true && codes.length === 0) {
|
|
1496
2046
|
return {
|
|
1497
2047
|
state: 'undetermined',
|
|
1498
|
-
codes
|
|
2048
|
+
codes,
|
|
1499
2049
|
retryable: true,
|
|
1500
2050
|
nextStep: 'Eligibility evidence is undetermined, not negative. Retry preflight; if the evidence read remains unavailable, use the support code.',
|
|
1501
2051
|
};
|
|
1502
2052
|
}
|
|
1503
|
-
const codes = Array.isArray(eligibility.codes) ? eligibility.codes.map(String) : [];
|
|
1504
2053
|
const steps = [...new Set(codes.map((code) => SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE[code]).filter(Boolean))];
|
|
1505
|
-
const storeId = String(attempt?.storeId ?? attempt?.worldId ?? '').trim();
|
|
1506
2054
|
const mustRecapture = codes.some((code) => [
|
|
1507
2055
|
'tape-missing',
|
|
1508
2056
|
'tape-total-over-limit',
|
|
@@ -1514,11 +2062,9 @@ export function createTools(config) {
|
|
|
1514
2062
|
'temporal-clock-revision-mismatch',
|
|
1515
2063
|
'interleaved-simulation-state-change',
|
|
1516
2064
|
].includes(code));
|
|
1517
|
-
const recaptureStep =
|
|
2065
|
+
const recaptureStep = 'This immutable attempt is ineligible and cannot be repaired. Do not retry this immutable attempt. Call practice_run_start for the same practice store to create a new simulation attempt; run one bounded capture through the current Connect or package-free CI path using Shopify Admin API 2026-04 at one Store-time coordinate without advancing the store between captured calls; finish it; then call exam_preflight with the new attemptId and exact connected development-store domain.';
|
|
1518
2066
|
return {
|
|
1519
|
-
state:
|
|
1520
|
-
schemaVersion: eligibility.schemaVersion,
|
|
1521
|
-
replayDisposition: eligibility.replayDisposition,
|
|
2067
|
+
state: 'blocked',
|
|
1522
2068
|
codes,
|
|
1523
2069
|
retryable: false,
|
|
1524
2070
|
nextStep: mustRecapture
|
|
@@ -1533,20 +2079,23 @@ export function createTools(config) {
|
|
|
1533
2079
|
const source = response.json?.examError && typeof response.json.examError === 'object'
|
|
1534
2080
|
? response.json.examError
|
|
1535
2081
|
: { code: 'exam-request-failed', message: 'The Shopify Exam request failed safely.', retryable: response.status >= 500 };
|
|
1536
|
-
const eligibility = source
|
|
1537
|
-
|
|
1538
|
-
: null;
|
|
1539
|
-
const retryable = eligibility?.state === 'undetermined'
|
|
1540
|
-
? true
|
|
1541
|
-
: source.retryable === true;
|
|
2082
|
+
const eligibility = examEligibility(source);
|
|
2083
|
+
const retryable = source.retryable === true;
|
|
1542
2084
|
const startPairNotFound = toolName === 'exam_start' && source.code === 'not_found';
|
|
1543
|
-
const
|
|
2085
|
+
const preflightPairNotFound = toolName === 'exam_preflight' && source.code === 'not_found';
|
|
2086
|
+
const examIdentityNotFound = (toolName === 'exam_status' || toolName === 'exam_report')
|
|
2087
|
+
&& source.code === 'not_found';
|
|
2088
|
+
const nextStep = startPairNotFound || preflightPairNotFound
|
|
1544
2089
|
? 'Use a completed attemptId returned by practice_run_start and the exact connected .myshopify.com domain shown in Console. Call exam_preflight({ attemptId: "<completed attemptId>", shopDomain: "<exact connected .myshopify.com domain>" }); only when it reports eligible, call exam_start({ attemptId: "<same completed attemptId>", shopDomain: "<same exact connected .myshopify.com domain>" }).'
|
|
2090
|
+
: examIdentityNotFound
|
|
2091
|
+
? 'Call practice_runs_list({}), choose a completed attemptId, call exam_preflight with that attemptId and an exact connected .myshopify.com domain, then call exam_start. Use only the examId returned by exam_start with this tool.'
|
|
1545
2092
|
: eligibility?.nextStep
|
|
1546
2093
|
?? (retryable
|
|
1547
|
-
?
|
|
2094
|
+
? toolName === 'exam_preflight'
|
|
2095
|
+
? 'Retry exam_preflight with the same attemptId and shopDomain after the transient condition clears. Preflight creates no Exam identity; do not call exam_status unless exam_start later returns an examId.'
|
|
2096
|
+
: 'Read exam_status before retrying so an ambiguous delivery is never replayed blindly.'
|
|
1548
2097
|
: 'Correct the stated Exam requirement, then retry only the failed bounded step.');
|
|
1549
|
-
|
|
2098
|
+
const result = {
|
|
1550
2099
|
content: [{
|
|
1551
2100
|
type: 'text',
|
|
1552
2101
|
text: JSON.stringify(secretSafe({
|
|
@@ -1560,19 +2109,28 @@ export function createTools(config) {
|
|
|
1560
2109
|
codes: eligibility.codes,
|
|
1561
2110
|
} } : {}),
|
|
1562
2111
|
teaching: {
|
|
1563
|
-
meaning: startPairNotFound
|
|
2112
|
+
meaning: startPairNotFound || preflightPairNotFound
|
|
1564
2113
|
? 'This tenant-safe not-found refusal means the requested practice attempt and connected development-store domain pair is unavailable to this account; Meguro does not reveal which member of the pair was absent.'
|
|
2114
|
+
: examIdentityNotFound
|
|
2115
|
+
? 'No Shopify Exam with this examId is available to this account. Exam identities are created only by exam_start.'
|
|
1565
2116
|
: source.code === 'exam-ineligible'
|
|
1566
2117
|
? 'Shopify Exam accepts only a completed, graded, immutable captured attempt whose replay evidence is current and supported.'
|
|
1567
2118
|
: 'The requested bounded Shopify Exam step was not assumed complete.',
|
|
1568
2119
|
nextStep,
|
|
1569
|
-
...(startPairNotFound
|
|
2120
|
+
...(startPairNotFound || preflightPairNotFound
|
|
2121
|
+
? { stopCondition: `If a completed attemptId or exact connected domain is unavailable, stop. Do not retry ${toolName} with the refused pair.` }
|
|
2122
|
+
: examIdentityNotFound
|
|
2123
|
+
? { stopCondition: `If exam_start did not return this examId, stop. Do not retry ${toolName} with the unavailable examId.` }
|
|
2124
|
+
: {}),
|
|
1570
2125
|
},
|
|
1571
2126
|
supportCode: await fleetSupportCode(path, response.requestId),
|
|
1572
2127
|
}), null, 2),
|
|
1573
2128
|
}],
|
|
1574
2129
|
isError: true,
|
|
1575
2130
|
};
|
|
2131
|
+
return startPairNotFound || preflightPairNotFound
|
|
2132
|
+
? withOperationCommitBoundary(result, 'pre-commit')
|
|
2133
|
+
: result;
|
|
1576
2134
|
}
|
|
1577
2135
|
|
|
1578
2136
|
async function examRequest(toolName, method, path, body, attemptId) {
|
|
@@ -1655,8 +2213,13 @@ export function createTools(config) {
|
|
|
1655
2213
|
description: DOCUMENTATION_TOOL_CONTRACT.topicDescription,
|
|
1656
2214
|
},
|
|
1657
2215
|
version: {
|
|
1658
|
-
|
|
1659
|
-
|
|
2216
|
+
// MEG-1059: an exact published integer, or the "latest" sentinel. The historical enum is
|
|
2217
|
+
// withdrawn — it grew with every mint and told a caller nothing about which topic
|
|
2218
|
+
// publishes which version.
|
|
2219
|
+
anyOf: [
|
|
2220
|
+
{ type: 'integer', minimum: 1 },
|
|
2221
|
+
{ type: 'string', enum: ['latest'] },
|
|
2222
|
+
],
|
|
1660
2223
|
description: DOCUMENTATION_TOOL_CONTRACT.versionDescription,
|
|
1661
2224
|
},
|
|
1662
2225
|
},
|
|
@@ -1665,7 +2228,7 @@ export function createTools(config) {
|
|
|
1665
2228
|
},
|
|
1666
2229
|
{
|
|
1667
2230
|
name: 'templates_list',
|
|
1668
|
-
description: 'List the
|
|
2231
|
+
description: 'List the complete ordered, public-safe practice-store template catalog available to store_create; it is not filtered by stores already in the fleet. Each row carries only what a cold agent needs to choose — key, label, category, recommended, bestFor and recommendedRunDays. Call template_get({ templateKey }) for one template\'s complete record: supported reads and writes, operation contracts, unsupported contracts, capability boundaries, modeled evidence and physics, and rubric fields. starterRecommendation names every server-recommended template and provides a server-owned starterPlans entry for every selectable template. Each plan states the authenticated account tier, ideal horizon, exact first practice_run_start clock, run-start availability, and continuation. While the one account onboarding grant is available, every template is recommendable and its plan carries that authorization; afterward the same plans apply ordinary tier policy. If no executable recommendation is available, the catalog remains complete and starterRecommendation.recommendationUnavailable explains why. The matching recommended boolean follows the server declaration, and recommendedRunDays remains each template\'s ideal simulation length. The response excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
|
|
1669
2232
|
inputSchema: {
|
|
1670
2233
|
type: 'object',
|
|
1671
2234
|
additionalProperties: false,
|
|
@@ -1675,6 +2238,19 @@ export function createTools(config) {
|
|
|
1675
2238
|
required: [],
|
|
1676
2239
|
},
|
|
1677
2240
|
},
|
|
2241
|
+
{
|
|
2242
|
+
name: 'template_get',
|
|
2243
|
+
description: 'Read the complete public-safe record for exactly one practice-store template: the compact catalog fields plus what the agent sees, timing, supported reads and writes, modeled evidence and physics, operation contracts, unsupported contracts, capability boundaries, and rubric fields. templates_list is the discovery step and carries only the fields needed to choose; this is the detail step and carries everything needed to use the chosen template. Both are projected from the same compiled catalog authority. An unknown templateKey returns a typed not-found response naming templates_list; it creates nothing and is never worth retrying with the same key. The response excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
|
|
2244
|
+
inputSchema: {
|
|
2245
|
+
type: 'object',
|
|
2246
|
+
additionalProperties: false,
|
|
2247
|
+
properties: {
|
|
2248
|
+
templateKey: { type: 'string', minLength: 1, description: 'Exact template key from templates_list storeTemplates[].key.' },
|
|
2249
|
+
workspaceId: { type: 'string', minLength: 1, description: 'Optional OAuth selector for an account-owned non-default workspace. A workspace API key may omit it or repeat only its own binding.' },
|
|
2250
|
+
},
|
|
2251
|
+
required: ['templateKey'],
|
|
2252
|
+
},
|
|
2253
|
+
},
|
|
1678
2254
|
{
|
|
1679
2255
|
name: 'stores_list',
|
|
1680
2256
|
description: 'List one workspace\'s practice-store fleet with ids, names, templates, generated-history span, readiness, active-run state, and deletion context. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace.',
|
|
@@ -1688,7 +2264,7 @@ export function createTools(config) {
|
|
|
1688
2264
|
},
|
|
1689
2265
|
{
|
|
1690
2266
|
name: 'store_create',
|
|
1691
|
-
description: 'Create a populated practice store from any templateKey returned by templates_list, including a template already represented in the fleet, in the selected workspace. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace. At the active-store limit, the server teaching error includes the current fleet.',
|
|
2267
|
+
description: 'Create a populated practice store from any templateKey returned by templates_list, including a template already represented in the fleet, in the selected workspace. The server gives ordinary stores 120 days of queryable history by default. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace. At the active-store limit, the server teaching error includes the current fleet.',
|
|
1692
2268
|
inputSchema: {
|
|
1693
2269
|
type: 'object', additionalProperties: false,
|
|
1694
2270
|
properties: {
|
|
@@ -1762,70 +2338,6 @@ export function createTools(config) {
|
|
|
1762
2338
|
required: ['workspaceId'],
|
|
1763
2339
|
},
|
|
1764
2340
|
},
|
|
1765
|
-
{
|
|
1766
|
-
name: 'share_create',
|
|
1767
|
-
description: 'Create a private evidence-share draft, or update its title/client-viewer audience while it remains draft or changes-requested. For a practice simulation receipt, pass the complete publicReceipt or impactPublicReceipt object from practice_run_report unchanged as artifactRef; practice_run_impact returns its impact reference as publicReceipt. The schema also names every legacy receipt, Shopify Exam, and Gate reference form. This owner-only OAuth tool does not expose evidence by itself: call share_publish for the outward-facing transition. Omit shareId to create with title, artifactType, artifactRef, and audienceUserIds; provide shareId to update only title and/or audienceUserIds. OAuth may select an owned workspace.',
|
|
1768
|
-
inputSchema: {
|
|
1769
|
-
type: 'object', additionalProperties: false,
|
|
1770
|
-
properties: {
|
|
1771
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1772
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Existing draft/changes-requested share to update. Omit to create.' },
|
|
1773
|
-
title: { type: 'string', minLength: 1, maxLength: 120 },
|
|
1774
|
-
artifactType: { type: 'string', enum: ['receipt', 'gate-evidence-export'], description: 'Required only when creating.' },
|
|
1775
|
-
artifactRef: SHARE_ARTIFACT_REF_INPUT_SCHEMA,
|
|
1776
|
-
audienceUserIds: { type: 'array', maxItems: 40, items: { type: 'string', minLength: 1 }, description: 'Active client-viewer user ids assigned to this exact workspace.' },
|
|
1777
|
-
},
|
|
1778
|
-
required: [],
|
|
1779
|
-
},
|
|
1780
|
-
},
|
|
1781
|
-
{
|
|
1782
|
-
name: 'shares_list',
|
|
1783
|
-
description: 'List private and outward evidence-share lifecycle rows in the selected workspace. This owner-only OAuth read does not expose evidence or mint viewer access. Use share_status for the exact artifact reference and audience ids.',
|
|
1784
|
-
inputSchema: {
|
|
1785
|
-
type: 'object', additionalProperties: false,
|
|
1786
|
-
properties: {
|
|
1787
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1788
|
-
},
|
|
1789
|
-
required: [],
|
|
1790
|
-
},
|
|
1791
|
-
},
|
|
1792
|
-
{
|
|
1793
|
-
name: 'share_status',
|
|
1794
|
-
description: 'Read one exact evidence share, including its immutable public artifact reference, selected client-viewer ids, state, and revocation status. This owner-only OAuth read has no outward effect.',
|
|
1795
|
-
inputSchema: {
|
|
1796
|
-
type: 'object', additionalProperties: false,
|
|
1797
|
-
properties: {
|
|
1798
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1799
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
|
|
1800
|
-
},
|
|
1801
|
-
required: ['shareId'],
|
|
1802
|
-
},
|
|
1803
|
-
},
|
|
1804
|
-
{
|
|
1805
|
-
name: 'share_publish',
|
|
1806
|
-
description: 'Outward-facing action: publish exposes the share artifact to its selected client viewers; re-share refreshes that exposure after a viewer requested changes. This is not a private action. Use action=publish only for a draft, or action=re-share only after share_status reports changes-requested.',
|
|
1807
|
-
inputSchema: {
|
|
1808
|
-
type: 'object', additionalProperties: false,
|
|
1809
|
-
properties: {
|
|
1810
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1811
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from share_status.' },
|
|
1812
|
-
action: { type: 'string', enum: ['publish', 're-share'], description: 'Exact server lifecycle transition. Defaults to publish.' },
|
|
1813
|
-
},
|
|
1814
|
-
required: ['shareId'],
|
|
1815
|
-
},
|
|
1816
|
-
},
|
|
1817
|
-
{
|
|
1818
|
-
name: 'share_revoke',
|
|
1819
|
-
description: 'Revoke an evidence share and withdraw client-viewer access. This outward-facing destructive permission change is idempotent and retains the owner-visible share lifecycle record.',
|
|
1820
|
-
inputSchema: {
|
|
1821
|
-
type: 'object', additionalProperties: false,
|
|
1822
|
-
properties: {
|
|
1823
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1824
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
|
|
1825
|
-
},
|
|
1826
|
-
required: ['shareId'],
|
|
1827
|
-
},
|
|
1828
|
-
},
|
|
1829
2341
|
{
|
|
1830
2342
|
name: 'catalog_slice_read',
|
|
1831
2343
|
description: 'Read up to 25 variants from one connected Shopify development store through the existing live catalog-slice route. This is an external Shopify read with no Shopify or Meguro mutation. OAuth may select an owned workspace.',
|
|
@@ -1843,32 +2355,12 @@ export function createTools(config) {
|
|
|
1843
2355
|
{
|
|
1844
2356
|
name: 'catalog_slice_snapshot',
|
|
1845
2357
|
description: 'Mint a short-lived signed catalog snapshot capability from either a live Shopify selection or one saved catalog slice. For a live external read provide shopDomain plus 1–25 variantIds; for a saved slice provide only sliceId. The capability remains bound to the selected workspace and store.',
|
|
1846
|
-
inputSchema:
|
|
1847
|
-
type: 'object', additionalProperties: false,
|
|
1848
|
-
properties: {
|
|
1849
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1850
|
-
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
|
|
1851
|
-
variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Exact Shopify variant ids returned by catalog_slice_read. Required with shopDomain.' },
|
|
1852
|
-
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
|
|
1853
|
-
},
|
|
1854
|
-
required: [],
|
|
1855
|
-
},
|
|
2358
|
+
inputSchema: CATALOG_SNAPSHOT_INPUT_SCHEMA,
|
|
1856
2359
|
},
|
|
1857
2360
|
{
|
|
1858
2361
|
name: 'catalog_slices_saved',
|
|
1859
2362
|
description: 'Manage the existing saved-catalog-slice lifecycle with action=list, get, save, delete, refresh, or changes. save/refresh read the connected Shopify store; delete removes only the exact saved slice; list/get/changes are Meguro reads. The worst-case annotation is intentionally destructive and outward because this grouped tool includes delete and external refresh.',
|
|
1860
|
-
inputSchema:
|
|
1861
|
-
type: 'object', additionalProperties: false,
|
|
1862
|
-
properties: {
|
|
1863
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1864
|
-
action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
|
|
1865
|
-
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
|
|
1866
|
-
label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
|
|
1867
|
-
variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
|
|
1868
|
-
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
|
|
1869
|
-
},
|
|
1870
|
-
required: ['action'],
|
|
1871
|
-
},
|
|
2363
|
+
inputSchema: CATALOG_SAVED_SLICES_INPUT_SCHEMA,
|
|
1872
2364
|
},
|
|
1873
2365
|
{
|
|
1874
2366
|
name: 'store_claim_by_code',
|
|
@@ -1927,7 +2419,7 @@ export function createTools(config) {
|
|
|
1927
2419
|
},
|
|
1928
2420
|
{
|
|
1929
2421
|
name: 'run_status',
|
|
1930
|
-
description: '
|
|
2422
|
+
description: 'Read one exact owned history run returned by runs_list. Returns lifecycle, progress, and its server-owned twinPair relation when present. Missing, expired, and foreign-workspace identities fail closed with runs_list recovery teaching.',
|
|
1931
2423
|
inputSchema: {
|
|
1932
2424
|
type: 'object',
|
|
1933
2425
|
additionalProperties: false,
|
|
@@ -1950,7 +2442,7 @@ export function createTools(config) {
|
|
|
1950
2442
|
},
|
|
1951
2443
|
{
|
|
1952
2444
|
name: 'run_report',
|
|
1953
|
-
description: '
|
|
2445
|
+
description: 'Read the retained receipt for one exact owned history run returned by runs_list. The receipt never claims more than the ledger can prove. Missing, expired, and foreign-workspace identities fail closed with runs_list recovery teaching.',
|
|
1954
2446
|
inputSchema: {
|
|
1955
2447
|
type: 'object',
|
|
1956
2448
|
additionalProperties: false,
|
|
@@ -1974,7 +2466,7 @@ export function createTools(config) {
|
|
|
1974
2466
|
},
|
|
1975
2467
|
{
|
|
1976
2468
|
name: 'runs_diff',
|
|
1977
|
-
description: 'Legacy receipt comparison for two Shopify dev-store history runs created by run_start. It does not compare practice simulation attempts returned by practice_run_start; a pa-* id receives a teaching error with the terminating receipt-fact method. For history runs it verifies the shared prefix, finds the fork, and returns impact receipt deltas. Prefer twin_diff for the named history-run impact receipt vocabulary. "Not comparable" is an honest answer, not an error in your usage.
|
|
2469
|
+
description: 'Legacy receipt comparison for two Shopify dev-store history runs created by run_start. It does not compare practice simulation attempts returned by practice_run_start; a pa-* id receives a teaching error with the terminating receipt-fact method. For history runs it verifies the shared prefix, finds the fork, and returns impact receipt deltas. Prefer twin_diff for the named history-run impact receipt vocabulary. "Not comparable" is an honest answer, not an error in your usage. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
|
|
1978
2470
|
inputSchema: {
|
|
1979
2471
|
type: 'object',
|
|
1980
2472
|
additionalProperties: false,
|
|
@@ -1987,26 +2479,26 @@ export function createTools(config) {
|
|
|
1987
2479
|
},
|
|
1988
2480
|
{
|
|
1989
2481
|
name: 'gate_configure',
|
|
1990
|
-
description:
|
|
2482
|
+
description: "Configure the optional Configured Receipt Gate policy over selected receipt facts. The receipt's automatic recorded API behavior remains available separately whether or not this configured policy exists. Choose one finished practice simulation receipt, enable or update the policy, or set enabled to false to disable it. Saving an enabled receipt also performs the same immediate server evaluation as Console.",
|
|
1991
2483
|
inputSchema: {
|
|
1992
2484
|
type: 'object',
|
|
1993
2485
|
additionalProperties: false,
|
|
1994
2486
|
properties: {
|
|
1995
|
-
receiptId: { type: 'string', minLength: 1, description: 'Finished pa-* practice simulation receipt to
|
|
2487
|
+
receiptId: { type: 'string', minLength: 1, description: 'Finished pa-* practice simulation receipt to evaluate with the Configured Receipt Gate.' },
|
|
1996
2488
|
storeId: { type: 'string', minLength: 1, description: 'Optional practice-store identity carried by the selected Console receipt.' },
|
|
1997
|
-
enabled: { type: 'boolean', description: 'Enable or update the Gate when true or omitted; disable
|
|
2489
|
+
enabled: { type: 'boolean', description: 'Enable or update the Configured Receipt Gate when true or omitted; disable it when false.' },
|
|
1998
2490
|
},
|
|
1999
2491
|
required: ['receiptId'],
|
|
2000
2492
|
},
|
|
2001
2493
|
},
|
|
2002
2494
|
{
|
|
2003
2495
|
name: 'gate_evaluate',
|
|
2004
|
-
description:
|
|
2496
|
+
description: "Evaluate the Configured Receipt Gate exactly as Console does. It is an optional configured policy over selected receipt facts, separate from the receipt's automatic recorded API behavior. Omit receiptId to evaluate the configured receipt, or supply one finished pa-* receipt explicitly. This records a new server-owned evaluation; it does not derive a local verdict.",
|
|
2005
2497
|
inputSchema: {
|
|
2006
2498
|
type: 'object',
|
|
2007
2499
|
additionalProperties: false,
|
|
2008
2500
|
properties: {
|
|
2009
|
-
receiptId: { type: 'string', minLength: 1, description: 'Optional finished pa-* practice simulation receipt. Omit it to evaluate the
|
|
2501
|
+
receiptId: { type: 'string', minLength: 1, description: 'Optional finished pa-* practice simulation receipt. Omit it to evaluate the Configured Receipt Gate receipt.' },
|
|
2010
2502
|
storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity. Omit when receiptId identifies the owner or the workspace has exactly one store.' },
|
|
2011
2503
|
},
|
|
2012
2504
|
required: [],
|
|
@@ -2014,20 +2506,20 @@ export function createTools(config) {
|
|
|
2014
2506
|
},
|
|
2015
2507
|
{
|
|
2016
2508
|
name: 'gate_verdict',
|
|
2017
|
-
description:
|
|
2509
|
+
description: "Read the latest server-computed Configured Receipt Gate verdict. This optional configured policy is separate from the receipt's automatic recorded API behavior, which remains available. A previously issued legacy artifact remains readable by its exact runId, but no customer surface creates one. Returns the exact meguro.gate-verdict.v1 object with go/waiting/no-go status, named fact-citing checks, per-check outcomes, flip condition, and named policy version. Facts are the product: this remains a named, reproducible, replaceable policy over facts the customer fully holds; check it yourself.",
|
|
2018
2510
|
inputSchema: {
|
|
2019
2511
|
type: 'object',
|
|
2020
2512
|
additionalProperties: false,
|
|
2021
2513
|
properties: {
|
|
2022
|
-
runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Receipt Gate verdict.' },
|
|
2023
|
-
storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity for the current per-store Gate lane.' },
|
|
2514
|
+
runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Configured Receipt Gate verdict.' },
|
|
2515
|
+
storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity for the current per-store Configured Receipt Gate lane.' },
|
|
2024
2516
|
},
|
|
2025
2517
|
required: [],
|
|
2026
2518
|
},
|
|
2027
2519
|
},
|
|
2028
2520
|
{
|
|
2029
2521
|
name: 'runs_list',
|
|
2030
|
-
description: '
|
|
2522
|
+
description: 'Discover the owned Shopify dev-store history runs readable through hosted OAuth. A completed server-owned twin relation is projected as twinPair with pairId, baseline/treated role, and exact counterpartRunId; pass those returned identities to run_status, run_report, and twin_diff. It does not list practice simulation attempts returned by practice_run_start. Optional lifecycle filtering supports running, paused, completed, failed, or cleaning.',
|
|
2031
2523
|
inputSchema: {
|
|
2032
2524
|
type: 'object',
|
|
2033
2525
|
additionalProperties: false,
|
|
@@ -2039,12 +2531,22 @@ export function createTools(config) {
|
|
|
2039
2531
|
},
|
|
2040
2532
|
{
|
|
2041
2533
|
name: 'usage_read',
|
|
2042
|
-
description: 'Read authoritative tier-meter simulation runs, practice-store, and workspace headroom
|
|
2043
|
-
inputSchema: {
|
|
2534
|
+
description: 'Read authoritative tier-meter simulation runs, practice-store, and workspace headroom. The compatibility field limits.runStartAllowed means only that metering allowance permits one charged run; it is not overall run-start eligibility. Omit storeId to receive runStartEligibility as explicitly not evaluated. Supply an exact storeId, and optionally the intended continueFromAttemptId, to receive the separate server-owned store/root lifecycle eligibility and exact next call. When retained activity reconciles, simulationRunAccounting gives the exact counted + excluded non-billable = visible completed equation and one exclusion entry per durable classification that caused the exclusion. Incomplete coverage or a source mismatch is disclosed without projecting a competing consumption number.',
|
|
2535
|
+
inputSchema: {
|
|
2536
|
+
type: 'object',
|
|
2537
|
+
additionalProperties: false,
|
|
2538
|
+
properties: {
|
|
2539
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2540
|
+
storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Optional exact practice-store id whose run-start lifecycle should be evaluated. Omission never implies global eligibility.' },
|
|
2541
|
+
continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Optional intended continuation root. Requires storeId and is evaluated against that exact store.' },
|
|
2542
|
+
},
|
|
2543
|
+
required: [],
|
|
2544
|
+
dependentRequired: { continueFromAttemptId: ['storeId'] },
|
|
2545
|
+
},
|
|
2044
2546
|
},
|
|
2045
2547
|
{
|
|
2046
2548
|
name: 'twin_diff',
|
|
2047
|
-
description: 'Read the impact receipt
|
|
2549
|
+
description: 'Read the impact receipt for the baseline and treated members of one server-recorded twinPair, in that order. The result carries pair identity, both exact source run IDs, their run_status and run_report calls, and current receipt retention. Missing, expired, foreign, reversed, or nonpaired sources fail closed; unrelated runs are never compared.',
|
|
2048
2550
|
inputSchema: {
|
|
2049
2551
|
type: 'object',
|
|
2050
2552
|
additionalProperties: false,
|
|
@@ -2069,7 +2571,7 @@ export function createTools(config) {
|
|
|
2069
2571
|
},
|
|
2070
2572
|
{
|
|
2071
2573
|
name: 'exam_start',
|
|
2072
|
-
description: 'Start or safely continue a Shopify Exam from one immutable practice attempt on the exact typed development-store domain. The tool performs idempotent create/preview/typed-confirm setup, then advances at most one bounded
|
|
2574
|
+
description: 'Start or safely continue a Shopify Exam from one immutable practice attempt on the exact typed development-store domain. The tool performs idempotent create/preview/typed-confirm setup, then advances at most one bounded execution or cleanup checkpoint. Call exam_status after every invocation; while nextAction names exam_start, repeat the same arguments. GET polling never advances or cleans the Exam and ambiguous delivery is never retried blindly.',
|
|
2073
2575
|
inputSchema: {
|
|
2074
2576
|
type: 'object', additionalProperties: false,
|
|
2075
2577
|
properties: {
|
|
@@ -2081,7 +2583,7 @@ export function createTools(config) {
|
|
|
2081
2583
|
},
|
|
2082
2584
|
{
|
|
2083
2585
|
name: 'exam_status',
|
|
2084
|
-
description: 'GET-only server-authoritative Shopify Exam lifecycle status: current revision and phase cursors, verdict/comparison summary when present, cleanup/residue state, immutable receipt availability, Gate projection, and the exact next tool. Reading status never advances or cleans the Exam.',
|
|
2586
|
+
description: 'GET-only server-authoritative Shopify Exam lifecycle status: current revision and phase cursors, verdict/comparison summary when present, cleanup/residue state, immutable receipt availability, Configured Receipt Gate projection, and the exact next tool. Reading status never advances or cleans the Exam; pending cleanup names exam_start as the continuation.',
|
|
2085
2587
|
inputSchema: {
|
|
2086
2588
|
type: 'object', additionalProperties: false,
|
|
2087
2589
|
properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Exam id returned by exam_start.' } },
|
|
@@ -2090,7 +2592,7 @@ export function createTools(config) {
|
|
|
2090
2592
|
},
|
|
2091
2593
|
{
|
|
2092
2594
|
name: 'exam_report',
|
|
2093
|
-
description: 'Read
|
|
2595
|
+
description: 'Read a cleaned or safely blocked Shopify Exam result and verify the exact stored bytes of both private and public immutable receipts against their artifact digests. A completed comparison with pending cleanup must be continued through exam_start before this tool returns a final report. Returns bounded receipt identities, digests, byte counts, verdict, comparison totals, cleanup/residue facts, and Configured Receipt Gate evidence — never raw private receipt payloads or credentials. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
|
|
2094
2596
|
inputSchema: {
|
|
2095
2597
|
type: 'object', additionalProperties: false,
|
|
2096
2598
|
properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Completed Exam id returned by exam_start.' } },
|
|
@@ -2099,18 +2601,18 @@ export function createTools(config) {
|
|
|
2099
2601
|
},
|
|
2100
2602
|
{
|
|
2101
2603
|
name: 'practice_run_start',
|
|
2102
|
-
description: 'Start an external-agent practice simulation run on an existing Meguro practice store, or continue one completed
|
|
2604
|
+
description: 'Start an external-agent practice simulation run on an existing Meguro practice store, or continue one completed segment on the same aging store when server authorization allows it. The returned lifecycleContract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and ordinary completed runs meter once. A fresh account\'s first accepted start may bind its one account-owned onboarding evaluation grant to the chosen store/template/root and pinned ideal horizon; every authorized segment is unmetered, while every run after that horizon follows ordinary tier policy. A fresh start replays the practice store\'s original scenario baseline as segment 1 by default; chaining is explicit through continueFromAttemptId. An optional immutable assertionPlan may predeclare exact expected negative-control refusals before call sequence zero; only exact server-matched reason, layer, target or request, and cardinality can prevent that rejection from becoming an unhandled Configured Receipt Gate failure. When clock.simulationDays is omitted, Meguro derives the full remaining scenario arc. Without active onboarding authorization, Builder plans a capped legal segment while Free or Solo refuses an above-cap arc. For an executable onboarding call, choose the matching starterRecommendation.starterPlans entry returned by templates_list and use its exact practiceRunStart.clock; otherwise use the returned ordinary starterPlan. Every accepted start returns a machine-readable segmentPlan with the exact Store-day range, authorization, and continuation narration. An advance target that reaches or crosses the planned end requires explicit terminal confirmation before the run ends; state-changing Admin calls require a running run, so use return-request-pending to stop on an actionable return before the planned end. The returned machine-readable simulationRunClock announces every exit: 1 hour for an agent that never executes, 48 hours with neither agent contact nor Store-time advance, and a 7-day run age (2 days on Free). Takes the canonical practice-store id (`storeId`) exactly as returned by get_connection_details, and returns the run identity (`attemptId`) every later practice_run_* tool uses — the store id and the run id are different identifiers. Meguro Console is quick proof, while the agency agent still runs in the caller\'s own environment. Returns the same server-authoritative practice-run contract without connection credentials.',
|
|
2103
2605
|
inputSchema: {
|
|
2104
2606
|
type: 'object',
|
|
2105
2607
|
additionalProperties: false,
|
|
2106
2608
|
properties: {
|
|
2609
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2107
2610
|
storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
|
|
2108
|
-
|
|
2109
|
-
continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed prior segment to continue on the same aging practice store. Available only when the server tier policy allows continuation. Omit clock.simulationDays to let Meguro plan the next legal segment automatically.' },
|
|
2611
|
+
continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed prior segment to continue on the same aging practice store. Available only when the server tier policy allows continuation. On Builder, omit clock.simulationDays to let Meguro plan the next legal segment automatically.' },
|
|
2110
2612
|
clock: {
|
|
2111
2613
|
type: 'object',
|
|
2112
2614
|
additionalProperties: false,
|
|
2113
|
-
description: 'Optional hosted practice-run clock.
|
|
2615
|
+
description: 'Optional hosted practice-run clock. Omission derives the full remaining scenario arc and may refuse on Free or Solo; use templates_list starterPlan.practiceRunStart.clock for the current tier\'s executable first segment, or set a shorter bounded segment explicitly.',
|
|
2114
2616
|
properties: {
|
|
2115
2617
|
mode: { type: 'string', enum: ['manual', 'harness', 'scheduled'] },
|
|
2116
2618
|
simulationDays: { type: 'integer', minimum: 0, description: 'Explicit segment length. Requests above the tier segment or remaining canvas cap are refused with an executable maximum.' },
|
|
@@ -2132,10 +2634,37 @@ export function createTools(config) {
|
|
|
2132
2634
|
required: ['name', 'version', 'ref'],
|
|
2133
2635
|
},
|
|
2134
2636
|
observationPlan: { ...PRACTICE_RUN_OBSERVATION_PLAN_INPUT_SCHEMA, description: 'Optional trusted read-only checkpoint observation plan. Use one closed observations envelope; each row is either an inline read request or one saved read probe.' },
|
|
2637
|
+
assertionPlan: {
|
|
2638
|
+
type: 'object',
|
|
2639
|
+
additionalProperties: false,
|
|
2640
|
+
description: 'Optional immutable run-start expected-refusal declarations. Generic expected flags, wildcards, HTTP classes, and Meguro compatibility failures are refused.',
|
|
2641
|
+
properties: {
|
|
2642
|
+
schemaVersion: { type: 'string', const: 'meguro.run-assertion-plan.v1' },
|
|
2643
|
+
expectedRefusals: {
|
|
2644
|
+
type: 'array', minItems: 1, maxItems: 10,
|
|
2645
|
+
items: {
|
|
2646
|
+
type: 'object', additionalProperties: false,
|
|
2647
|
+
properties: {
|
|
2648
|
+
assertionId: { type: 'string', pattern: '^[a-z][a-z0-9_.-]{2,95}$' },
|
|
2649
|
+
operationFamily: { type: 'string', const: 'shopify-admin-graphql-mutation' },
|
|
2650
|
+
operationName: { type: 'string', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,95}$' },
|
|
2651
|
+
targetGid: { type: 'string', pattern: '^gid://shopify/[A-Za-z][A-Za-z0-9]*/[A-Za-z0-9._:-]{1,160}$' },
|
|
2652
|
+
requestFingerprint: { type: 'string', pattern: '^[0-9a-f]{64}$' },
|
|
2653
|
+
expectedResult: { type: 'string', const: 'rejected' },
|
|
2654
|
+
rejectionReasonCodes: { type: 'array', minItems: 1, maxItems: 8, uniqueItems: true, items: { type: 'string', enum: ['shopify_idempotency_required', 'agent_request_verified_invalid', 'modeled_business_rule_rejected', 'external_platform_rejected'] } },
|
|
2655
|
+
responsibleLayer: { type: 'string', enum: ['agent_request', 'modeled_business_rule', 'external_platform'] },
|
|
2656
|
+
cardinality: { type: 'object', additionalProperties: false, properties: { minAttempts: { type: 'integer', minimum: 1, maximum: 10 }, maxAttempts: { type: 'integer', minimum: 1, maximum: 10 } }, required: ['minAttempts', 'maxAttempts'] },
|
|
2657
|
+
correctionExpectation: { type: 'object', additionalProperties: false, properties: { result: { type: 'string', const: 'accepted-same-operation-target' }, withinFollowingCalls: { type: 'integer', minimum: 1, maximum: 50 } }, required: ['result', 'withinFollowingCalls'] },
|
|
2658
|
+
},
|
|
2659
|
+
required: ['assertionId', 'operationFamily', 'operationName', 'expectedResult', 'rejectionReasonCodes', 'responsibleLayer', 'cardinality'],
|
|
2660
|
+
},
|
|
2661
|
+
},
|
|
2662
|
+
},
|
|
2663
|
+
required: ['schemaVersion', 'expectedRefusals'],
|
|
2664
|
+
},
|
|
2135
2665
|
graphqlThrottleMode: { type: 'string', enum: ['off', 'standard-1000', 'plus-2000'], description: 'Optional Meguro compatibility GraphQL throttle mode.' },
|
|
2136
2666
|
},
|
|
2137
|
-
required: [],
|
|
2138
|
-
anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
|
|
2667
|
+
required: ['storeId'],
|
|
2139
2668
|
},
|
|
2140
2669
|
},
|
|
2141
2670
|
{
|
|
@@ -2154,10 +2683,13 @@ export function createTools(config) {
|
|
|
2154
2683
|
},
|
|
2155
2684
|
{
|
|
2156
2685
|
name: 'practice_run_status',
|
|
2157
|
-
description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines
|
|
2686
|
+
description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines, immutable simulation run receipt after any exit, and lifecycleContract. That contract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and an ordinary completed run is metered once while the explicitly provisioned sample is exempt. Immediately before advancing, copy practiceRun.advanceCursor unchanged into practice_run_advance; do not choose between currentDay and watermarkDay or assemble call-sequence fields. If the next target reaches or crosses endDay, follow the exact terminal-confirmation retry returned by practice_run_advance. A running run authorizes state-changing Admin calls; after completion later writes receive the stable run-complete refusal. Use Console for quick proof; the tested agent remains in the caller\'s environment.',
|
|
2158
2687
|
inputSchema: {
|
|
2159
2688
|
type: 'object', additionalProperties: false,
|
|
2160
|
-
properties: {
|
|
2689
|
+
properties: {
|
|
2690
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2691
|
+
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2692
|
+
},
|
|
2161
2693
|
required: ['attemptId'],
|
|
2162
2694
|
},
|
|
2163
2695
|
},
|
|
@@ -2166,24 +2698,28 @@ export function createTools(config) {
|
|
|
2166
2698
|
description: 'Capture an immutable checkpoint for an external-agent practice run and return a bounded evidence summary. Console is quick proof; agent execution remains in the caller\'s environment.',
|
|
2167
2699
|
inputSchema: {
|
|
2168
2700
|
type: 'object', additionalProperties: false,
|
|
2169
|
-
properties: {
|
|
2701
|
+
properties: {
|
|
2702
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2703
|
+
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2704
|
+
},
|
|
2170
2705
|
required: ['attemptId'],
|
|
2171
2706
|
},
|
|
2172
2707
|
},
|
|
2173
2708
|
{
|
|
2174
2709
|
name: 'practice_run_advance',
|
|
2175
|
-
description: 'Advance an external-agent practice run by whole days or until one closed-set commerce condition, using
|
|
2710
|
+
description: 'Advance an external-agent practice run by whole days or until one closed-set commerce condition, using the server-owned optimistic-concurrency advanceCursor. Accepted writes are disposable attempt-local state: they remain in receipt and Impact evidence, while the underlying practice store returns to baseline after completion. Terminal reads remain available and terminal writes are refused. An advance that reaches or crosses planned endDay refuses without changing Store time unless the caller explicitly retries the returned shape with confirmConclusion: true; that confirmed retry concludes the attempt and closes its write lane. return-request-pending stops before planned end with the evaluated Store day, pending-request count, and one bounded opportunity for the next read; it never resolves the request automatically. If no opportunity exists before planned end, the explicit confirmation path ends in the stable run-complete refusal for later writes and offers no write suggestion. State-changing Admin calls require a running run. Immediately before every advance, call practice_run_status and copy practiceRun.advanceCursor unchanged. If it is stale, Meguro refuses without moving Store time and returns a fresh advanceCursor for one exact retry; do not infer from currentDay, watermarkDay, or call-sequence fields. Before durable agentExecutionStartedAt exists, at most 7 Store days may be advanced; connect your agent — any call unlocks further time. Console is quick proof; agent execution remains in the caller\'s environment.',
|
|
2176
2711
|
inputSchema: {
|
|
2177
2712
|
type: 'object',
|
|
2178
2713
|
additionalProperties: false,
|
|
2179
2714
|
properties: {
|
|
2715
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2180
2716
|
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2181
2717
|
days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
|
|
2182
2718
|
until: PRACTICE_RUN_UNTIL_INPUT_SCHEMA,
|
|
2183
|
-
|
|
2184
|
-
|
|
2719
|
+
advanceCursor: PRACTICE_RUN_ADVANCE_CURSOR_INPUT_SCHEMA,
|
|
2720
|
+
confirmConclusion: { type: 'boolean', description: 'Set true only after reading the terminal-boundary teaching refusal and choosing to conclude the attempt and close its write lane.' },
|
|
2185
2721
|
},
|
|
2186
|
-
required: ['attemptId', '
|
|
2722
|
+
required: ['attemptId', 'advanceCursor'],
|
|
2187
2723
|
oneOf: [
|
|
2188
2724
|
{ required: ['days'], not: { required: ['until'] } },
|
|
2189
2725
|
{ required: ['until'], not: { required: ['days'] } },
|
|
@@ -2192,43 +2728,57 @@ export function createTools(config) {
|
|
|
2192
2728
|
},
|
|
2193
2729
|
{
|
|
2194
2730
|
name: 'practice_run_finish',
|
|
2195
|
-
description: '
|
|
2731
|
+
description: 'End an external-agent practice segment without inventing time or evidence. A segment at planned end completes; an under-measured segment stops and remains explicitly incomplete. For a stopped onboarding segment, the server-owned practiceRun.recovery object gives the exact practice_run_start call that resumes the same bound root at its authoritative watermark. To abandon that incomplete onboarding evaluation permanently, set both permanentStop and acknowledgeIncompleteOnboardingEvaluation true; Meguro preserves evidence and consumed onboarding value. The response includes the immutable segment receipt and announced deadlines. Console is quick proof; agent execution remains in the caller\'s environment.',
|
|
2196
2732
|
inputSchema: {
|
|
2197
2733
|
type: 'object', additionalProperties: false,
|
|
2198
|
-
properties: {
|
|
2734
|
+
properties: {
|
|
2735
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2736
|
+
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2737
|
+
permanentStop: { type: 'boolean', description: 'Set true only to abandon an under-measured onboarding evaluation permanently. Requires acknowledgeIncompleteOnboardingEvaluation=true.' },
|
|
2738
|
+
acknowledgeIncompleteOnboardingEvaluation: { type: 'boolean', description: 'Set true with permanentStop to acknowledge that the bound onboarding evaluation will remain incomplete while its evidence and consumed value remain preserved.' },
|
|
2739
|
+
},
|
|
2199
2740
|
required: ['attemptId'],
|
|
2741
|
+
dependentRequired: {
|
|
2742
|
+
permanentStop: ['acknowledgeIncompleteOnboardingEvaluation'],
|
|
2743
|
+
acknowledgeIncompleteOnboardingEvaluation: ['permanentStop'],
|
|
2744
|
+
},
|
|
2200
2745
|
},
|
|
2201
2746
|
},
|
|
2202
2747
|
{
|
|
2203
2748
|
name: 'practice_run_report',
|
|
2204
|
-
description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. `report` is the protocol/route compatibility name for the receipt (the tool name and the `operation: "report"` field keep it for wire stability); user-facing language is "receipt". The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Raw private request and response payloads stay out of MCP; use Console for quick proof while the agent remains in the caller\'s environment.
|
|
2749
|
+
description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. `reportBack` is the exact server-owned customer summary in the fixed order Mechanics, Conduct, Scenario outcome; MCP passes it through without re-deriving or reclassifying any plane, and modeled money closes its narrative. `takeHome` is the frozen take-home packet the issued receipt itself carries — the same ordered Mechanics, Conduct, Scenario outcome facts the public receipt view serves, plus the source URL and SHA-256, the claims/evidence boundary, the verification steps, and a copy-ready `memo`; MCP passes it through unchanged and compiles no part of it. `eventResolution` passes through the exact creation-pinned template identity/revision, evaluation horizon, measured horizon, and typed event-window result; it never recovers current catalog metadata for a historical run. `gate` is the exact server-owned final machine projection; `rejectionAttribution.gateStatus` is mechanically aligned with it, and neither value is re-derived by MCP. Immutable expected-refusal declarations appear with their exact server-emitted matched or failure status; a matched rejection remains in raw receipt counts while only its unhandled Configured Receipt Gate consequence changes. Interpret `resultSemantics.lifecycle`, `evidenceAssertions`, `technicalPolicyVerdict`, and `modeledScenarioOutcome` as independent named dimensions; there is no implicit overall status. Existing `summary`, `defaultVerdict`, `verdict`, and `measurement` fields are legacy compatibility fields. `report` is the protocol/route compatibility name for the receipt (the tool name and the `operation: "report"` field keep it for wire stability); user-facing language is "receipt". The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Raw private request and response payloads stay out of MCP; use Console for quick proof while the agent remains in the caller\'s environment. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
|
|
2205
2750
|
inputSchema: {
|
|
2206
2751
|
type: 'object', additionalProperties: false,
|
|
2207
|
-
properties: {
|
|
2752
|
+
properties: {
|
|
2753
|
+
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2754
|
+
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace coordinate returned by practice_runs_list. Omit for Default.' },
|
|
2755
|
+
},
|
|
2208
2756
|
required: ['attemptId'],
|
|
2209
2757
|
},
|
|
2210
2758
|
},
|
|
2211
2759
|
{
|
|
2212
2760
|
name: 'practice_run_impact',
|
|
2213
|
-
description: 'Read the impact receipt for a completed external-agent practice run: the base year — the same store, same days, without your agent — compared with your agent\'s year, showing what changed because of your agent (orders, units, and revenue), a footprint of every write, and an integrity gate that reports a reason instead of numbers when the comparison is not clean. The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Read-only; never mutates the run and returns no credentials. Takes the run\'s attemptId (from practice_run_start), not the practice-store id (storeId).
|
|
2761
|
+
description: 'Read the impact receipt for a completed external-agent practice run: the base year — the same store, same days, without your agent — compared with your agent\'s year, showing what changed because of your agent (orders, units, and revenue), a footprint of every write, and an integrity gate that reports a reason instead of numbers when the comparison is not clean. `eventResolution` is the exact creation-pinned template identity/revision, evaluation horizon, measured horizon, and typed event-window result; historical runs without that snapshot are explicitly unavailable and never consult current template metadata. The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Read-only; never mutates the run and returns no credentials. Takes the run\'s attemptId (from practice_run_start), not the practice-store id (storeId). Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
|
|
2214
2762
|
inputSchema: {
|
|
2215
2763
|
type: 'object', additionalProperties: false,
|
|
2216
|
-
properties: {
|
|
2764
|
+
properties: {
|
|
2765
|
+
attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
|
|
2766
|
+
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace coordinate returned by practice_runs_list. Omit for Default.' },
|
|
2767
|
+
},
|
|
2217
2768
|
required: ['attemptId'],
|
|
2218
2769
|
},
|
|
2219
2770
|
},
|
|
2220
2771
|
{
|
|
2221
2772
|
name: 'get_connection_details',
|
|
2222
|
-
description: `Fetch connection material for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token,
|
|
2773
|
+
description: `Fetch connection material and a structured, state-aware Admin execution guide for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token, shop domain, current run status/cursors, valid next calls, and the read/write/idempotency sequence. The hosted OAuth grant or the public STDIO workspace-bound account API key authenticates MCP/control-plane calls only. Shopify-shaped Admin data-plane calls use the returned Admin URL and per-store token in the X-Shopify-Access-Token header; never send the OAuth bearer to that URL. Examples carry only <SHOPIFY_ADMIN_ACCESS_TOKEN>, never the returned secret. Inspect the selected recipe or mutation with admin_schema before the first Admin call; an agent targeting a newer Shopify release outside the supported set must treat that behavior as not established. Takes the canonical practice-store id (storeId) and returns it back, so the result passes directly into practice_run_start({ storeId }) — no identifier translation. The practice-store id is not an attemptId (run identity).`,
|
|
2223
2774
|
inputSchema: {
|
|
2224
2775
|
type: 'object',
|
|
2225
2776
|
additionalProperties: false,
|
|
2226
2777
|
properties: {
|
|
2778
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2227
2779
|
storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id, e.g. w-abc123. Pass the returned storeId straight into practice_run_start.' },
|
|
2228
|
-
worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
|
|
2229
2780
|
},
|
|
2230
|
-
required: [],
|
|
2231
|
-
anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
|
|
2781
|
+
required: ['storeId'],
|
|
2232
2782
|
},
|
|
2233
2783
|
},
|
|
2234
2784
|
{
|
|
@@ -2238,6 +2788,7 @@ export function createTools(config) {
|
|
|
2238
2788
|
type: 'object',
|
|
2239
2789
|
additionalProperties: false,
|
|
2240
2790
|
properties: {
|
|
2791
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2241
2792
|
worldId: { type: 'string', minLength: 1, description: 'Meguro practice store id, e.g. w-abc123.' },
|
|
2242
2793
|
query: { type: 'string', maxLength: 30000, description: 'The Admin GraphQL query or mutation document to run.' },
|
|
2243
2794
|
apiVersion: { type: 'string', pattern: '^\\d{4}-\\d{2}$', description: `Admin API version as YYYY-MM. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}. Defaults to ${ADMIN_API_DEFAULT_VERSION}.` },
|
|
@@ -2250,16 +2801,66 @@ export function createTools(config) {
|
|
|
2250
2801
|
},
|
|
2251
2802
|
{
|
|
2252
2803
|
name: 'admin_schema',
|
|
2253
|
-
description: `Look up an exact, bounded slice of the Shopify Admin GraphQL surface Meguro models for supported versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns a type and its direct fields, a Type.field or root mutation with its arguments and return type, a shared commerce recipe, or an authored valid-not-modeled teaching boundary. Deterministic — it never returns the full schema, tenant data, catalog, or credentials, and on a miss it suggests only exact prefix/substring alternatives.`,
|
|
2804
|
+
description: `Look up an exact, bounded slice of the Shopify Admin GraphQL surface Meguro models for supported versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns a type and its direct fields, a Type.field or root mutation with its arguments and return type, a shared commerce recipe, or an authored valid-not-modeled teaching boundary. A recipe match carries its bindingPlan, and every published mutation recipe carries match.recipe.evidencePlan: the eligibility read to run first (its exact query, variables, a selection sentence naming which returned row to use, and bindings from each response path to the mutation variable it fills), then the mutation, then the existing independent readback. mutation.documentOperationName is the GraphQL document label; mutation.rootField is the Shopify mutation coordinate. Where the mutation has a read-before-write cursor the plan also carries expectedRefusalOperationName — the exact root-field matcher key — plus staleCursorExpectedRefusal reason and layer facts to copy unchanged into an assertionPlan. Mutation example values that only the eligibility read can supply appear as <from-eligibility:VARIABLE_PATH> placeholders, never as plausible literals. Deterministic — it never returns the full schema, tenant data, catalog, or credentials, and on a miss it suggests only exact prefix/substring alternatives. Alternatively, submit query instead of lookup + name — with optional variables, operationName, and apiVersion — to ask what this exact Admin document would receive before you create a practice store or spend credit. It parses the document and classifies the selected operation and every supplied coordinate through the same served schema, versioned compatibility registry, and required-argument recovery authority the served Admin surface uses, returning exactly one state: "served" — no compatibility teaching refusal would be produced; "unsupported-with-recovery" — the exact refusal plus the registry remedy naming the supported coordinate or the exact admin_schema call that answers a required-argument refusal; "out-of-plane" — outside the claimed Admin plane, or no exact recovery authority exists. A compatibility withdrawal also carries teachingRefusal: the same refusal the served Admin surface returns, with its code, coordinate, reason, and next step. A document that will not parse, or whose operation cannot be selected without guessing, comes back with that as its own reason rather than guessed compatibility. Store-free and pre-credit: it creates or reads no practice store, run, attempt, ledger, probe, or checkpoint, consumes no credit, and never executes the submitted document. The two modes are exclusive: pass lookup + name or query, never both.`,
|
|
2254
2805
|
inputSchema: {
|
|
2255
2806
|
type: 'object',
|
|
2256
2807
|
additionalProperties: false,
|
|
2257
2808
|
properties: {
|
|
2258
|
-
lookup: { type: 'string', enum: ['type', 'field', 'mutation', 'recipe'], description: '
|
|
2259
|
-
name: { type: 'string', minLength: 1, maxLength: 200, description: '
|
|
2809
|
+
lookup: { type: 'string', enum: ['type', 'field', 'mutation', 'recipe'], description: 'Lookup mode: what to look up — a type, a Type.field, a root mutation, or a commerce recipe. Requires name.' },
|
|
2810
|
+
name: { type: 'string', minLength: 1, maxLength: 200, description: 'Lookup mode: exact name, e.g. ProductVariant, ProductVariant.inventoryQuantity, inventoryAdjustQuantities, or a recipe id/operationName like low-inventory or LowInventory.' },
|
|
2811
|
+
query: { type: 'string', minLength: 1, maxLength: 30000, description: 'Refusal mode: the exact Admin GraphQL document you intend to send. Classified, never executed.' },
|
|
2812
|
+
variables: { type: 'object', description: 'Refusal mode: optional GraphQL variables object (max 20,000 serialized characters). Supplied input-object fields are classified, not executed.' },
|
|
2813
|
+
operationName: { type: 'string', minLength: 1, maxLength: 200, description: 'Refusal mode: which operation to classify when the document defines more than one. Without it, a multi-operation document is refused rather than guessed.' },
|
|
2260
2814
|
apiVersion: { type: 'string', pattern: '^\\d{4}-\\d{2}$', description: `Admin API version YYYY-MM. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}. Defaults to ${ADMIN_API_DEFAULT_VERSION}.` },
|
|
2261
2815
|
},
|
|
2262
|
-
required: [
|
|
2816
|
+
required: [],
|
|
2817
|
+
oneOf: [
|
|
2818
|
+
{ type: 'object', required: ['lookup', 'name'], not: { anyOf: [{ required: ['query'] }, { required: ['variables'] }, { required: ['operationName'] }] } },
|
|
2819
|
+
{ type: 'object', required: ['query'], not: { anyOf: [{ required: ['lookup'] }, { required: ['name'] }] } },
|
|
2820
|
+
],
|
|
2821
|
+
},
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
name: 'admin_recipes_list',
|
|
2825
|
+
description: 'List every canonical Admin API recipe Meguro models, with its stable id, document operation name, purpose, confirmation requirement, authored aliases/keywords, declared API version and templates, support boundary and fidelity, what an empty result means, any prerequisite that must hold first, and its bindingPlan. A published mutation recipe also carries evidencePlan with the eligibility and readback document labels, mutationDocumentOperationName, mutationRootField, and — when a stale-cursor refusal is predeclarable — expectedRefusalOperationName as the exact root-field matcher key. Names only, so discovery stays document-free. Registry-only discovery: it returns no tenant, store, catalog, credential, or probe data and never executes an Admin request. Use a returned id with admin_schema({ lookup: "recipe", name: "<id>" }) for the exact executable documents.',
|
|
2826
|
+
inputSchema: {
|
|
2827
|
+
type: 'object',
|
|
2828
|
+
additionalProperties: false,
|
|
2829
|
+
properties: {},
|
|
2830
|
+
required: [],
|
|
2831
|
+
},
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
name: 'plan_validate',
|
|
2835
|
+
description: `Check whether an intended set of Shopify Admin GraphQL operations fits Meguro before creating a practice store or starting a run. Takes up to 25 documents in order and returns one result per input item, in input order, each carrying exactly one state: "served" — the selected operation and every supplied coordinate are represented by the current served practice Admin schema, with the exact coordinates it reaches; "unsupported-with-recovery" — an authority hands back a concrete recovery path, either the compatibility registry's remedy naming the supported coordinate (with the canonical recipe document when one is authored) or the exact admin_schema call that answers a required-argument refusal; "out-of-plane" — the operation is outside the served contract, or no exact recovery authority exists for it. Supplied variables and input-object fields are classified through the same input-field compatibility seam the served Admin surface uses. A document that will not parse, or whose operation cannot be selected without guessing, returns an item-local refusal instead of a guessed operation. Every reason, coordinate, alternative, and recovery comes from the served schema, the versioned compatibility registry, or the recovery authority — never an invented alternative, and never a claim about whether a particular practice store or template can execute it. Pre-credit and store-free: it needs no practice store, attempt, or run, creates and reads none, consumes no credit or metered usage, and never executes a submitted document. Supported Admin API versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; per-item apiVersion defaults to ${ADMIN_API_DEFAULT_VERSION}.`,
|
|
2836
|
+
inputSchema: {
|
|
2837
|
+
type: 'object',
|
|
2838
|
+
additionalProperties: false,
|
|
2839
|
+
properties: {
|
|
2840
|
+
operations: {
|
|
2841
|
+
type: 'array',
|
|
2842
|
+
minItems: 1,
|
|
2843
|
+
maxItems: 25,
|
|
2844
|
+
description: 'The intended Admin GraphQL documents, in the order you plan to run them.',
|
|
2845
|
+
items: {
|
|
2846
|
+
type: 'object',
|
|
2847
|
+
additionalProperties: false,
|
|
2848
|
+
properties: {
|
|
2849
|
+
query: { type: 'string', minLength: 1, maxLength: 30000, description: 'The exact Admin GraphQL document you intend to send.' },
|
|
2850
|
+
variables: { type: 'object', description: 'Optional GraphQL variables object (max 20,000 serialized characters). Supplied input-object fields are classified, not executed.' },
|
|
2851
|
+
operationName: { type: 'string', minLength: 1, maxLength: 200, description: 'Which operation to check when the document defines more than one. Without it, a multi-operation document is refused rather than guessed.' },
|
|
2852
|
+
apiVersion: {
|
|
2853
|
+
type: 'string',
|
|
2854
|
+
enum: [...ADMIN_API_SUPPORTED_VERSIONS],
|
|
2855
|
+
default: ADMIN_API_DEFAULT_VERSION,
|
|
2856
|
+
description: `Admin API version to classify this item against. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}.`,
|
|
2857
|
+
},
|
|
2858
|
+
},
|
|
2859
|
+
required: ['query'],
|
|
2860
|
+
},
|
|
2861
|
+
},
|
|
2862
|
+
},
|
|
2863
|
+
required: ['operations'],
|
|
2263
2864
|
},
|
|
2264
2865
|
},
|
|
2265
2866
|
];
|
|
@@ -2277,13 +2878,16 @@ export function createTools(config) {
|
|
|
2277
2878
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
2278
2879
|
throw new Error(`${name} arguments must be an object`);
|
|
2279
2880
|
}
|
|
2881
|
+
if (name === 'workspace_archive' || name === 'workspace_unarchive') {
|
|
2882
|
+
requiredWorkspaceId(args);
|
|
2883
|
+
}
|
|
2280
2884
|
if (SCHEMA_VALIDATED_TOOL_NAMES.has(name)) {
|
|
2281
|
-
// Preserve the established identifier teaching messages while the published schema carries
|
|
2282
|
-
// the same shape for schema-first clients. This also handles the equal-value legacy alias
|
|
2283
|
-
// pair, whose equality cannot be expressed in portable JSON Schema.
|
|
2284
2885
|
if (name === 'get_connection_details' || name === 'practice_run_start') requiredPracticeStoreId(args);
|
|
2285
2886
|
const error = schemaValidationError(definition.inputSchema, args);
|
|
2286
|
-
if (error)
|
|
2887
|
+
if (error) {
|
|
2888
|
+
const correction = conditionalToolCorrection(name, args);
|
|
2889
|
+
throw new Error(`${name} arguments do not match the declared input schema: ${error}${correction ? `. Corrected call: ${correction}` : ''}`);
|
|
2890
|
+
}
|
|
2287
2891
|
return;
|
|
2288
2892
|
}
|
|
2289
2893
|
if (definition.inputSchema.additionalProperties !== false) return;
|
|
@@ -2302,15 +2906,34 @@ export function createTools(config) {
|
|
|
2302
2906
|
if (!DOCUMENTATION_TOPICS.has(topic)) {
|
|
2303
2907
|
throw new Error(DOCUMENTATION_TOOL_CONTRACT.topicError);
|
|
2304
2908
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2909
|
+
// Refuse a malformed version before any document read: only a positive integer or the
|
|
2910
|
+
// exact "latest" sentinel reaches the resolver.
|
|
2911
|
+
const requested = args.version;
|
|
2912
|
+
const wellFormed = requested === 'latest'
|
|
2913
|
+
|| (Number.isSafeInteger(requested) && requested >= 1);
|
|
2914
|
+
if (!wellFormed) {
|
|
2915
|
+
throw new Error(DOCUMENTATION_TOOL_CONTRACT.versionError);
|
|
2307
2916
|
}
|
|
2308
|
-
return textResult(
|
|
2917
|
+
return textResult(resolveDocumentationRead(topic, requested, DOCUMENTATION_TOOL_CONTRACT));
|
|
2309
2918
|
}
|
|
2310
2919
|
case 'templates_list': {
|
|
2311
2920
|
const workspaceId = optionalWorkspaceId(args);
|
|
2312
2921
|
const response = await fleetRequest(name, 'GET', '/practice/profiles', undefined, { workspaceId });
|
|
2313
|
-
return 'error' in response
|
|
2922
|
+
return 'error' in response
|
|
2923
|
+
? response.error
|
|
2924
|
+
: textResult(JSON.stringify(storeTemplatesProjection(response.value)));
|
|
2925
|
+
}
|
|
2926
|
+
case 'template_get': {
|
|
2927
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2928
|
+
const templateKey = requiredString(args, 'templateKey');
|
|
2929
|
+
const response = await fleetRequest(
|
|
2930
|
+
name,
|
|
2931
|
+
'GET',
|
|
2932
|
+
`/practice/profiles/${encodeURIComponent(templateKey)}`,
|
|
2933
|
+
undefined,
|
|
2934
|
+
{ workspaceId },
|
|
2935
|
+
);
|
|
2936
|
+
return 'error' in response ? response.error : textResult(storeTemplateDetailProjection(response.value));
|
|
2314
2937
|
}
|
|
2315
2938
|
case 'stores_list': {
|
|
2316
2939
|
const workspaceId = optionalWorkspaceId(args);
|
|
@@ -2363,59 +2986,6 @@ export function createTools(config) {
|
|
|
2363
2986
|
const response = await fleetRequest(name, 'POST', `/workspaces/${encodeURIComponent(workspaceId)}/${action}`, {});
|
|
2364
2987
|
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2365
2988
|
}
|
|
2366
|
-
case 'share_create': {
|
|
2367
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2368
|
-
if (args.shareId !== undefined) {
|
|
2369
|
-
const shareId = requiredShareId(args);
|
|
2370
|
-
if (args.artifactType !== undefined || args.artifactRef !== undefined) {
|
|
2371
|
-
throw new Error('artifactType and artifactRef are immutable; omit shareId to create a new share');
|
|
2372
|
-
}
|
|
2373
|
-
const body = {};
|
|
2374
|
-
if (args.title !== undefined) {
|
|
2375
|
-
body.title = requiredString(args, 'title');
|
|
2376
|
-
if (body.title.length > 120) throw new Error('title must be 120 characters or fewer');
|
|
2377
|
-
}
|
|
2378
|
-
const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
|
|
2379
|
-
if (audienceUserIds !== undefined) body.audienceUserIds = audienceUserIds;
|
|
2380
|
-
if (Object.keys(body).length === 0) throw new Error('share update requires title and/or audienceUserIds');
|
|
2381
|
-
const response = await fleetRequest(name, 'PATCH', `/shares/${encodeURIComponent(shareId)}`, body, { workspaceId });
|
|
2382
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2383
|
-
}
|
|
2384
|
-
const title = requiredString(args, 'title');
|
|
2385
|
-
if (title.length > 120) throw new Error('title must be 120 characters or fewer');
|
|
2386
|
-
const { artifactType, artifactRef } = requiredShareArtifact(args);
|
|
2387
|
-
const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
|
|
2388
|
-
if (!audienceUserIds) throw new Error('audienceUserIds is required when creating a share');
|
|
2389
|
-
const response = await fleetRequest(name, 'POST', '/shares', {
|
|
2390
|
-
title, artifactType, artifactRef, audienceUserIds,
|
|
2391
|
-
}, { workspaceId });
|
|
2392
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2393
|
-
}
|
|
2394
|
-
case 'shares_list': {
|
|
2395
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2396
|
-
const response = await fleetRequest(name, 'GET', '/shares', undefined, { workspaceId });
|
|
2397
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2398
|
-
}
|
|
2399
|
-
case 'share_status': {
|
|
2400
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2401
|
-
const shareId = requiredShareId(args);
|
|
2402
|
-
const response = await fleetRequest(name, 'GET', `/shares/${encodeURIComponent(shareId)}`, undefined, { workspaceId });
|
|
2403
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2404
|
-
}
|
|
2405
|
-
case 'share_publish': {
|
|
2406
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2407
|
-
const shareId = requiredShareId(args);
|
|
2408
|
-
const action = args.action === undefined ? 'publish' : requiredString(args, 'action');
|
|
2409
|
-
if (!['publish', 're-share'].includes(action)) throw new Error('action must be publish or re-share');
|
|
2410
|
-
const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/${action}`, {}, { workspaceId });
|
|
2411
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2412
|
-
}
|
|
2413
|
-
case 'share_revoke': {
|
|
2414
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2415
|
-
const shareId = requiredShareId(args);
|
|
2416
|
-
const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/revoke`, {}, { workspaceId });
|
|
2417
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2418
|
-
}
|
|
2419
2989
|
case 'catalog_slice_read': {
|
|
2420
2990
|
const workspaceId = optionalWorkspaceId(args);
|
|
2421
2991
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
@@ -2429,33 +2999,33 @@ export function createTools(config) {
|
|
|
2429
2999
|
}
|
|
2430
3000
|
case 'catalog_slice_snapshot': {
|
|
2431
3001
|
const workspaceId = optionalWorkspaceId(args);
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
if (hasSavedSlice === hasLiveSlice) {
|
|
2435
|
-
throw new Error('provide exactly one snapshot source: sliceId, or shopDomain plus variantIds');
|
|
2436
|
-
}
|
|
3002
|
+
const branch = conditionalToolBranch(name, args);
|
|
3003
|
+
if (!branch) throw new Error('catalog_slice_snapshot branch selection did not match its declared input schema');
|
|
2437
3004
|
let response;
|
|
2438
|
-
if (
|
|
3005
|
+
if (branch.id === 'saved') {
|
|
2439
3006
|
const sliceId = requiredSavedSliceId(args);
|
|
2440
|
-
response = await fleetRequest(name, 'POST', `/catalog-slices/${encodeURIComponent(sliceId)}/snapshot`, {}, {
|
|
3007
|
+
response = await fleetRequest(name, 'POST', `/catalog-slices/${encodeURIComponent(sliceId)}/snapshot`, {}, {
|
|
3008
|
+
workspaceId, invocationMutates: branch.mutates,
|
|
3009
|
+
});
|
|
2441
3010
|
} else {
|
|
2442
3011
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
2443
3012
|
const variantIds = requiredCatalogVariantIds(args);
|
|
2444
3013
|
response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slice/snapshot`, {
|
|
2445
3014
|
variantIds,
|
|
2446
|
-
}, { workspaceId });
|
|
3015
|
+
}, { workspaceId, invocationMutates: branch.mutates });
|
|
2447
3016
|
}
|
|
2448
3017
|
return 'error' in response ? response.error : textResult(catalogSnapshotProjection(response.value));
|
|
2449
3018
|
}
|
|
2450
3019
|
case 'catalog_slices_saved': {
|
|
2451
3020
|
const workspaceId = optionalWorkspaceId(args);
|
|
2452
|
-
const
|
|
2453
|
-
if (!
|
|
2454
|
-
|
|
2455
|
-
}
|
|
3021
|
+
const branch = conditionalToolBranch(name, args);
|
|
3022
|
+
if (!branch) throw new Error('catalog_slices_saved branch selection did not match its declared input schema');
|
|
3023
|
+
const action = branch.action;
|
|
2456
3024
|
let response;
|
|
2457
3025
|
if (action === 'list') {
|
|
2458
|
-
response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
|
|
3026
|
+
response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
|
|
3027
|
+
workspaceId, invocationMutates: branch.mutates,
|
|
3028
|
+
});
|
|
2459
3029
|
} else if (action === 'save') {
|
|
2460
3030
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
2461
3031
|
const label = requiredString(args, 'label');
|
|
@@ -2463,14 +3033,20 @@ export function createTools(config) {
|
|
|
2463
3033
|
response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slices`, {
|
|
2464
3034
|
label,
|
|
2465
3035
|
variantIds: requiredCatalogVariantIds(args),
|
|
2466
|
-
}, { workspaceId });
|
|
3036
|
+
}, { workspaceId, invocationMutates: branch.mutates });
|
|
2467
3037
|
} else {
|
|
2468
3038
|
const sliceId = requiredSavedSliceId(args);
|
|
2469
3039
|
const suffix = action === 'refresh'
|
|
2470
3040
|
? '/refresh'
|
|
2471
3041
|
: action === 'changes' ? '/changes/latest' : '';
|
|
2472
3042
|
const method = action === 'delete' ? 'DELETE' : action === 'refresh' ? 'POST' : 'GET';
|
|
2473
|
-
response = await fleetRequest(
|
|
3043
|
+
response = await fleetRequest(
|
|
3044
|
+
name,
|
|
3045
|
+
method,
|
|
3046
|
+
`/catalog-slices/${encodeURIComponent(sliceId)}${suffix}`,
|
|
3047
|
+
action === 'refresh' ? {} : undefined,
|
|
3048
|
+
{ workspaceId, invocationMutates: branch.mutates },
|
|
3049
|
+
);
|
|
2474
3050
|
}
|
|
2475
3051
|
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2476
3052
|
}
|
|
@@ -2533,8 +3109,9 @@ export function createTools(config) {
|
|
|
2533
3109
|
case 'run_status': {
|
|
2534
3110
|
const runId = requiredString(args, 'runId');
|
|
2535
3111
|
if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
|
|
2536
|
-
const status = await
|
|
2537
|
-
|
|
3112
|
+
const status = await apiRaw('GET', `/runs/${encodeURIComponent(runId)}`, undefined, { cacheBustRead: true });
|
|
3113
|
+
if (!status.ok) return historyReadErrorResult(status);
|
|
3114
|
+
return textResult({ ...status.json, dashboard: dashboardLink(runId) });
|
|
2538
3115
|
}
|
|
2539
3116
|
case 'run_ledger': {
|
|
2540
3117
|
const runId = requiredString(args, 'runId');
|
|
@@ -2545,7 +3122,8 @@ export function createTools(config) {
|
|
|
2545
3122
|
case 'run_report': {
|
|
2546
3123
|
const runId = requiredString(args, 'runId');
|
|
2547
3124
|
if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
|
|
2548
|
-
|
|
3125
|
+
const report = await apiRaw('GET', `/runs/${encodeURIComponent(runId)}/report`);
|
|
3126
|
+
return report.ok ? textResult(report.json) : historyReadErrorResult(report);
|
|
2549
3127
|
}
|
|
2550
3128
|
case 'run_resume': {
|
|
2551
3129
|
const runId = requiredString(args, 'runId');
|
|
@@ -2593,9 +3171,9 @@ export function createTools(config) {
|
|
|
2593
3171
|
const teaching = isGateReceiptUnavailable(response.status, response.json)
|
|
2594
3172
|
? gateReceiptUnavailableTeaching(name, receiptId)
|
|
2595
3173
|
: undefined;
|
|
2596
|
-
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3174
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
|
|
2597
3175
|
}
|
|
2598
|
-
return
|
|
3176
|
+
return configuredReceiptGateResult(secretSafe(response.json), enabled ? 'configuration saved' : 'disabled');
|
|
2599
3177
|
}
|
|
2600
3178
|
case 'gate_evaluate': {
|
|
2601
3179
|
const receiptId = args.receiptId === undefined
|
|
@@ -2612,9 +3190,9 @@ export function createTools(config) {
|
|
|
2612
3190
|
const teaching = receiptId && isGateReceiptUnavailable(response.status, response.json)
|
|
2613
3191
|
? gateReceiptUnavailableTeaching(name, receiptId)
|
|
2614
3192
|
: undefined;
|
|
2615
|
-
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3193
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
|
|
2616
3194
|
}
|
|
2617
|
-
return
|
|
3195
|
+
return configuredReceiptGateResult(secretSafe(response.json), 'evaluation recorded');
|
|
2618
3196
|
}
|
|
2619
3197
|
case 'gate_verdict': {
|
|
2620
3198
|
const runId = args.runId === undefined ? '' : requiredString(args, 'runId');
|
|
@@ -2626,7 +3204,7 @@ export function createTools(config) {
|
|
|
2626
3204
|
if (!latest.ok) return practiceErrorResult(latest.status, latest.json, latest.retryAfterSeconds);
|
|
2627
3205
|
const evaluation = latest.json?.evaluations?.[0];
|
|
2628
3206
|
if (!evaluation) {
|
|
2629
|
-
throw new Error(
|
|
3207
|
+
throw new Error("No configured Receipt Gate exists. The receipt's recorded API behavior remains available. Finish a practice simulation run and keep its pa-* receiptId; call gate_configure({ receiptId, enabled: true }); call gate_evaluate({}); then call gate_verdict({}) again.");
|
|
2630
3208
|
}
|
|
2631
3209
|
const verdict = evaluation.gateVerdict;
|
|
2632
3210
|
if (!verdict || verdict.schemaVersion !== 'meguro.gate-verdict.v1') {
|
|
@@ -2634,9 +3212,9 @@ export function createTools(config) {
|
|
|
2634
3212
|
const recovery = receiptId
|
|
2635
3213
|
? `gate_evaluate({ receiptId: ${JSON.stringify(receiptId)} })`
|
|
2636
3214
|
: 'gate_evaluate({})';
|
|
2637
|
-
throw new Error(`The latest Receipt Gate row predates reproducible gate policy v1 evidence. Re-evaluate it with ${recovery}, then call gate_verdict({}) again; Meguro will not invent missing cited facts from historical compact status.`);
|
|
3215
|
+
throw new Error(`The latest Configured Receipt Gate row predates reproducible gate policy v1 evidence. Re-evaluate it with ${recovery}, then call gate_verdict({}) again; Meguro will not invent missing cited facts from historical compact status.`);
|
|
2638
3216
|
}
|
|
2639
|
-
return
|
|
3217
|
+
return configuredReceiptGateResult(secretSafe(verdict), 'verdict read');
|
|
2640
3218
|
}
|
|
2641
3219
|
const response = await practiceApi('GET', `/practice/evaluation-suites/${encodeURIComponent(runId)}`);
|
|
2642
3220
|
if (!response.ok && response.status === 404) return legacyGateArtifactNotFoundGuidance(runId);
|
|
@@ -2647,32 +3225,51 @@ export function createTools(config) {
|
|
|
2647
3225
|
}
|
|
2648
3226
|
// Exact pass-through is the parity contract: Console and MCP consume these same bytes as
|
|
2649
3227
|
// an object, with no local policy derivation in this registry.
|
|
2650
|
-
return
|
|
3228
|
+
return configuredReceiptGateResult(verdict, 'legacy verdict read');
|
|
2651
3229
|
}
|
|
2652
3230
|
case 'runs_list': {
|
|
2653
3231
|
const lifecycle = args.lifecycle === undefined ? undefined : requiredString(args, 'lifecycle');
|
|
2654
3232
|
if (lifecycle && !['running', 'paused', 'completed', 'failed', 'cleaning'].includes(lifecycle)) {
|
|
2655
3233
|
throw new Error('lifecycle must be running, paused, completed, failed, or cleaning');
|
|
2656
3234
|
}
|
|
3235
|
+
const query = new URLSearchParams({
|
|
3236
|
+
limit: '200',
|
|
3237
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
3238
|
+
});
|
|
2657
3239
|
return textResult(runsListProjection(
|
|
2658
|
-
await api('GET',
|
|
3240
|
+
await api('GET', `/runs?${query}`, undefined, { auth: true }),
|
|
2659
3241
|
lifecycle,
|
|
2660
3242
|
));
|
|
2661
3243
|
}
|
|
2662
|
-
case 'usage_read':
|
|
2663
|
-
|
|
3244
|
+
case 'usage_read': {
|
|
3245
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3246
|
+
const usage = await api('GET', '/account/usage', undefined, { auth: true });
|
|
3247
|
+
if (args.continueFromAttemptId !== undefined && args.storeId === undefined) {
|
|
3248
|
+
throw new Error('usage_read continueFromAttemptId requires storeId');
|
|
3249
|
+
}
|
|
3250
|
+
let eligibility = usage?.runStartEligibility;
|
|
3251
|
+
if (args.storeId !== undefined) {
|
|
3252
|
+
const storeId = validatedStoreId(requiredString(args, 'storeId'), 'storeId');
|
|
3253
|
+
const continueFromAttemptId = args.continueFromAttemptId === undefined
|
|
3254
|
+
? undefined
|
|
3255
|
+
: requiredPracticeAttemptId(args, 'continueFromAttemptId');
|
|
3256
|
+
const query = continueFromAttemptId
|
|
3257
|
+
? `?continueFromAttemptId=${encodeURIComponent(continueFromAttemptId)}`
|
|
3258
|
+
: '';
|
|
3259
|
+
const response = await practiceApi('GET', `/practice/stores/${encodeURIComponent(storeId)}/playbacks/start-eligibility${query}`, undefined, { workspaceId });
|
|
3260
|
+
if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
|
|
3261
|
+
eligibility = response.json;
|
|
3262
|
+
}
|
|
3263
|
+
return textResult(usageProjection(usage, eligibility));
|
|
3264
|
+
}
|
|
2664
3265
|
case 'twin_diff': {
|
|
2665
3266
|
const baselineRunId = requiredString(args, 'baselineRunId');
|
|
2666
3267
|
const treatedRunId = requiredString(args, 'treatedRunId');
|
|
2667
3268
|
if (PRACTICE_ATTEMPT_ID.test(baselineRunId) || PRACTICE_ATTEMPT_ID.test(treatedRunId)) {
|
|
2668
3269
|
return practiceAttemptComparisonGuidanceFor(name, baselineRunId, treatedRunId);
|
|
2669
3270
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
`/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`,
|
|
2673
|
-
undefined,
|
|
2674
|
-
{ auth: true },
|
|
2675
|
-
)));
|
|
3271
|
+
const response = await apiRaw('GET', `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`);
|
|
3272
|
+
return response.ok ? textResult(twinDiffProjection(response.json)) : historyReadErrorResult(response);
|
|
2676
3273
|
}
|
|
2677
3274
|
case 'exam_preflight': {
|
|
2678
3275
|
const attemptId = requiredPracticeAttemptId(args);
|
|
@@ -2710,6 +3307,12 @@ export function createTools(config) {
|
|
|
2710
3307
|
expectedRevision: revision,
|
|
2711
3308
|
commandId: `mcp-exam-execute-${examId}-${revision}`,
|
|
2712
3309
|
}, attemptId);
|
|
3310
|
+
} else if ((exam.status === 'completed' || exam.status === 'cleaning') && !executorAdvanced) {
|
|
3311
|
+
executorAdvanced = true;
|
|
3312
|
+
advanced = await examRequest(name, 'POST', `/practice-exams/${encodeURIComponent(examId)}/cleanup`, {
|
|
3313
|
+
expectedRevision: revision,
|
|
3314
|
+
commandId: `mcp-exam-cleanup-${examId}-${revision}`,
|
|
3315
|
+
}, attemptId);
|
|
2713
3316
|
} else {
|
|
2714
3317
|
break;
|
|
2715
3318
|
}
|
|
@@ -2730,18 +3333,14 @@ export function createTools(config) {
|
|
|
2730
3333
|
const response = await examRequest(name, 'GET', `/practice-exams/${encodeURIComponent(examId)}`);
|
|
2731
3334
|
if ('error' in response) return response.error;
|
|
2732
3335
|
const projected = examLifecycleProjection(response.value);
|
|
2733
|
-
if (
|
|
3336
|
+
if (projected.exam.status === 'completed' || projected.exam.status === 'cleaning') {
|
|
3337
|
+
return errorResult(`Shopify Exam ${examId} is not final; call exam_start({ attemptId: "${projected.exam.attemptId}", shopDomain: "${projected.exam.shopDomain}" }) to continue cleanup, then call exam_status before requesting its final report`);
|
|
3338
|
+
}
|
|
3339
|
+
if (!['blocked', 'failed', 'cleaned'].includes(projected.exam.status)) {
|
|
2734
3340
|
return errorResult(`Shopify Exam ${examId} is ${projected.exam.status}; call exam_status and continue bounded execution before requesting its immutable receipts`);
|
|
2735
3341
|
}
|
|
2736
3342
|
if (projected.receipts.retention?.status === 'expired') {
|
|
2737
|
-
|
|
2738
|
-
return errorResult(
|
|
2739
|
-
retention.tier === 'developer'
|
|
2740
|
-
? 'Free keeps receipts available for 7 days. Solo keeps receipts available for 90 days; Builder keeps them available for 365 days. Upgrade, then retry this receipt if its age fits the new window, or start a new run.'
|
|
2741
|
-
: retention.tier === 'solo'
|
|
2742
|
-
? 'Solo keeps receipts available for 90 days. Builder keeps receipts available for 365 days. Upgrade, then retry this receipt if its age fits the new window, or start a new run.'
|
|
2743
|
-
: 'Builder keeps receipts available for 365 days. Start a new run to mint a fresh receipt.',
|
|
2744
|
-
);
|
|
3343
|
+
return errorResult(projected.receipts.retention.expiredMessage);
|
|
2745
3344
|
}
|
|
2746
3345
|
if (!projected.receipts.privateAvailable || !projected.receipts.publicAvailable) {
|
|
2747
3346
|
return errorResult(`Shopify Exam ${examId} does not yet carry both immutable receipts`);
|
|
@@ -2764,6 +3363,7 @@ export function createTools(config) {
|
|
|
2764
3363
|
}
|
|
2765
3364
|
case 'practice_run_start': {
|
|
2766
3365
|
const storeId = requiredPracticeStoreId(args);
|
|
3366
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2767
3367
|
const body = {
|
|
2768
3368
|
purpose: 'external-agent',
|
|
2769
3369
|
...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
|
|
@@ -2771,15 +3371,26 @@ export function createTools(config) {
|
|
|
2771
3371
|
...(args.externalAgent !== undefined ? { externalAgent: args.externalAgent } : {}),
|
|
2772
3372
|
...(args.subject !== undefined ? { subject: args.subject } : {}),
|
|
2773
3373
|
...(args.observationPlan !== undefined ? { observationPlan: args.observationPlan } : {}),
|
|
3374
|
+
...(args.assertionPlan !== undefined ? { assertionPlan: args.assertionPlan } : {}),
|
|
2774
3375
|
...(args.graphqlThrottleMode !== undefined ? { graphqlThrottleMode: args.graphqlThrottleMode } : {}),
|
|
2775
3376
|
};
|
|
2776
|
-
const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body);
|
|
2777
|
-
if (!response.ok)
|
|
3377
|
+
const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body, { workspaceId });
|
|
3378
|
+
if (!response.ok) {
|
|
3379
|
+
const unavailable = isPracticeStoreUnavailable(response.status, response.json);
|
|
3380
|
+
return practiceErrorResult(
|
|
3381
|
+
response.status,
|
|
3382
|
+
response.json,
|
|
3383
|
+
response.retryAfterSeconds,
|
|
3384
|
+
unavailable ? practiceStoreUnavailableTeaching(storeId) : undefined,
|
|
3385
|
+
unavailable ? 'pre-commit' : undefined,
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
2778
3388
|
const started = response.json ?? {};
|
|
2779
3389
|
return textResult(secretSafe({
|
|
2780
3390
|
operation: 'start',
|
|
2781
3391
|
attemptId: started.attemptId ?? started.practiceRun?.attemptId,
|
|
2782
3392
|
...(started.segmentPlan ? { segmentPlan: started.segmentPlan } : {}),
|
|
3393
|
+
...(started.assertionPlan ? { assertionPlan: started.assertionPlan } : {}),
|
|
2783
3394
|
practiceRun: practiceRunProjection(started.practiceRun),
|
|
2784
3395
|
paths: {
|
|
2785
3396
|
state: started.statePath,
|
|
@@ -2799,16 +3410,23 @@ export function createTools(config) {
|
|
|
2799
3410
|
? undefined
|
|
2800
3411
|
: requiredPracticeAttemptId(args, 'receiptId');
|
|
2801
3412
|
if (receiptId) {
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
3413
|
+
const pointPath = `/practice/playbacks/${encodeURIComponent(receiptId)}/state`;
|
|
3414
|
+
const point = await apiRaw('GET', pointPath, undefined, { workspaceId });
|
|
3415
|
+
if (!point.ok) {
|
|
3416
|
+
const authenticationError = authenticationErrorResult(point.status, point.json);
|
|
3417
|
+
if (authenticationError) return authenticationError;
|
|
3418
|
+
if (point.status === 404) return practiceRunReceiptSelectionError(receiptId);
|
|
3419
|
+
return fleetErrorResult(name, pointPath, point.status, point.json, point.requestId);
|
|
3420
|
+
}
|
|
3421
|
+
const referenced = point.json ?? {};
|
|
2807
3422
|
const receiptStoreId = String(referenced.storeId ?? referenced.worldId ?? '');
|
|
2808
3423
|
if (requestedStoreId && requestedStoreId !== receiptStoreId) {
|
|
2809
3424
|
return practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId);
|
|
2810
3425
|
}
|
|
2811
|
-
|
|
3426
|
+
const listPath = queryPath('/practice/playbacks', { worldId: receiptStoreId });
|
|
3427
|
+
const listed = await fleetRequest(name, 'GET', listPath, undefined, { workspaceId });
|
|
3428
|
+
if ('error' in listed) return listed.error;
|
|
3429
|
+
return textResult(practiceRunsProjection(listed.value, receiptStoreId, 'receipt-derived', workspaceId));
|
|
2812
3430
|
}
|
|
2813
3431
|
|
|
2814
3432
|
const fleet = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
|
|
@@ -2832,12 +3450,19 @@ export function createTools(config) {
|
|
|
2832
3450
|
listed.value,
|
|
2833
3451
|
selectedStoreId,
|
|
2834
3452
|
requestedStoreId ? 'explicit-store-id' : 'single-store',
|
|
3453
|
+
workspaceId,
|
|
2835
3454
|
));
|
|
2836
3455
|
}
|
|
2837
3456
|
case 'practice_run_status': {
|
|
2838
3457
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2839
|
-
const
|
|
2840
|
-
|
|
3458
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3459
|
+
const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`, undefined, { workspaceId });
|
|
3460
|
+
if (!response.ok) {
|
|
3461
|
+
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
3462
|
+
? practiceRunUnavailableTeaching(name, attemptId)
|
|
3463
|
+
: undefined;
|
|
3464
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3465
|
+
}
|
|
2841
3466
|
const state = response.json ?? {};
|
|
2842
3467
|
return textResult(secretSafe({
|
|
2843
3468
|
operation: 'status',
|
|
@@ -2848,7 +3473,8 @@ export function createTools(config) {
|
|
|
2848
3473
|
}
|
|
2849
3474
|
case 'practice_run_checkpoint': {
|
|
2850
3475
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2851
|
-
const
|
|
3476
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3477
|
+
const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {}, { workspaceId });
|
|
2852
3478
|
if (!response.ok) {
|
|
2853
3479
|
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
2854
3480
|
? practiceRunUnavailableTeaching(name, attemptId)
|
|
@@ -2859,23 +3485,31 @@ export function createTools(config) {
|
|
|
2859
3485
|
operation: 'checkpoint',
|
|
2860
3486
|
attemptId,
|
|
2861
3487
|
alreadyCaptured: response.json?.alreadyCaptured === true,
|
|
3488
|
+
advanceCursor: response.json?.advanceCursor,
|
|
2862
3489
|
checkpoint: checkpointProjection(response.json?.checkpoint),
|
|
2863
3490
|
...consoleUrlFields(response.json?.consoleRef, response.json?.consoleRefExpiresAt),
|
|
2864
3491
|
}));
|
|
2865
3492
|
}
|
|
2866
3493
|
case 'practice_run_advance': {
|
|
2867
3494
|
const attemptId = requiredPracticeAttemptId(args);
|
|
3495
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2868
3496
|
const hasDays = args.days !== undefined;
|
|
2869
3497
|
const hasUntil = args.until !== undefined;
|
|
2870
3498
|
if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
|
|
2871
|
-
const
|
|
2872
|
-
const expectedCallSeq = requiredNonNegativeInteger(args, 'expectedCallSeq', -1);
|
|
3499
|
+
const advanceCursor = args.advanceCursor;
|
|
2873
3500
|
if (hasDays && (!Number.isSafeInteger(args.days) || args.days < 1)) throw new Error('days must be a positive integer');
|
|
2874
3501
|
if (hasUntil && (!args.until || typeof args.until !== 'object' || Array.isArray(args.until))) throw new Error('until must be an object');
|
|
2875
3502
|
const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/advance`, {
|
|
2876
|
-
...(hasDays ? { days: args.days } : { until: args.until }),
|
|
2877
|
-
|
|
2878
|
-
|
|
3503
|
+
...(hasDays ? { days: args.days } : { until: args.until }),
|
|
3504
|
+
advanceCursor,
|
|
3505
|
+
...(args.confirmConclusion === true ? { confirmConclusion: true } : {}),
|
|
3506
|
+
}, { workspaceId });
|
|
3507
|
+
if (!response.ok) {
|
|
3508
|
+
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
3509
|
+
? practiceRunUnavailableTeaching(name, attemptId)
|
|
3510
|
+
: undefined;
|
|
3511
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3512
|
+
}
|
|
2879
3513
|
const advanced = response.json ?? {};
|
|
2880
3514
|
return textResult(secretSafe({
|
|
2881
3515
|
operation: 'advance',
|
|
@@ -2890,7 +3524,13 @@ export function createTools(config) {
|
|
|
2890
3524
|
}
|
|
2891
3525
|
case 'practice_run_finish': {
|
|
2892
3526
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2893
|
-
const
|
|
3527
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3528
|
+
const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {
|
|
3529
|
+
...(args.permanentStop === true ? { permanentStop: true } : {}),
|
|
3530
|
+
...(args.acknowledgeIncompleteOnboardingEvaluation === true
|
|
3531
|
+
? { acknowledgeIncompleteOnboardingEvaluation: true }
|
|
3532
|
+
: {}),
|
|
3533
|
+
}, { workspaceId });
|
|
2894
3534
|
if (!response.ok) {
|
|
2895
3535
|
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
2896
3536
|
? practiceRunUnavailableTeaching(name, attemptId)
|
|
@@ -2906,8 +3546,14 @@ export function createTools(config) {
|
|
|
2906
3546
|
}
|
|
2907
3547
|
case 'practice_run_report': {
|
|
2908
3548
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2909
|
-
const
|
|
2910
|
-
|
|
3549
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3550
|
+
const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`, undefined, { workspaceId });
|
|
3551
|
+
if (!response.ok) {
|
|
3552
|
+
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
3553
|
+
? practiceRunUnavailableTeaching(name, attemptId)
|
|
3554
|
+
: undefined;
|
|
3555
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3556
|
+
}
|
|
2911
3557
|
return textResult({
|
|
2912
3558
|
operation: 'report',
|
|
2913
3559
|
...receiptProjection(response.json ?? {}),
|
|
@@ -2916,17 +3562,24 @@ export function createTools(config) {
|
|
|
2916
3562
|
}
|
|
2917
3563
|
case 'practice_run_impact': {
|
|
2918
3564
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2919
|
-
const
|
|
2920
|
-
|
|
3565
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3566
|
+
const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`, undefined, { workspaceId });
|
|
3567
|
+
if (!response.ok) {
|
|
3568
|
+
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
3569
|
+
? practiceRunUnavailableTeaching(name, attemptId)
|
|
3570
|
+
: undefined;
|
|
3571
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3572
|
+
}
|
|
2921
3573
|
// T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim
|
|
2922
3574
|
// (no array truncation) rather than re-projecting.
|
|
2923
3575
|
return textResult({ operation: 'impact', ...(response.json ?? {}) });
|
|
2924
3576
|
}
|
|
2925
3577
|
case 'get_connection_details': {
|
|
2926
3578
|
const requestedStoreId = requiredPracticeStoreId(args);
|
|
3579
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2927
3580
|
let details;
|
|
2928
3581
|
try {
|
|
2929
|
-
details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true });
|
|
3582
|
+
details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true, workspaceId });
|
|
2930
3583
|
} catch (error) {
|
|
2931
3584
|
if (error instanceof ToolResultError) throw error;
|
|
2932
3585
|
if (error instanceof Error && error.message.startsWith('404 ')) {
|
|
@@ -2934,8 +3587,8 @@ export function createTools(config) {
|
|
|
2934
3587
|
}
|
|
2935
3588
|
throw error;
|
|
2936
3589
|
}
|
|
2937
|
-
const storeId = details.storeId ??
|
|
2938
|
-
const shopDomain = details.host ?? `${
|
|
3590
|
+
const storeId = details.storeId ?? requestedStoreId;
|
|
3591
|
+
const shopDomain = details.host ?? `${requestedStoreId}.meguro.io`;
|
|
2939
3592
|
return textResult({
|
|
2940
3593
|
identity: details.identity,
|
|
2941
3594
|
// Canonical practice-store id first: pass it straight into practice_run_start({ storeId }).
|
|
@@ -2947,6 +3600,7 @@ export function createTools(config) {
|
|
|
2947
3600
|
adminApiVersions: details.adminApiVersions,
|
|
2948
3601
|
adminVersions: details.admin?.versions,
|
|
2949
3602
|
adminVersionPolicy: details.adminVersionPolicy ?? details.admin?.supportPolicy,
|
|
3603
|
+
adminExecutionGuide: details.adminExecutionGuide,
|
|
2950
3604
|
env: {
|
|
2951
3605
|
SHOPIFY_ADMIN_GRAPHQL_URL: details.adminUrl,
|
|
2952
3606
|
SHOPIFY_ADMIN_ACCESS_TOKEN: details.token,
|
|
@@ -2956,6 +3610,7 @@ export function createTools(config) {
|
|
|
2956
3610
|
}
|
|
2957
3611
|
case 'admin_probe': {
|
|
2958
3612
|
const worldId = requiredWorldId(args);
|
|
3613
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2959
3614
|
const body = {
|
|
2960
3615
|
query: typeof args.query === 'string' ? args.query : '',
|
|
2961
3616
|
...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
|
|
@@ -2965,7 +3620,7 @@ export function createTools(config) {
|
|
|
2965
3620
|
// MEG-34: always ask the server to mint a durable Console reference for a successful probe.
|
|
2966
3621
|
createConsoleReference: true,
|
|
2967
3622
|
};
|
|
2968
|
-
const { ok, status, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body);
|
|
3623
|
+
const { ok, status, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body, { workspaceId });
|
|
2969
3624
|
if (ok) {
|
|
2970
3625
|
// Replace the internal reference id with a ready-to-open Console URL (opaque ref only).
|
|
2971
3626
|
const { consoleRef, consoleRefExpiresAt, ...envelope } = json;
|
|
@@ -2978,15 +3633,27 @@ export function createTools(config) {
|
|
|
2978
3633
|
return { content: [{ type: 'text', text: JSON.stringify(secretSafe(json), null, 2) }], isError: true };
|
|
2979
3634
|
}
|
|
2980
3635
|
case 'admin_schema': {
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3636
|
+
// MEG-1164: two exclusive modes on one tool. A submitted `query` asks what refusal the
|
|
3637
|
+
// served Admin surface would return for that document; `lookup` + `name` is the unchanged
|
|
3638
|
+
// schema lookup. Neither executes an Admin operation or touches practice-store state.
|
|
3639
|
+
const body = args.query !== undefined
|
|
3640
|
+
? {
|
|
3641
|
+
query: requiredString(args, 'query'),
|
|
3642
|
+
...(args.variables !== undefined ? { variables: args.variables } : {}),
|
|
3643
|
+
...(args.operationName !== undefined ? { operationName: args.operationName } : {}),
|
|
3644
|
+
...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
|
|
3645
|
+
}
|
|
3646
|
+
: {
|
|
3647
|
+
lookup: requiredString(args, 'lookup'),
|
|
3648
|
+
name: requiredString(args, 'name'),
|
|
3649
|
+
...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
|
|
3650
|
+
};
|
|
2988
3651
|
return textResult(await api('POST', '/practice/admin-schema', body, { auth: true }));
|
|
2989
3652
|
}
|
|
3653
|
+
case 'admin_recipes_list':
|
|
3654
|
+
return textResult(await api('GET', '/practice/admin-recipes', undefined, { auth: true }));
|
|
3655
|
+
case 'plan_validate':
|
|
3656
|
+
return textResult(await api('POST', '/practice/plan-validate', { operations: args.operations }, { auth: true }));
|
|
2990
3657
|
default:
|
|
2991
3658
|
return errorResult(`Unknown tool: ${name}`);
|
|
2992
3659
|
}
|
|
@@ -2999,24 +3666,32 @@ export function createTools(config) {
|
|
|
2999
3666
|
meaning: 'The fleet tool arguments were invalid or the request could not be sent.',
|
|
3000
3667
|
nextStep: 'Correct the arguments shown by the tool schema and retry.',
|
|
3001
3668
|
}],
|
|
3002
|
-
});
|
|
3669
|
+
}, undefined, CONDITIONAL_TOOL_BRANCHES.has(name) ? toolInvocationMutates(name, args) : undefined);
|
|
3003
3670
|
}
|
|
3004
|
-
|
|
3671
|
+
const failed = errorResult(error instanceof Error ? error.message : String(error));
|
|
3672
|
+
return typeof error?.operationCommitBoundary === 'string'
|
|
3673
|
+
? withOperationCommitBoundary(failed, error.operationCommitBoundary)
|
|
3674
|
+
: failed;
|
|
3005
3675
|
}
|
|
3006
3676
|
}
|
|
3007
3677
|
|
|
3008
3678
|
async function call(name, args = {}) {
|
|
3009
|
-
const mutates =
|
|
3679
|
+
const mutates = toolInvocationMutates(name, args);
|
|
3010
3680
|
try {
|
|
3011
3681
|
validateTopLevelArguments(name, args);
|
|
3012
3682
|
} catch (error) {
|
|
3683
|
+
if ((name === 'workspace_archive' || name === 'workspace_unarchive')
|
|
3684
|
+
&& error instanceof Error
|
|
3685
|
+
&& error.message.startsWith('workspaceId')) {
|
|
3686
|
+
return workspaceLifecycleIdentityGuidance(name, args?.workspaceId, error.message);
|
|
3687
|
+
}
|
|
3013
3688
|
const result = errorResult(error instanceof Error ? error.message : String(error));
|
|
3014
3689
|
return mutates ? withOperationCommitBoundary(result, 'pre-commit') : result;
|
|
3015
3690
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3691
|
+
// MEG-1054: no generic derivation here. A failed mutation carries only the classification the
|
|
3692
|
+
// control plane declared for it; an undeclared outcome falls through to the single `unknown`
|
|
3693
|
+
// default at the resource boundary rather than being asserted twice.
|
|
3694
|
+
return dispatch(name, args);
|
|
3020
3695
|
}
|
|
3021
3696
|
|
|
3022
3697
|
return { definitions, call };
|