meguro-mcp 0.2.12 → 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 +7 -0
- package/README.md +25 -19
- package/package.json +1 -1
- package/scripts/generate-readme-tools.mjs +67 -12
- package/src/docs.mjs +2205 -19
- package/src/protocol.mjs +6 -0
- package/src/server.mjs +1 -1
- package/src/tools.mjs +1053 -488
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 },
|
|
@@ -81,12 +78,22 @@ const TOOL_PRESENTATION = Object.freeze({
|
|
|
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 },
|
|
83
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 },
|
|
84
82
|
});
|
|
85
83
|
|
|
86
84
|
function textResult(value) {
|
|
87
85
|
return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
|
|
88
86
|
}
|
|
89
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
|
+
|
|
90
97
|
function errorResult(message) {
|
|
91
98
|
return { content: [{ type: 'text', text: scrubString(message) }], isError: true };
|
|
92
99
|
}
|
|
@@ -109,19 +116,60 @@ export function redactSecrets(value) {
|
|
|
109
116
|
return scrubString(value);
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
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) {
|
|
113
127
|
if (depth > 12) return '[TRUNCATED]';
|
|
114
|
-
if (typeof value === 'string')
|
|
115
|
-
|
|
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
|
+
}
|
|
116
148
|
if (value && typeof value === 'object') {
|
|
117
|
-
|
|
118
|
-
.filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key))
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
]));
|
|
121
159
|
}
|
|
122
160
|
return value;
|
|
123
161
|
}
|
|
124
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
|
+
|
|
125
173
|
function authenticationErrorResult(status, value) {
|
|
126
174
|
if (status !== 401 && status !== 403) return null;
|
|
127
175
|
const body = value && typeof value === 'object' ? value : {};
|
|
@@ -177,8 +225,55 @@ function catalogSnapshotProjection(value) {
|
|
|
177
225
|
: safe;
|
|
178
226
|
}
|
|
179
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
|
+
*/
|
|
180
273
|
const STORE_TEMPLATE_FIELDS = Object.freeze([
|
|
181
274
|
'key',
|
|
275
|
+
'revision',
|
|
276
|
+
'evaluationContext',
|
|
182
277
|
'label',
|
|
183
278
|
'category',
|
|
184
279
|
'recommended',
|
|
@@ -193,30 +288,60 @@ const STORE_TEMPLATE_FIELDS = Object.freeze([
|
|
|
193
288
|
'extendedRunDays',
|
|
194
289
|
'whatAgentGets',
|
|
195
290
|
'supportedReads',
|
|
291
|
+
'modeledEvidence',
|
|
196
292
|
'liveRun',
|
|
197
293
|
'goodFirstAgents',
|
|
198
294
|
'supportedWrites',
|
|
199
295
|
'operationContracts',
|
|
200
296
|
'knownUnsupported',
|
|
297
|
+
'unsupportedContracts',
|
|
298
|
+
'capabilityBoundaries',
|
|
299
|
+
'modeledPhysics',
|
|
201
300
|
'goodAgentShould',
|
|
202
301
|
'avoid',
|
|
203
302
|
'reportGrades',
|
|
204
303
|
]);
|
|
205
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
|
+
|
|
206
321
|
function storeTemplatesProjection(value) {
|
|
207
322
|
if (!Array.isArray(value?.storeTemplates)) {
|
|
208
323
|
throw new Error('Meguro template catalog response did not contain storeTemplates');
|
|
209
324
|
}
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
340
|
const declaredStarterBasis = typeof value?.starterRecommendation?.basis === 'string'
|
|
214
341
|
&& /^[a-z0-9-]{1,64}$/u.test(value.starterRecommendation.basis)
|
|
215
342
|
? value.starterRecommendation.basis
|
|
216
343
|
: 'starter-for-first-simulation';
|
|
217
|
-
const
|
|
218
|
-
let projectedStarterPlan;
|
|
219
|
-
if (starterPlan !== undefined) {
|
|
344
|
+
const projectStarterPlan = (starterPlan) => {
|
|
220
345
|
const positiveInteger = (candidate) => Number.isSafeInteger(candidate) && candidate > 0;
|
|
221
346
|
const nonNegativeInteger = (candidate) => Number.isSafeInteger(candidate) && candidate >= 0;
|
|
222
347
|
const valid = starterPlan
|
|
@@ -225,7 +350,7 @@ function storeTemplatesProjection(value) {
|
|
|
225
350
|
&& starterPlan.schemaVersion === 'meguro.practice-run-starter-plan.v1'
|
|
226
351
|
&& ['developer', 'solo', 'builder'].includes(starterPlan.tier)
|
|
227
352
|
&& typeof starterPlan.templateKey === 'string'
|
|
228
|
-
&&
|
|
353
|
+
&& catalogTemplateKeys.includes(starterPlan.templateKey)
|
|
229
354
|
&& positiveInteger(starterPlan.idealRunDays)
|
|
230
355
|
&& positiveInteger(starterPlan.scenarioArcDays)
|
|
231
356
|
&& positiveInteger(starterPlan.firstSegmentDays)
|
|
@@ -238,57 +363,129 @@ function storeTemplatesProjection(value) {
|
|
|
238
363
|
&& typeof starterPlan.continuation === 'object'
|
|
239
364
|
&& typeof starterPlan.continuation.available === 'boolean'
|
|
240
365
|
&& nonNegativeInteger(starterPlan.continuation.remainingArcDaysAfterFirstSegment)
|
|
366
|
+
&& (starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment === undefined
|
|
367
|
+
|| nonNegativeInteger(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment))
|
|
241
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
|
+
))
|
|
242
374
|
&& starterPlan.usage
|
|
243
375
|
&& typeof starterPlan.usage === 'object'
|
|
244
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
|
+
))
|
|
245
387
|
&& nonNegativeInteger(starterPlan.usage.remainingSimulationRuns);
|
|
246
388
|
if (!valid) throw new Error('Meguro template catalog contained an invalid starterPlan');
|
|
247
|
-
|
|
389
|
+
return {
|
|
248
390
|
schemaVersion: starterPlan.schemaVersion,
|
|
249
391
|
tier: starterPlan.tier,
|
|
250
392
|
templateKey: starterPlan.templateKey,
|
|
251
393
|
idealRunDays: starterPlan.idealRunDays,
|
|
252
394
|
scenarioArcDays: starterPlan.scenarioArcDays,
|
|
253
395
|
firstSegmentDays: starterPlan.firstSegmentDays,
|
|
396
|
+
...(starterPlan.authorization ? { authorization: {
|
|
397
|
+
source: starterPlan.authorization.source,
|
|
398
|
+
idealHorizonDays: starterPlan.authorization.idealHorizonDays,
|
|
399
|
+
} } : {}),
|
|
254
400
|
practiceRunStart: { clock: { simulationDays: starterPlan.practiceRunStart.clock.simulationDays } },
|
|
255
401
|
continuation: {
|
|
256
402
|
available: starterPlan.continuation.available,
|
|
257
403
|
remainingArcDaysAfterFirstSegment: starterPlan.continuation.remainingArcDaysAfterFirstSegment,
|
|
404
|
+
...(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment !== undefined
|
|
405
|
+
? { remainingAuthorizedDaysAfterFirstSegment: starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment }
|
|
406
|
+
: {}),
|
|
258
407
|
maxCanvasWorldDays: starterPlan.continuation.maxCanvasWorldDays,
|
|
259
408
|
},
|
|
260
409
|
usage: {
|
|
261
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
|
+
} : {}),
|
|
262
421
|
remainingSimulationRuns: starterPlan.usage.remainingSimulationRuns,
|
|
263
422
|
},
|
|
264
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;
|
|
265
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;
|
|
266
465
|
return {
|
|
267
466
|
starterRecommendation: {
|
|
268
467
|
basis: declaredStarterBasis,
|
|
269
468
|
templateKeys: starterTemplateKeys,
|
|
270
469
|
...(projectedStarterPlan ? { starterPlan: projectedStarterPlan } : {}),
|
|
470
|
+
...(projectedStarterPlans ? { starterPlans: projectedStarterPlans } : {}),
|
|
471
|
+
...(recommendationUnavailable ? { recommendationUnavailable } : {}),
|
|
271
472
|
},
|
|
272
|
-
storeTemplates
|
|
273
|
-
if (!template || typeof template !== 'object' || Array.isArray(template)) {
|
|
274
|
-
throw new Error('Meguro template catalog contained an invalid template entry');
|
|
275
|
-
}
|
|
276
|
-
return Object.fromEntries(STORE_TEMPLATE_FIELDS
|
|
277
|
-
.filter((field) => Object.hasOwn(template, field))
|
|
278
|
-
.map((field) => [field, secretSafe(template[field], 1)]));
|
|
279
|
-
}),
|
|
473
|
+
storeTemplates,
|
|
280
474
|
};
|
|
281
475
|
}
|
|
282
476
|
|
|
283
|
-
function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
|
|
477
|
+
function practiceErrorResult(status, value, retryAfterSeconds, teaching, declaredBoundary) {
|
|
284
478
|
const authenticationError = authenticationErrorResult(status, value);
|
|
285
479
|
if (authenticationError) return authenticationError;
|
|
286
480
|
const body = value && typeof value === 'object' ? value : {};
|
|
287
|
-
const stable = { httpStatus: status };
|
|
481
|
+
const stable = { httpStatus: status, ...enforcementProjection(body) };
|
|
288
482
|
if (body.code === 'terminal-advance-confirmation-required') {
|
|
289
483
|
stable.code = body.code;
|
|
290
484
|
stable.terminalAdvance = secretSafe(body.terminalAdvance);
|
|
291
485
|
}
|
|
486
|
+
if (body.advanceCursor && typeof body.advanceCursor === 'object') {
|
|
487
|
+
stable.advanceCursor = secretSafe(body.advanceCursor);
|
|
488
|
+
}
|
|
292
489
|
if (['requested-exceeds-cap', 'continuation-unavailable', 'limit-reached'].includes(body.reason)) {
|
|
293
490
|
stable.reason = body.reason;
|
|
294
491
|
}
|
|
@@ -309,7 +506,16 @@ function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
|
|
|
309
506
|
else if (Number.isFinite(bodyRetryAfter) && bodyRetryAfter >= 0) stable.retryAfterSeconds = bodyRetryAfter;
|
|
310
507
|
if (typeof body.retryable === 'boolean') stable.retryable = body.retryable;
|
|
311
508
|
if (teaching) stable.teaching = secretSafe(teaching);
|
|
312
|
-
|
|
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;
|
|
313
519
|
}
|
|
314
520
|
|
|
315
521
|
function practiceRunUnavailableTeaching(toolName, attemptId) {
|
|
@@ -322,22 +528,30 @@ function practiceRunUnavailableTeaching(toolName, attemptId) {
|
|
|
322
528
|
|
|
323
529
|
function gateReceiptUnavailableTeaching(toolName, receiptId) {
|
|
324
530
|
return {
|
|
325
|
-
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.`,
|
|
326
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>" }).`,
|
|
327
|
-
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.`,
|
|
328
542
|
};
|
|
329
543
|
}
|
|
330
544
|
|
|
331
545
|
function isPracticeRunUnavailable(status, value) {
|
|
332
|
-
return status === 404
|
|
546
|
+
return status === 404;
|
|
333
547
|
}
|
|
334
548
|
|
|
335
549
|
function isGateReceiptUnavailable(status, value) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return
|
|
550
|
+
return status === 404;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isPracticeStoreUnavailable(status, value) {
|
|
554
|
+
return status === 404;
|
|
341
555
|
}
|
|
342
556
|
|
|
343
557
|
function requiredString(args, key) {
|
|
@@ -367,11 +581,11 @@ function requiredWorkspaceId(args) {
|
|
|
367
581
|
return workspaceId;
|
|
368
582
|
}
|
|
369
583
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
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
|
+
});
|
|
375
589
|
|
|
376
590
|
function optionalStringList(args, key, options = {}) {
|
|
377
591
|
const value = args?.[key];
|
|
@@ -407,6 +621,115 @@ function requiredSavedSliceId(args) {
|
|
|
407
621
|
return sliceId;
|
|
408
622
|
}
|
|
409
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
|
+
|
|
410
733
|
const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
|
|
411
734
|
type: 'object',
|
|
412
735
|
additionalProperties: false,
|
|
@@ -422,26 +745,6 @@ const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
|
|
|
422
745
|
required: ['schemaVersion', 'publicReceiptId', 'kind', 'revision', 'issuedAt', 'sha256', 'canonicalPath'],
|
|
423
746
|
});
|
|
424
747
|
|
|
425
|
-
const SHARE_ARTIFACT_REF_INPUT_SCHEMA = Object.freeze({
|
|
426
|
-
oneOf: [
|
|
427
|
-
PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA,
|
|
428
|
-
{ type: 'object', additionalProperties: false, properties: { attemptId: { type: 'string', minLength: 1 } }, required: ['attemptId'] },
|
|
429
|
-
{ type: 'object', additionalProperties: false, properties: { runId: { type: 'string', minLength: 1 } }, required: ['runId'] },
|
|
430
|
-
{ type: 'object', additionalProperties: false, properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$' } }, required: ['examId'] },
|
|
431
|
-
{
|
|
432
|
-
type: 'object',
|
|
433
|
-
additionalProperties: false,
|
|
434
|
-
properties: {
|
|
435
|
-
probeSetRunId: { type: 'string', minLength: 1 },
|
|
436
|
-
evaluationId: { type: 'string', minLength: 1 },
|
|
437
|
-
visibility: { type: 'string', const: 'public' },
|
|
438
|
-
},
|
|
439
|
-
required: ['probeSetRunId', 'evaluationId', 'visibility'],
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
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"}.',
|
|
443
|
-
});
|
|
444
|
-
|
|
445
748
|
const PRACTICE_STORE_ID_INPUT_SCHEMA = Object.freeze({
|
|
446
749
|
type: 'string',
|
|
447
750
|
minLength: 1,
|
|
@@ -511,8 +814,7 @@ const PRACTICE_RUN_EXTERNAL_AGENT_INPUT_SCHEMA = Object.freeze({
|
|
|
511
814
|
name: { type: 'string', maxLength: 80 },
|
|
512
815
|
source: { type: 'string', maxLength: 80 },
|
|
513
816
|
version: { type: 'string', maxLength: 80 },
|
|
514
|
-
commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.
|
|
515
|
-
commit: { type: 'string', maxLength: 80, description: 'Legacy alias for commitSha.' },
|
|
817
|
+
commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.' },
|
|
516
818
|
testCommand: { type: 'string', maxLength: 300 },
|
|
517
819
|
ciStatus: { type: 'string', enum: ['unknown', 'passed', 'failed'] },
|
|
518
820
|
examRole: { type: 'string', enum: ['candidate', 'negative-control'] },
|
|
@@ -559,10 +861,27 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
|
|
|
559
861
|
required: ['schemaVersion', 'maxDays', 'condition'],
|
|
560
862
|
});
|
|
561
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
|
+
|
|
562
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',
|
|
563
880
|
'get_connection_details',
|
|
881
|
+
'workspace_create',
|
|
564
882
|
'practice_run_start',
|
|
565
883
|
'practice_run_advance',
|
|
884
|
+
...CONDITIONAL_TOOL_BRANCHES.keys(),
|
|
566
885
|
]);
|
|
567
886
|
|
|
568
887
|
function schemaValidationError(schema, value, path = 'arguments') {
|
|
@@ -627,18 +946,32 @@ function schemaValidationError(schema, value, path = 'arguments') {
|
|
|
627
946
|
return null;
|
|
628
947
|
}
|
|
629
948
|
|
|
630
|
-
|
|
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
|
+
}
|
|
631
955
|
|
|
632
|
-
function
|
|
633
|
-
const
|
|
634
|
-
if (!
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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)})`;
|
|
642
975
|
}
|
|
643
976
|
|
|
644
977
|
function queryPath(path, values) {
|
|
@@ -654,17 +987,8 @@ function requiredWorldId(args) {
|
|
|
654
987
|
return validatedStoreId(requiredString(args, 'worldId'), 'worldId');
|
|
655
988
|
}
|
|
656
989
|
|
|
657
|
-
// MEG-256: `storeId` is the one canonical public practice-store identifier across the assistant
|
|
658
|
-
// workflow (connection discovery → run start → receipt). `worldId` names the same identifier and
|
|
659
|
-
// stays accepted as a bounded legacy alias so existing callers keep working.
|
|
660
990
|
function requiredPracticeStoreId(args) {
|
|
661
|
-
|
|
662
|
-
if (provided.length === 0) throw new Error('storeId is required (worldId is accepted as a legacy alias for the same practice-store id)');
|
|
663
|
-
const canonical = requiredString(args, provided[0]);
|
|
664
|
-
if (provided.length === 2 && requiredString(args, provided[1]) !== canonical) {
|
|
665
|
-
throw new Error('storeId and worldId name the same practice store; they were both provided with different values');
|
|
666
|
-
}
|
|
667
|
-
return validatedStoreId(canonical, provided[0]);
|
|
991
|
+
return validatedStoreId(requiredString(args, 'storeId'), 'storeId');
|
|
668
992
|
}
|
|
669
993
|
|
|
670
994
|
function requiredPracticeAttemptId(args, key = 'attemptId') {
|
|
@@ -787,54 +1111,17 @@ function receiptProjection(value) {
|
|
|
787
1111
|
const practiceRun = attempt.practiceRun ?? value?.practiceRun ?? null;
|
|
788
1112
|
const calls = Array.isArray(value?.events?.calls) ? value.events.calls : [];
|
|
789
1113
|
const actions = Array.isArray(value?.events?.actions) ? value.events.actions : [];
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
status: assertion.status,
|
|
802
|
-
label: assertion.label,
|
|
803
|
-
...(assertion?.category === 'expected-refusal' ? { detail: assertion.detail } : {}),
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
if (webhookCapability?.availability === 'available') {
|
|
807
|
-
return { id: assertion.id, status: assertion.status, label: assertion.label, detail: assertion.detail };
|
|
808
|
-
}
|
|
809
|
-
if (webhookCapability?.availability === 'unavailable'
|
|
810
|
-
&& webhookCapability.statusLabel === 'not available in this run mode'
|
|
811
|
-
&& typeof webhookCapability.detail === 'string'
|
|
812
|
-
&& webhookCapability.detail.trim()) {
|
|
813
|
-
return {
|
|
814
|
-
id: assertion.id,
|
|
815
|
-
status: webhookCapability.statusLabel,
|
|
816
|
-
label: assertion.label,
|
|
817
|
-
detail: webhookCapability.detail,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
if (webhookCapability?.availability === 'unknown'
|
|
821
|
-
&& webhookCapability.statusLabel === 'availability unknown'
|
|
822
|
-
&& typeof webhookCapability.detail === 'string'
|
|
823
|
-
&& webhookCapability.detail.trim()) {
|
|
824
|
-
return {
|
|
825
|
-
id: assertion.id,
|
|
826
|
-
status: webhookCapability.statusLabel,
|
|
827
|
-
label: assertion.label,
|
|
828
|
-
detail: webhookCapability.detail,
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
return {
|
|
832
|
-
id: assertion.id,
|
|
833
|
-
status: 'availability unknown',
|
|
834
|
-
label: assertion.label,
|
|
835
|
-
detail: 'Webhook assertion availability is unknown because this receipt has no capability metadata.',
|
|
836
|
-
};
|
|
837
|
-
})
|
|
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
|
+
}))
|
|
838
1125
|
: [];
|
|
839
1126
|
const verdict = value?.report?.verdict && typeof value.report.verdict === 'object'
|
|
840
1127
|
? Object.fromEntries(['status', 'outcome', 'grade', 'score', 'headline', 'horizonDays']
|
|
@@ -879,6 +1166,11 @@ function receiptProjection(value) {
|
|
|
879
1166
|
const footprint = m?.footprint ?? null;
|
|
880
1167
|
const defaultVerdict = value?.defaultVerdict ?? value?.report?.defaultVerdict ?? value?.attempt?.defaultVerdict;
|
|
881
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;
|
|
882
1174
|
return secretSafe({
|
|
883
1175
|
schemaVersion: value?.schemaVersion,
|
|
884
1176
|
storeIdentity: value.storeIdentity,
|
|
@@ -900,8 +1192,14 @@ function receiptProjection(value) {
|
|
|
900
1192
|
assertions,
|
|
901
1193
|
...(value?.receiptEvidence?.expectedRefusals ? { expectedRefusals: value.receiptEvidence.expectedRefusals } : {}),
|
|
902
1194
|
},
|
|
1195
|
+
changes: Array.isArray(value?.changes) ? value.changes : [],
|
|
903
1196
|
...(defaultVerdict && typeof defaultVerdict === 'object' ? { defaultVerdict } : {}),
|
|
904
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 } : {}),
|
|
905
1203
|
...(verdict ? { verdict } : {}),
|
|
906
1204
|
...(measurement ? { measurement } : {}),
|
|
907
1205
|
...(value?.publicReceipts?.run ? {
|
|
@@ -914,12 +1212,15 @@ function receiptProjection(value) {
|
|
|
914
1212
|
|
|
915
1213
|
function runsListProjection(value, lifecycle) {
|
|
916
1214
|
const runs = Array.isArray(value?.runs) ? value.runs : [];
|
|
917
|
-
|
|
1215
|
+
if (!Number.isSafeInteger(value?.total) || value.total < 0) {
|
|
1216
|
+
throw new Error('Meguro runs response did not contain a valid total');
|
|
1217
|
+
}
|
|
918
1218
|
return secretSafe({
|
|
919
1219
|
runFamily: 'Shopify dev-store history runs created by run_start.',
|
|
920
1220
|
excludes: 'Practice simulation attempts (pa-*) returned by practice_run_start are a separate family and are not included.',
|
|
921
1221
|
lifecycleFilter: lifecycle ?? 'all',
|
|
922
|
-
|
|
1222
|
+
total: value.total,
|
|
1223
|
+
runs: runs.map((run) => {
|
|
923
1224
|
const {
|
|
924
1225
|
startedAt,
|
|
925
1226
|
updatedAt,
|
|
@@ -938,7 +1239,7 @@ function runsListProjection(value, lifecycle) {
|
|
|
938
1239
|
},
|
|
939
1240
|
};
|
|
940
1241
|
}),
|
|
941
|
-
...(
|
|
1242
|
+
...(runs.length === 0 ? {
|
|
942
1243
|
emptyState: {
|
|
943
1244
|
message: `No ${lifecycle ?? 'matching'} Shopify dev-store history runs were found. This says nothing about practice simulation attempts.`,
|
|
944
1245
|
practiceAttemptRecovery: 'Call practice_runs_list to discover practice simulation attempts, then pass one exact attemptId to practice_run_report or practice_run_impact.',
|
|
@@ -1005,17 +1306,20 @@ function practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedSto
|
|
|
1005
1306
|
};
|
|
1006
1307
|
}
|
|
1007
1308
|
|
|
1008
|
-
function practiceRunsProjection(value, storeId, storeSelection) {
|
|
1309
|
+
function practiceRunsProjection(value, storeId, storeSelection, workspaceId) {
|
|
1009
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
|
+
}
|
|
1010
1314
|
return secretSafe({
|
|
1011
1315
|
schemaVersion: 'meguro.practice-run-list.v1',
|
|
1012
1316
|
storeId,
|
|
1013
1317
|
storeSelection,
|
|
1318
|
+
total: value.total,
|
|
1014
1319
|
attempts: attempts
|
|
1015
|
-
.filter((attempt) => attempt?.storeId === storeId || attempt?.worldId === storeId)
|
|
1016
1320
|
.map((attempt) => ({
|
|
1017
1321
|
attemptId: attempt.attemptId,
|
|
1018
|
-
state: attempt.
|
|
1322
|
+
state: typeof attempt.practiceRun?.state === 'string' ? attempt.practiceRun.state : null,
|
|
1019
1323
|
clock: {
|
|
1020
1324
|
mode: attempt.clock?.mode ?? null,
|
|
1021
1325
|
baselineDay: attempt.baselineDay ?? null,
|
|
@@ -1024,7 +1328,10 @@ function practiceRunsProjection(value, storeId, storeSelection) {
|
|
|
1024
1328
|
elapsedSimulationDays: attempt.elapsedSimulationDays ?? null,
|
|
1025
1329
|
remainingSimulationDays: attempt.remainingSimulationDays ?? null,
|
|
1026
1330
|
},
|
|
1027
|
-
receiptRef: {
|
|
1331
|
+
receiptRef: {
|
|
1332
|
+
attemptId: attempt.attemptId,
|
|
1333
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
1334
|
+
},
|
|
1028
1335
|
})),
|
|
1029
1336
|
});
|
|
1030
1337
|
}
|
|
@@ -1040,8 +1347,10 @@ function practiceAttemptComparisonGuidanceFor(toolName, a, b) {
|
|
|
1040
1347
|
type: 'text',
|
|
1041
1348
|
text: JSON.stringify({
|
|
1042
1349
|
code: 'practice-attempt-comparator-unavailable',
|
|
1043
|
-
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.`,
|
|
1044
1351
|
attemptIds,
|
|
1352
|
+
nextAction: calls.join('; '),
|
|
1353
|
+
stopCondition: `Use the listed practice receipt reads instead. Do not retry ${toolName} with pa-* practice attempt ids.`,
|
|
1045
1354
|
workingMethod: {
|
|
1046
1355
|
calls,
|
|
1047
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.',
|
|
@@ -1105,14 +1414,32 @@ function practiceRunsStoreNotFoundGuidance(storeId) {
|
|
|
1105
1414
|
};
|
|
1106
1415
|
}
|
|
1107
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
|
+
|
|
1108
1435
|
function legacyGateArtifactNotFoundGuidance(runId) {
|
|
1109
1436
|
return {
|
|
1110
1437
|
content: [{
|
|
1111
1438
|
type: 'text',
|
|
1112
1439
|
text: JSON.stringify({
|
|
1113
1440
|
code: 'legacy-gate-artifact-not-found',
|
|
1114
|
-
message: `No retained legacy Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
|
|
1115
|
-
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.',
|
|
1116
1443
|
stopCondition: `Do not retry ${JSON.stringify(runId)}. The legacy artifact cannot be created from a customer surface; stop after gate_verdict({}).`,
|
|
1117
1444
|
}, null, 2),
|
|
1118
1445
|
}],
|
|
@@ -1120,7 +1447,7 @@ function legacyGateArtifactNotFoundGuidance(runId) {
|
|
|
1120
1447
|
};
|
|
1121
1448
|
}
|
|
1122
1449
|
|
|
1123
|
-
function usageProjection(value) {
|
|
1450
|
+
function usageProjection(value, evaluatedRunStartEligibility) {
|
|
1124
1451
|
const totalRuns = Number(value?.totalRuns ?? 0);
|
|
1125
1452
|
const nonBillableRuns = Number(value?.nonBillableRuns ?? 0);
|
|
1126
1453
|
const activityBillableRuns = Math.max(0, totalRuns - nonBillableRuns);
|
|
@@ -1131,6 +1458,17 @@ function usageProjection(value) {
|
|
|
1131
1458
|
const activeStoreLimit = Number(meter.activeStoreLimit ?? 0);
|
|
1132
1459
|
const activeWorkspaces = Number(meter.activeWorkspaces ?? 0);
|
|
1133
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');
|
|
1134
1472
|
const activityCoverageComplete = value?.coverage?.complete === true;
|
|
1135
1473
|
const activityMatchesMeter = activityCoverageComplete && activityBillableRuns === consumedSimulationRuns;
|
|
1136
1474
|
const reconciliationStatus = !activityCoverageComplete
|
|
@@ -1172,17 +1510,23 @@ function usageProjection(value) {
|
|
|
1172
1510
|
activeWorkspaces,
|
|
1173
1511
|
activeWorkspaceLimit,
|
|
1174
1512
|
runStartAllowed: meter.runStartAllowed,
|
|
1513
|
+
runStartAllowedMeaning: allowance?.meaning ?? null,
|
|
1514
|
+
runStartAllowance: allowance ?? null,
|
|
1175
1515
|
enforcement: meter.enforcement,
|
|
1176
1516
|
},
|
|
1517
|
+
runStartEligibility: evaluatedRunStartEligibility ?? value?.runStartEligibility ?? null,
|
|
1177
1518
|
coverage: value?.coverage ?? null,
|
|
1519
|
+
trial: value?.trial ?? null,
|
|
1178
1520
|
});
|
|
1179
1521
|
}
|
|
1180
1522
|
|
|
1181
1523
|
function twinDiffProjection(value) {
|
|
1182
1524
|
if (!value || typeof value !== 'object') return secretSafe(value);
|
|
1183
|
-
const { generatedAt, markdown: _markdown, ...impactReceipt } = value;
|
|
1525
|
+
const { generatedAt, markdown: _markdown, pair, sources, ...impactReceipt } = value;
|
|
1184
1526
|
return secretSafe({
|
|
1185
1527
|
schemaVersion: 'meguro.mcp-twin-diff.v1',
|
|
1528
|
+
pair,
|
|
1529
|
+
sources,
|
|
1186
1530
|
runClock: {
|
|
1187
1531
|
generatedAtWallClock: generatedAt ?? null,
|
|
1188
1532
|
note: 'This is when the impact receipt was generated; it is not Store-time.',
|
|
@@ -1194,6 +1538,34 @@ function twinDiffProjection(value) {
|
|
|
1194
1538
|
});
|
|
1195
1539
|
}
|
|
1196
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
|
+
|
|
1197
1569
|
const SHOPIFY_EXAM_ID = /^pex-[0-9a-f]{24}$/u;
|
|
1198
1570
|
const SHOPIFY_DEV_DOMAIN = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/u;
|
|
1199
1571
|
const SHOPIFY_EXAM_ACTIVE_STATUSES = new Set(['confirmed', 'materializing', 'replaying', 'comparing']);
|
|
@@ -1208,6 +1580,7 @@ const SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE = Object.freeze({
|
|
|
1208
1580
|
'temporal-clock-context-mismatch': 'Use the target practice store clock and rerun the bounded Exam source.',
|
|
1209
1581
|
'temporal-clock-revision-mismatch': 'Capture one bounded burst at one Store-time coordinate; after advancing time, start a new practice run.',
|
|
1210
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.',
|
|
1211
1584
|
});
|
|
1212
1585
|
|
|
1213
1586
|
function requiredExamId(args) {
|
|
@@ -1228,10 +1601,15 @@ function requiredShopifyDevDomain(args) {
|
|
|
1228
1601
|
|
|
1229
1602
|
function examNextAction(exam, receipt) {
|
|
1230
1603
|
const status = String(exam?.status ?? '');
|
|
1231
|
-
if (
|
|
1232
|
-
return
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
+
};
|
|
1235
1613
|
}
|
|
1236
1614
|
if (status === 'blocked') {
|
|
1237
1615
|
if (receipt?.privateAvailable === true && receipt?.publicAvailable === true) {
|
|
@@ -1240,7 +1618,11 @@ function examNextAction(exam, receipt) {
|
|
|
1240
1618
|
return { tool: 'exam_preflight', args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain }, reason: 'preview-blocked' };
|
|
1241
1619
|
}
|
|
1242
1620
|
if (status === 'cleaning') {
|
|
1243
|
-
return {
|
|
1621
|
+
return {
|
|
1622
|
+
tool: 'exam_start',
|
|
1623
|
+
args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain },
|
|
1624
|
+
reason: 'cleanup-checkpoint-complete',
|
|
1625
|
+
};
|
|
1244
1626
|
}
|
|
1245
1627
|
return {
|
|
1246
1628
|
tool: 'exam_start',
|
|
@@ -1312,6 +1694,7 @@ export function createTools(config) {
|
|
|
1312
1694
|
const headers = { accept: 'application/json' };
|
|
1313
1695
|
const needsControlCredential = Boolean(options.auth || method !== 'GET');
|
|
1314
1696
|
if (needsControlCredential) Object.assign(headers, controlAuthHeaders());
|
|
1697
|
+
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1315
1698
|
if (method !== 'GET') {
|
|
1316
1699
|
headers['content-type'] = 'application/json';
|
|
1317
1700
|
}
|
|
@@ -1329,7 +1712,11 @@ export function createTools(config) {
|
|
|
1329
1712
|
const authenticationError = authenticationErrorResult(response.status, json);
|
|
1330
1713
|
if (authenticationError) throw new ToolResultError(authenticationError);
|
|
1331
1714
|
const message = json.errors?.[0]?.message ?? json.error ?? text.slice(0, 300);
|
|
1332
|
-
|
|
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;
|
|
1333
1720
|
}
|
|
1334
1721
|
return json;
|
|
1335
1722
|
}
|
|
@@ -1340,7 +1727,10 @@ export function createTools(config) {
|
|
|
1340
1727
|
const headers = { accept: 'application/json', ...controlAuthHeaders() };
|
|
1341
1728
|
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1342
1729
|
if (method !== 'GET') headers['content-type'] = 'application/json';
|
|
1343
|
-
const
|
|
1730
|
+
const bust = method === 'GET' && options.cacheBustRead === true
|
|
1731
|
+
? `${path.includes('?') ? '&' : '?'}_=${Date.now()}`
|
|
1732
|
+
: '';
|
|
1733
|
+
const response = await fetchImpl(`${apiBaseUrl}${path}${bust}`, {
|
|
1344
1734
|
method,
|
|
1345
1735
|
headers,
|
|
1346
1736
|
...(body ? { body: JSON.stringify(body) } : {}),
|
|
@@ -1391,12 +1781,47 @@ export function createTools(config) {
|
|
|
1391
1781
|
return { schemaVersion: 'meguro.support-code.v1', buildSha, requestIds: [safeRequestId], occurredAt, route: path, line };
|
|
1392
1782
|
}
|
|
1393
1783
|
|
|
1394
|
-
|
|
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) {
|
|
1395
1816
|
const body = value && typeof value === 'object' ? value : {};
|
|
1817
|
+
const declaredBoundary = ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
|
|
1818
|
+
? body.operationCommitBoundary
|
|
1819
|
+
: null;
|
|
1396
1820
|
const sourceErrors = Array.isArray(body.errors) ? body.errors.slice(0, 5) : [];
|
|
1397
1821
|
const allowedShopDomains = Array.isArray(body.allowed)
|
|
1398
1822
|
? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
|
|
1399
1823
|
: [];
|
|
1824
|
+
const catalogConnection = catalogConnectionTeaching(toolName, path, allowedShopDomains, Array.isArray(body.allowed));
|
|
1400
1825
|
const workspaceLimit = body.code === 'active_workspace_limit_reached';
|
|
1401
1826
|
const rawError = typeof body.error === 'string' ? body.error : '';
|
|
1402
1827
|
const routeCode = typeof body.code === 'string'
|
|
@@ -1408,31 +1833,80 @@ export function createTools(config) {
|
|
|
1408
1833
|
const claimCodeError = toolName === 'store_claim_by_code'
|
|
1409
1834
|
&& ['invalid-claim-code', 'claim-code-unavailable', 'claim-code-busy'].includes(routeCode);
|
|
1410
1835
|
const sourceErrorText = sourceErrors.map((error) => String(error?.message ?? '')).join(' ');
|
|
1411
|
-
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
|
|
1412
1874
|
? {
|
|
1413
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.',
|
|
1414
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.`,
|
|
1415
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
|
+
}
|
|
1416
1885
|
: toolName === 'store_create' && status === 400 && /template/iu.test(`${rawError} ${routeMessage ?? ''} ${sourceErrorText}`)
|
|
1417
1886
|
? {
|
|
1418
1887
|
meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
|
|
1419
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.',
|
|
1420
1889
|
}
|
|
1421
|
-
|
|
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;
|
|
1422
1902
|
const defaultNextStep = resourceTeaching
|
|
1423
1903
|
? resourceTeaching.nextStep
|
|
1424
1904
|
: claimCodeError
|
|
1425
1905
|
? routeCode === 'claim-code-busy'
|
|
1426
1906
|
? 'Wait for the current connection update to finish, then reopen Meguro in Shopify Admin before retrying once.'
|
|
1427
1907
|
: 'Reopen Meguro in Shopify Admin and use the current one-time claim code shown for the intended development store.'
|
|
1428
|
-
: toolName === '
|
|
1429
|
-
|
|
1430
|
-
: toolName.startsWith('share_') || toolName === 'shares_list'
|
|
1431
|
-
? 'Call shares_list or share_status, inspect the current private/outward lifecycle state, and retry only the permitted transition.'
|
|
1432
|
-
: toolName === 'catalog_slice_read'
|
|
1433
|
-
? allowedShopDomains.length
|
|
1434
|
-
? `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.`
|
|
1435
|
-
: '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.'
|
|
1436
1910
|
: toolName.startsWith('catalog_')
|
|
1437
1911
|
? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
|
|
1438
1912
|
: toolName === 'store_claim'
|
|
@@ -1440,47 +1914,51 @@ export function createTools(config) {
|
|
|
1440
1914
|
: 'Call stores_list, inspect the current fleet, and retry with a current store id.';
|
|
1441
1915
|
const errors = sourceErrors.length ? sourceErrors.map((error) => ({
|
|
1442
1916
|
message: scrubString(error?.message ?? 'Meguro fleet request failed.'),
|
|
1443
|
-
meaning: scrubString(resourceTeaching?.meaning
|
|
1444
|
-
|
|
1917
|
+
meaning: scrubString(resourceTeaching?.meaning
|
|
1918
|
+
?? catalogConnection?.meaning
|
|
1919
|
+
?? (hasInvocationBoundaryAuthority ? operationMeaning : error?.meaning ?? operationMeaning)),
|
|
1920
|
+
nextStep: scrubString(resourceTeaching?.nextStep ?? catalogConnection?.nextStep ?? error?.nextStep ?? defaultNextStep),
|
|
1445
1921
|
})) : [{
|
|
1446
1922
|
message: scrubString(routeMessage ?? (typeof value === 'string' ? value : 'Meguro fleet request failed.')),
|
|
1447
1923
|
meaning: resourceTeaching
|
|
1448
1924
|
? resourceTeaching.meaning
|
|
1925
|
+
: catalogConnection
|
|
1926
|
+
? catalogConnection.meaning
|
|
1449
1927
|
: workspaceLimit
|
|
1450
1928
|
? 'The account is at its active-workspace limit, so the requested workspace operation was not applied.'
|
|
1451
1929
|
: claimCodeError
|
|
1452
1930
|
? 'The one-time Shopify development-store claim was not applied.'
|
|
1453
|
-
:
|
|
1931
|
+
: operationMeaning,
|
|
1454
1932
|
nextStep: resourceTeaching
|
|
1455
1933
|
? resourceTeaching.nextStep
|
|
1934
|
+
: catalogConnection
|
|
1935
|
+
? catalogConnection.nextStep
|
|
1456
1936
|
: workspaceLimit
|
|
1457
|
-
?
|
|
1937
|
+
? String(body.sameTierAction?.label ?? routeMessage ?? defaultNextStep)
|
|
1458
1938
|
: defaultNextStep,
|
|
1459
1939
|
}];
|
|
1460
1940
|
const stable = secretSafe({
|
|
1461
1941
|
tool: toolName,
|
|
1462
1942
|
httpStatus: status,
|
|
1463
1943
|
...(routeCode ? { code: routeCode } : {}),
|
|
1464
|
-
...(
|
|
1465
|
-
...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
|
|
1944
|
+
...enforcementProjection(body),
|
|
1466
1945
|
...(Number.isFinite(body.activeStores) ? { activeStores: body.activeStores } : {}),
|
|
1467
1946
|
...(Number.isFinite(body.activeStoreLimit) ? { activeStoreLimit: body.activeStoreLimit } : {}),
|
|
1468
1947
|
...(Number.isFinite(body.activeWorkspaces) ? { activeWorkspaces: body.activeWorkspaces } : {}),
|
|
1469
1948
|
...(Number.isFinite(body.activeWorkspaceLimit) ? { activeWorkspaceLimit: body.activeWorkspaceLimit } : {}),
|
|
1470
|
-
...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
|
|
1471
1949
|
errors,
|
|
1472
|
-
...(
|
|
1950
|
+
...(catalogConnection ? { allowedShopDomains } : {}),
|
|
1473
1951
|
...(Array.isArray(body.fleet) ? { fleet: body.fleet } : {}),
|
|
1474
|
-
supportCode: await fleetSupportCode(path, requestId),
|
|
1952
|
+
supportCode: await fleetSupportCode(supportRouteForFleetFailure(toolName, path), requestId),
|
|
1475
1953
|
});
|
|
1476
|
-
|
|
1954
|
+
const result = { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
|
|
1955
|
+
return authoritativeBoundary ? withOperationCommitBoundary(result, authoritativeBoundary) : result;
|
|
1477
1956
|
}
|
|
1478
1957
|
|
|
1479
1958
|
async function historyRunTeachingErrorResult(toolName, path, response, requestedId) {
|
|
1480
1959
|
const body = response.json && typeof response.json === 'object' ? response.json : {};
|
|
1481
1960
|
const rawMessage = String(body.errors?.[0]?.message ?? body.error ?? body.message ?? 'Meguro history-run request failed.');
|
|
1482
1961
|
if (toolName === 'run_start' && response.status === 400 && /not allowed|not connected/iu.test(rawMessage)) {
|
|
1483
|
-
const requestedDomain = String(body.requestedShopDomain ?? requestedId ?? '').trim().toLowerCase();
|
|
1484
1962
|
const allowedShopDomains = Array.isArray(body.allowed)
|
|
1485
1963
|
? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
|
|
1486
1964
|
: [];
|
|
@@ -1493,10 +1971,10 @@ export function createTools(config) {
|
|
|
1493
1971
|
error: { message: rawMessage },
|
|
1494
1972
|
allowedShopDomains,
|
|
1495
1973
|
teaching: {
|
|
1496
|
-
meaning:
|
|
1974
|
+
meaning: 'The requested shop domain is not connected or allowed for this account, so no Shopify dev-store history run was started.',
|
|
1497
1975
|
nextStep,
|
|
1498
1976
|
stopCondition: allowedShopDomains.length
|
|
1499
|
-
?
|
|
1977
|
+
? 'Do not retry the refused domain; use only an exact value from allowedShopDomains.'
|
|
1500
1978
|
: 'Stop until Console exposes a connected Shopify development-store domain.',
|
|
1501
1979
|
},
|
|
1502
1980
|
supportCode: await fleetSupportCode(path, response.requestId),
|
|
@@ -1524,14 +2002,22 @@ export function createTools(config) {
|
|
|
1524
2002
|
const response = await apiRaw(method, path, body, options);
|
|
1525
2003
|
if (!response.ok) {
|
|
1526
2004
|
const authenticationError = authenticationErrorResult(response.status, response.json);
|
|
1527
|
-
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
|
+
) };
|
|
1528
2013
|
}
|
|
1529
2014
|
return { value: response.json };
|
|
1530
2015
|
}
|
|
1531
2016
|
|
|
1532
|
-
async function practiceApi(method, path, body) {
|
|
2017
|
+
async function practiceApi(method, path, body, options = {}) {
|
|
1533
2018
|
if (!practiceApiToken) throw new Error('MEGURO_API_TOKEN is required for practice-run tools');
|
|
1534
2019
|
const headers = { accept: 'application/json', authorization: `Bearer ${practiceApiToken}` };
|
|
2020
|
+
if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
|
|
1535
2021
|
if (method !== 'GET') headers['content-type'] = 'application/json';
|
|
1536
2022
|
let response;
|
|
1537
2023
|
try {
|
|
@@ -1551,23 +2037,20 @@ export function createTools(config) {
|
|
|
1551
2037
|
return { ok: response.ok, status: response.status, json, retryAfterSeconds };
|
|
1552
2038
|
}
|
|
1553
2039
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
?
|
|
1559
|
-
|
|
1560
|
-
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) {
|
|
1561
2046
|
return {
|
|
1562
2047
|
state: 'undetermined',
|
|
1563
|
-
codes
|
|
2048
|
+
codes,
|
|
1564
2049
|
retryable: true,
|
|
1565
2050
|
nextStep: 'Eligibility evidence is undetermined, not negative. Retry preflight; if the evidence read remains unavailable, use the support code.',
|
|
1566
2051
|
};
|
|
1567
2052
|
}
|
|
1568
|
-
const codes = Array.isArray(eligibility.codes) ? eligibility.codes.map(String) : [];
|
|
1569
2053
|
const steps = [...new Set(codes.map((code) => SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE[code]).filter(Boolean))];
|
|
1570
|
-
const storeId = String(attempt?.storeId ?? attempt?.worldId ?? '').trim();
|
|
1571
2054
|
const mustRecapture = codes.some((code) => [
|
|
1572
2055
|
'tape-missing',
|
|
1573
2056
|
'tape-total-over-limit',
|
|
@@ -1579,11 +2062,9 @@ export function createTools(config) {
|
|
|
1579
2062
|
'temporal-clock-revision-mismatch',
|
|
1580
2063
|
'interleaved-simulation-state-change',
|
|
1581
2064
|
].includes(code));
|
|
1582
|
-
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.';
|
|
1583
2066
|
return {
|
|
1584
|
-
state:
|
|
1585
|
-
schemaVersion: eligibility.schemaVersion,
|
|
1586
|
-
replayDisposition: eligibility.replayDisposition,
|
|
2067
|
+
state: 'blocked',
|
|
1587
2068
|
codes,
|
|
1588
2069
|
retryable: false,
|
|
1589
2070
|
nextStep: mustRecapture
|
|
@@ -1598,20 +2079,23 @@ export function createTools(config) {
|
|
|
1598
2079
|
const source = response.json?.examError && typeof response.json.examError === 'object'
|
|
1599
2080
|
? response.json.examError
|
|
1600
2081
|
: { code: 'exam-request-failed', message: 'The Shopify Exam request failed safely.', retryable: response.status >= 500 };
|
|
1601
|
-
const eligibility = source
|
|
1602
|
-
|
|
1603
|
-
: null;
|
|
1604
|
-
const retryable = eligibility?.state === 'undetermined'
|
|
1605
|
-
? true
|
|
1606
|
-
: source.retryable === true;
|
|
2082
|
+
const eligibility = examEligibility(source);
|
|
2083
|
+
const retryable = source.retryable === true;
|
|
1607
2084
|
const startPairNotFound = toolName === 'exam_start' && source.code === 'not_found';
|
|
1608
|
-
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
|
|
1609
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.'
|
|
1610
2092
|
: eligibility?.nextStep
|
|
1611
2093
|
?? (retryable
|
|
1612
|
-
?
|
|
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.'
|
|
1613
2097
|
: 'Correct the stated Exam requirement, then retry only the failed bounded step.');
|
|
1614
|
-
|
|
2098
|
+
const result = {
|
|
1615
2099
|
content: [{
|
|
1616
2100
|
type: 'text',
|
|
1617
2101
|
text: JSON.stringify(secretSafe({
|
|
@@ -1625,19 +2109,28 @@ export function createTools(config) {
|
|
|
1625
2109
|
codes: eligibility.codes,
|
|
1626
2110
|
} } : {}),
|
|
1627
2111
|
teaching: {
|
|
1628
|
-
meaning: startPairNotFound
|
|
2112
|
+
meaning: startPairNotFound || preflightPairNotFound
|
|
1629
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.'
|
|
1630
2116
|
: source.code === 'exam-ineligible'
|
|
1631
2117
|
? 'Shopify Exam accepts only a completed, graded, immutable captured attempt whose replay evidence is current and supported.'
|
|
1632
2118
|
: 'The requested bounded Shopify Exam step was not assumed complete.',
|
|
1633
2119
|
nextStep,
|
|
1634
|
-
...(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
|
+
: {}),
|
|
1635
2125
|
},
|
|
1636
2126
|
supportCode: await fleetSupportCode(path, response.requestId),
|
|
1637
2127
|
}), null, 2),
|
|
1638
2128
|
}],
|
|
1639
2129
|
isError: true,
|
|
1640
2130
|
};
|
|
2131
|
+
return startPairNotFound || preflightPairNotFound
|
|
2132
|
+
? withOperationCommitBoundary(result, 'pre-commit')
|
|
2133
|
+
: result;
|
|
1641
2134
|
}
|
|
1642
2135
|
|
|
1643
2136
|
async function examRequest(toolName, method, path, body, attemptId) {
|
|
@@ -1720,8 +2213,13 @@ export function createTools(config) {
|
|
|
1720
2213
|
description: DOCUMENTATION_TOOL_CONTRACT.topicDescription,
|
|
1721
2214
|
},
|
|
1722
2215
|
version: {
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
+
],
|
|
1725
2223
|
description: DOCUMENTATION_TOOL_CONTRACT.versionDescription,
|
|
1726
2224
|
},
|
|
1727
2225
|
},
|
|
@@ -1730,7 +2228,7 @@ export function createTools(config) {
|
|
|
1730
2228
|
},
|
|
1731
2229
|
{
|
|
1732
2230
|
name: 'templates_list',
|
|
1733
|
-
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.',
|
|
1734
2232
|
inputSchema: {
|
|
1735
2233
|
type: 'object',
|
|
1736
2234
|
additionalProperties: false,
|
|
@@ -1740,6 +2238,19 @@ export function createTools(config) {
|
|
|
1740
2238
|
required: [],
|
|
1741
2239
|
},
|
|
1742
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
|
+
},
|
|
1743
2254
|
{
|
|
1744
2255
|
name: 'stores_list',
|
|
1745
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.',
|
|
@@ -1753,7 +2264,7 @@ export function createTools(config) {
|
|
|
1753
2264
|
},
|
|
1754
2265
|
{
|
|
1755
2266
|
name: 'store_create',
|
|
1756
|
-
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.',
|
|
1757
2268
|
inputSchema: {
|
|
1758
2269
|
type: 'object', additionalProperties: false,
|
|
1759
2270
|
properties: {
|
|
@@ -1827,70 +2338,6 @@ export function createTools(config) {
|
|
|
1827
2338
|
required: ['workspaceId'],
|
|
1828
2339
|
},
|
|
1829
2340
|
},
|
|
1830
|
-
{
|
|
1831
|
-
name: 'share_create',
|
|
1832
|
-
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.',
|
|
1833
|
-
inputSchema: {
|
|
1834
|
-
type: 'object', additionalProperties: false,
|
|
1835
|
-
properties: {
|
|
1836
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1837
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Existing draft/changes-requested share to update. Omit to create.' },
|
|
1838
|
-
title: { type: 'string', minLength: 1, maxLength: 120 },
|
|
1839
|
-
artifactType: { type: 'string', enum: ['receipt', 'gate-evidence-export'], description: 'Required only when creating.' },
|
|
1840
|
-
artifactRef: SHARE_ARTIFACT_REF_INPUT_SCHEMA,
|
|
1841
|
-
audienceUserIds: { type: 'array', maxItems: 40, items: { type: 'string', minLength: 1 }, description: 'Active client-viewer user ids assigned to this exact workspace.' },
|
|
1842
|
-
},
|
|
1843
|
-
required: [],
|
|
1844
|
-
},
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
name: 'shares_list',
|
|
1848
|
-
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.',
|
|
1849
|
-
inputSchema: {
|
|
1850
|
-
type: 'object', additionalProperties: false,
|
|
1851
|
-
properties: {
|
|
1852
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1853
|
-
},
|
|
1854
|
-
required: [],
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1857
|
-
{
|
|
1858
|
-
name: 'share_status',
|
|
1859
|
-
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.',
|
|
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
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
|
|
1865
|
-
},
|
|
1866
|
-
required: ['shareId'],
|
|
1867
|
-
},
|
|
1868
|
-
},
|
|
1869
|
-
{
|
|
1870
|
-
name: 'share_publish',
|
|
1871
|
-
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.',
|
|
1872
|
-
inputSchema: {
|
|
1873
|
-
type: 'object', additionalProperties: false,
|
|
1874
|
-
properties: {
|
|
1875
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1876
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from share_status.' },
|
|
1877
|
-
action: { type: 'string', enum: ['publish', 're-share'], description: 'Exact server lifecycle transition. Defaults to publish.' },
|
|
1878
|
-
},
|
|
1879
|
-
required: ['shareId'],
|
|
1880
|
-
},
|
|
1881
|
-
},
|
|
1882
|
-
{
|
|
1883
|
-
name: 'share_revoke',
|
|
1884
|
-
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.',
|
|
1885
|
-
inputSchema: {
|
|
1886
|
-
type: 'object', additionalProperties: false,
|
|
1887
|
-
properties: {
|
|
1888
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1889
|
-
shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
|
|
1890
|
-
},
|
|
1891
|
-
required: ['shareId'],
|
|
1892
|
-
},
|
|
1893
|
-
},
|
|
1894
2341
|
{
|
|
1895
2342
|
name: 'catalog_slice_read',
|
|
1896
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.',
|
|
@@ -1908,32 +2355,12 @@ export function createTools(config) {
|
|
|
1908
2355
|
{
|
|
1909
2356
|
name: 'catalog_slice_snapshot',
|
|
1910
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.',
|
|
1911
|
-
inputSchema:
|
|
1912
|
-
type: 'object', additionalProperties: false,
|
|
1913
|
-
properties: {
|
|
1914
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1915
|
-
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
|
|
1916
|
-
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.' },
|
|
1917
|
-
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
|
|
1918
|
-
},
|
|
1919
|
-
required: [],
|
|
1920
|
-
},
|
|
2358
|
+
inputSchema: CATALOG_SNAPSHOT_INPUT_SCHEMA,
|
|
1921
2359
|
},
|
|
1922
2360
|
{
|
|
1923
2361
|
name: 'catalog_slices_saved',
|
|
1924
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.',
|
|
1925
|
-
inputSchema:
|
|
1926
|
-
type: 'object', additionalProperties: false,
|
|
1927
|
-
properties: {
|
|
1928
|
-
workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
|
|
1929
|
-
action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
|
|
1930
|
-
shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
|
|
1931
|
-
label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
|
|
1932
|
-
variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
|
|
1933
|
-
sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
|
|
1934
|
-
},
|
|
1935
|
-
required: ['action'],
|
|
1936
|
-
},
|
|
2363
|
+
inputSchema: CATALOG_SAVED_SLICES_INPUT_SCHEMA,
|
|
1937
2364
|
},
|
|
1938
2365
|
{
|
|
1939
2366
|
name: 'store_claim_by_code',
|
|
@@ -1992,7 +2419,7 @@ export function createTools(config) {
|
|
|
1992
2419
|
},
|
|
1993
2420
|
{
|
|
1994
2421
|
name: 'run_status',
|
|
1995
|
-
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.',
|
|
1996
2423
|
inputSchema: {
|
|
1997
2424
|
type: 'object',
|
|
1998
2425
|
additionalProperties: false,
|
|
@@ -2015,7 +2442,7 @@ export function createTools(config) {
|
|
|
2015
2442
|
},
|
|
2016
2443
|
{
|
|
2017
2444
|
name: 'run_report',
|
|
2018
|
-
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.',
|
|
2019
2446
|
inputSchema: {
|
|
2020
2447
|
type: 'object',
|
|
2021
2448
|
additionalProperties: false,
|
|
@@ -2039,7 +2466,7 @@ export function createTools(config) {
|
|
|
2039
2466
|
},
|
|
2040
2467
|
{
|
|
2041
2468
|
name: 'runs_diff',
|
|
2042
|
-
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.',
|
|
2043
2470
|
inputSchema: {
|
|
2044
2471
|
type: 'object',
|
|
2045
2472
|
additionalProperties: false,
|
|
@@ -2052,26 +2479,26 @@ export function createTools(config) {
|
|
|
2052
2479
|
},
|
|
2053
2480
|
{
|
|
2054
2481
|
name: 'gate_configure',
|
|
2055
|
-
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.",
|
|
2056
2483
|
inputSchema: {
|
|
2057
2484
|
type: 'object',
|
|
2058
2485
|
additionalProperties: false,
|
|
2059
2486
|
properties: {
|
|
2060
|
-
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.' },
|
|
2061
2488
|
storeId: { type: 'string', minLength: 1, description: 'Optional practice-store identity carried by the selected Console receipt.' },
|
|
2062
|
-
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.' },
|
|
2063
2490
|
},
|
|
2064
2491
|
required: ['receiptId'],
|
|
2065
2492
|
},
|
|
2066
2493
|
},
|
|
2067
2494
|
{
|
|
2068
2495
|
name: 'gate_evaluate',
|
|
2069
|
-
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.",
|
|
2070
2497
|
inputSchema: {
|
|
2071
2498
|
type: 'object',
|
|
2072
2499
|
additionalProperties: false,
|
|
2073
2500
|
properties: {
|
|
2074
|
-
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.' },
|
|
2075
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.' },
|
|
2076
2503
|
},
|
|
2077
2504
|
required: [],
|
|
@@ -2079,20 +2506,20 @@ export function createTools(config) {
|
|
|
2079
2506
|
},
|
|
2080
2507
|
{
|
|
2081
2508
|
name: 'gate_verdict',
|
|
2082
|
-
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.",
|
|
2083
2510
|
inputSchema: {
|
|
2084
2511
|
type: 'object',
|
|
2085
2512
|
additionalProperties: false,
|
|
2086
2513
|
properties: {
|
|
2087
|
-
runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Receipt Gate verdict.' },
|
|
2088
|
-
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.' },
|
|
2089
2516
|
},
|
|
2090
2517
|
required: [],
|
|
2091
2518
|
},
|
|
2092
2519
|
},
|
|
2093
2520
|
{
|
|
2094
2521
|
name: 'runs_list',
|
|
2095
|
-
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.',
|
|
2096
2523
|
inputSchema: {
|
|
2097
2524
|
type: 'object',
|
|
2098
2525
|
additionalProperties: false,
|
|
@@ -2104,12 +2531,22 @@ export function createTools(config) {
|
|
|
2104
2531
|
},
|
|
2105
2532
|
{
|
|
2106
2533
|
name: 'usage_read',
|
|
2107
|
-
description: 'Read authoritative tier-meter simulation runs, practice-store, and workspace headroom
|
|
2108
|
-
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
|
+
},
|
|
2109
2546
|
},
|
|
2110
2547
|
{
|
|
2111
2548
|
name: 'twin_diff',
|
|
2112
|
-
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.',
|
|
2113
2550
|
inputSchema: {
|
|
2114
2551
|
type: 'object',
|
|
2115
2552
|
additionalProperties: false,
|
|
@@ -2134,7 +2571,7 @@ export function createTools(config) {
|
|
|
2134
2571
|
},
|
|
2135
2572
|
{
|
|
2136
2573
|
name: 'exam_start',
|
|
2137
|
-
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.',
|
|
2138
2575
|
inputSchema: {
|
|
2139
2576
|
type: 'object', additionalProperties: false,
|
|
2140
2577
|
properties: {
|
|
@@ -2146,7 +2583,7 @@ export function createTools(config) {
|
|
|
2146
2583
|
},
|
|
2147
2584
|
{
|
|
2148
2585
|
name: 'exam_status',
|
|
2149
|
-
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.',
|
|
2150
2587
|
inputSchema: {
|
|
2151
2588
|
type: 'object', additionalProperties: false,
|
|
2152
2589
|
properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Exam id returned by exam_start.' } },
|
|
@@ -2155,7 +2592,7 @@ export function createTools(config) {
|
|
|
2155
2592
|
},
|
|
2156
2593
|
{
|
|
2157
2594
|
name: 'exam_report',
|
|
2158
|
-
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.',
|
|
2159
2596
|
inputSchema: {
|
|
2160
2597
|
type: 'object', additionalProperties: false,
|
|
2161
2598
|
properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Completed Exam id returned by exam_start.' } },
|
|
@@ -2164,13 +2601,13 @@ export function createTools(config) {
|
|
|
2164
2601
|
},
|
|
2165
2602
|
{
|
|
2166
2603
|
name: 'practice_run_start',
|
|
2167
|
-
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.',
|
|
2168
2605
|
inputSchema: {
|
|
2169
2606
|
type: 'object',
|
|
2170
2607
|
additionalProperties: false,
|
|
2171
2608
|
properties: {
|
|
2609
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2172
2610
|
storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
|
|
2173
|
-
worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
|
|
2174
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.' },
|
|
2175
2612
|
clock: {
|
|
2176
2613
|
type: 'object',
|
|
@@ -2227,8 +2664,7 @@ export function createTools(config) {
|
|
|
2227
2664
|
},
|
|
2228
2665
|
graphqlThrottleMode: { type: 'string', enum: ['off', 'standard-1000', 'plus-2000'], description: 'Optional Meguro compatibility GraphQL throttle mode.' },
|
|
2229
2666
|
},
|
|
2230
|
-
required: [],
|
|
2231
|
-
anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
|
|
2667
|
+
required: ['storeId'],
|
|
2232
2668
|
},
|
|
2233
2669
|
},
|
|
2234
2670
|
{
|
|
@@ -2247,10 +2683,13 @@ export function createTools(config) {
|
|
|
2247
2683
|
},
|
|
2248
2684
|
{
|
|
2249
2685
|
name: 'practice_run_status',
|
|
2250
|
-
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
|
|
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.',
|
|
2251
2687
|
inputSchema: {
|
|
2252
2688
|
type: 'object', additionalProperties: false,
|
|
2253
|
-
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
|
+
},
|
|
2254
2693
|
required: ['attemptId'],
|
|
2255
2694
|
},
|
|
2256
2695
|
},
|
|
@@ -2259,25 +2698,28 @@ export function createTools(config) {
|
|
|
2259
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.',
|
|
2260
2699
|
inputSchema: {
|
|
2261
2700
|
type: 'object', additionalProperties: false,
|
|
2262
|
-
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
|
+
},
|
|
2263
2705
|
required: ['attemptId'],
|
|
2264
2706
|
},
|
|
2265
2707
|
},
|
|
2266
2708
|
{
|
|
2267
2709
|
name: 'practice_run_advance',
|
|
2268
|
-
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.',
|
|
2269
2711
|
inputSchema: {
|
|
2270
2712
|
type: 'object',
|
|
2271
2713
|
additionalProperties: false,
|
|
2272
2714
|
properties: {
|
|
2715
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2273
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).' },
|
|
2274
2717
|
days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
|
|
2275
2718
|
until: PRACTICE_RUN_UNTIL_INPUT_SCHEMA,
|
|
2276
|
-
|
|
2277
|
-
expectedCallSeq: { type: 'integer', minimum: -1, description: 'From the same immediately preceding practice_run_status response, copy current practiceRun.latestCallSequence (or -1 only when that response says no call exists). This cursor is required on every advance; if it is stale, the advance is refused without moving Store time.' },
|
|
2719
|
+
advanceCursor: PRACTICE_RUN_ADVANCE_CURSOR_INPUT_SCHEMA,
|
|
2278
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.' },
|
|
2279
2721
|
},
|
|
2280
|
-
required: ['attemptId', '
|
|
2722
|
+
required: ['attemptId', 'advanceCursor'],
|
|
2281
2723
|
oneOf: [
|
|
2282
2724
|
{ required: ['days'], not: { required: ['until'] } },
|
|
2283
2725
|
{ required: ['until'], not: { required: ['days'] } },
|
|
@@ -2286,28 +2728,43 @@ export function createTools(config) {
|
|
|
2286
2728
|
},
|
|
2287
2729
|
{
|
|
2288
2730
|
name: 'practice_run_finish',
|
|
2289
|
-
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.',
|
|
2290
2732
|
inputSchema: {
|
|
2291
2733
|
type: 'object', additionalProperties: false,
|
|
2292
|
-
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
|
+
},
|
|
2293
2740
|
required: ['attemptId'],
|
|
2741
|
+
dependentRequired: {
|
|
2742
|
+
permanentStop: ['acknowledgeIncompleteOnboardingEvaluation'],
|
|
2743
|
+
acknowledgeIncompleteOnboardingEvaluation: ['permanentStop'],
|
|
2744
|
+
},
|
|
2294
2745
|
},
|
|
2295
2746
|
},
|
|
2296
2747
|
{
|
|
2297
2748
|
name: 'practice_run_report',
|
|
2298
|
-
description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. 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
|
|
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.',
|
|
2299
2750
|
inputSchema: {
|
|
2300
2751
|
type: 'object', additionalProperties: false,
|
|
2301
|
-
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
|
+
},
|
|
2302
2756
|
required: ['attemptId'],
|
|
2303
2757
|
},
|
|
2304
2758
|
},
|
|
2305
2759
|
{
|
|
2306
2760
|
name: 'practice_run_impact',
|
|
2307
|
-
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.',
|
|
2308
2762
|
inputSchema: {
|
|
2309
2763
|
type: 'object', additionalProperties: false,
|
|
2310
|
-
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
|
+
},
|
|
2311
2768
|
required: ['attemptId'],
|
|
2312
2769
|
},
|
|
2313
2770
|
},
|
|
@@ -2318,11 +2775,10 @@ export function createTools(config) {
|
|
|
2318
2775
|
type: 'object',
|
|
2319
2776
|
additionalProperties: false,
|
|
2320
2777
|
properties: {
|
|
2778
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2321
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.' },
|
|
2322
|
-
worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
|
|
2323
2780
|
},
|
|
2324
|
-
required: [],
|
|
2325
|
-
anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
|
|
2781
|
+
required: ['storeId'],
|
|
2326
2782
|
},
|
|
2327
2783
|
},
|
|
2328
2784
|
{
|
|
@@ -2332,6 +2788,7 @@ export function createTools(config) {
|
|
|
2332
2788
|
type: 'object',
|
|
2333
2789
|
additionalProperties: false,
|
|
2334
2790
|
properties: {
|
|
2791
|
+
workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
|
|
2335
2792
|
worldId: { type: 'string', minLength: 1, description: 'Meguro practice store id, e.g. w-abc123.' },
|
|
2336
2793
|
query: { type: 'string', maxLength: 30000, description: 'The Admin GraphQL query or mutation document to run.' },
|
|
2337
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}.` },
|
|
@@ -2344,21 +2801,28 @@ export function createTools(config) {
|
|
|
2344
2801
|
},
|
|
2345
2802
|
{
|
|
2346
2803
|
name: 'admin_schema',
|
|
2347
|
-
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.`,
|
|
2348
2805
|
inputSchema: {
|
|
2349
2806
|
type: 'object',
|
|
2350
2807
|
additionalProperties: false,
|
|
2351
2808
|
properties: {
|
|
2352
|
-
lookup: { type: 'string', enum: ['type', 'field', 'mutation', 'recipe'], description: '
|
|
2353
|
-
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.' },
|
|
2354
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}.` },
|
|
2355
2815
|
},
|
|
2356
|
-
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
|
+
],
|
|
2357
2821
|
},
|
|
2358
2822
|
},
|
|
2359
2823
|
{
|
|
2360
2824
|
name: 'admin_recipes_list',
|
|
2361
|
-
description: 'List every canonical Admin API recipe Meguro models, with its stable id, operation name, purpose, confirmation requirement,
|
|
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.',
|
|
2362
2826
|
inputSchema: {
|
|
2363
2827
|
type: 'object',
|
|
2364
2828
|
additionalProperties: false,
|
|
@@ -2366,6 +2830,39 @@ export function createTools(config) {
|
|
|
2366
2830
|
required: [],
|
|
2367
2831
|
},
|
|
2368
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'],
|
|
2864
|
+
},
|
|
2865
|
+
},
|
|
2369
2866
|
];
|
|
2370
2867
|
const definitions = rawDefinitions.map((definition) => {
|
|
2371
2868
|
const presentation = TOOL_PRESENTATION[definition.name];
|
|
@@ -2381,13 +2878,16 @@ export function createTools(config) {
|
|
|
2381
2878
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
2382
2879
|
throw new Error(`${name} arguments must be an object`);
|
|
2383
2880
|
}
|
|
2881
|
+
if (name === 'workspace_archive' || name === 'workspace_unarchive') {
|
|
2882
|
+
requiredWorkspaceId(args);
|
|
2883
|
+
}
|
|
2384
2884
|
if (SCHEMA_VALIDATED_TOOL_NAMES.has(name)) {
|
|
2385
|
-
// Preserve the established identifier teaching messages while the published schema carries
|
|
2386
|
-
// the same shape for schema-first clients. This also handles the equal-value legacy alias
|
|
2387
|
-
// pair, whose equality cannot be expressed in portable JSON Schema.
|
|
2388
2885
|
if (name === 'get_connection_details' || name === 'practice_run_start') requiredPracticeStoreId(args);
|
|
2389
2886
|
const error = schemaValidationError(definition.inputSchema, args);
|
|
2390
|
-
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
|
+
}
|
|
2391
2891
|
return;
|
|
2392
2892
|
}
|
|
2393
2893
|
if (definition.inputSchema.additionalProperties !== false) return;
|
|
@@ -2406,15 +2906,34 @@ export function createTools(config) {
|
|
|
2406
2906
|
if (!DOCUMENTATION_TOPICS.has(topic)) {
|
|
2407
2907
|
throw new Error(DOCUMENTATION_TOOL_CONTRACT.topicError);
|
|
2408
2908
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
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);
|
|
2411
2916
|
}
|
|
2412
|
-
return textResult(
|
|
2917
|
+
return textResult(resolveDocumentationRead(topic, requested, DOCUMENTATION_TOOL_CONTRACT));
|
|
2413
2918
|
}
|
|
2414
2919
|
case 'templates_list': {
|
|
2415
2920
|
const workspaceId = optionalWorkspaceId(args);
|
|
2416
2921
|
const response = await fleetRequest(name, 'GET', '/practice/profiles', undefined, { workspaceId });
|
|
2417
|
-
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));
|
|
2418
2937
|
}
|
|
2419
2938
|
case 'stores_list': {
|
|
2420
2939
|
const workspaceId = optionalWorkspaceId(args);
|
|
@@ -2467,59 +2986,6 @@ export function createTools(config) {
|
|
|
2467
2986
|
const response = await fleetRequest(name, 'POST', `/workspaces/${encodeURIComponent(workspaceId)}/${action}`, {});
|
|
2468
2987
|
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2469
2988
|
}
|
|
2470
|
-
case 'share_create': {
|
|
2471
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2472
|
-
if (args.shareId !== undefined) {
|
|
2473
|
-
const shareId = requiredShareId(args);
|
|
2474
|
-
if (args.artifactType !== undefined || args.artifactRef !== undefined) {
|
|
2475
|
-
throw new Error('artifactType and artifactRef are immutable; omit shareId to create a new share');
|
|
2476
|
-
}
|
|
2477
|
-
const body = {};
|
|
2478
|
-
if (args.title !== undefined) {
|
|
2479
|
-
body.title = requiredString(args, 'title');
|
|
2480
|
-
if (body.title.length > 120) throw new Error('title must be 120 characters or fewer');
|
|
2481
|
-
}
|
|
2482
|
-
const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
|
|
2483
|
-
if (audienceUserIds !== undefined) body.audienceUserIds = audienceUserIds;
|
|
2484
|
-
if (Object.keys(body).length === 0) throw new Error('share update requires title and/or audienceUserIds');
|
|
2485
|
-
const response = await fleetRequest(name, 'PATCH', `/shares/${encodeURIComponent(shareId)}`, body, { workspaceId });
|
|
2486
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2487
|
-
}
|
|
2488
|
-
const title = requiredString(args, 'title');
|
|
2489
|
-
if (title.length > 120) throw new Error('title must be 120 characters or fewer');
|
|
2490
|
-
const { artifactType, artifactRef } = requiredShareArtifact(args);
|
|
2491
|
-
const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
|
|
2492
|
-
if (!audienceUserIds) throw new Error('audienceUserIds is required when creating a share');
|
|
2493
|
-
const response = await fleetRequest(name, 'POST', '/shares', {
|
|
2494
|
-
title, artifactType, artifactRef, audienceUserIds,
|
|
2495
|
-
}, { workspaceId });
|
|
2496
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2497
|
-
}
|
|
2498
|
-
case 'shares_list': {
|
|
2499
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2500
|
-
const response = await fleetRequest(name, 'GET', '/shares', undefined, { workspaceId });
|
|
2501
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2502
|
-
}
|
|
2503
|
-
case 'share_status': {
|
|
2504
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2505
|
-
const shareId = requiredShareId(args);
|
|
2506
|
-
const response = await fleetRequest(name, 'GET', `/shares/${encodeURIComponent(shareId)}`, undefined, { workspaceId });
|
|
2507
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2508
|
-
}
|
|
2509
|
-
case 'share_publish': {
|
|
2510
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2511
|
-
const shareId = requiredShareId(args);
|
|
2512
|
-
const action = args.action === undefined ? 'publish' : requiredString(args, 'action');
|
|
2513
|
-
if (!['publish', 're-share'].includes(action)) throw new Error('action must be publish or re-share');
|
|
2514
|
-
const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/${action}`, {}, { workspaceId });
|
|
2515
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2516
|
-
}
|
|
2517
|
-
case 'share_revoke': {
|
|
2518
|
-
const workspaceId = optionalWorkspaceId(args);
|
|
2519
|
-
const shareId = requiredShareId(args);
|
|
2520
|
-
const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/revoke`, {}, { workspaceId });
|
|
2521
|
-
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2522
|
-
}
|
|
2523
2989
|
case 'catalog_slice_read': {
|
|
2524
2990
|
const workspaceId = optionalWorkspaceId(args);
|
|
2525
2991
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
@@ -2533,33 +2999,33 @@ export function createTools(config) {
|
|
|
2533
2999
|
}
|
|
2534
3000
|
case 'catalog_slice_snapshot': {
|
|
2535
3001
|
const workspaceId = optionalWorkspaceId(args);
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
if (hasSavedSlice === hasLiveSlice) {
|
|
2539
|
-
throw new Error('provide exactly one snapshot source: sliceId, or shopDomain plus variantIds');
|
|
2540
|
-
}
|
|
3002
|
+
const branch = conditionalToolBranch(name, args);
|
|
3003
|
+
if (!branch) throw new Error('catalog_slice_snapshot branch selection did not match its declared input schema');
|
|
2541
3004
|
let response;
|
|
2542
|
-
if (
|
|
3005
|
+
if (branch.id === 'saved') {
|
|
2543
3006
|
const sliceId = requiredSavedSliceId(args);
|
|
2544
|
-
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
|
+
});
|
|
2545
3010
|
} else {
|
|
2546
3011
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
2547
3012
|
const variantIds = requiredCatalogVariantIds(args);
|
|
2548
3013
|
response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slice/snapshot`, {
|
|
2549
3014
|
variantIds,
|
|
2550
|
-
}, { workspaceId });
|
|
3015
|
+
}, { workspaceId, invocationMutates: branch.mutates });
|
|
2551
3016
|
}
|
|
2552
3017
|
return 'error' in response ? response.error : textResult(catalogSnapshotProjection(response.value));
|
|
2553
3018
|
}
|
|
2554
3019
|
case 'catalog_slices_saved': {
|
|
2555
3020
|
const workspaceId = optionalWorkspaceId(args);
|
|
2556
|
-
const
|
|
2557
|
-
if (!
|
|
2558
|
-
|
|
2559
|
-
}
|
|
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;
|
|
2560
3024
|
let response;
|
|
2561
3025
|
if (action === 'list') {
|
|
2562
|
-
response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
|
|
3026
|
+
response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
|
|
3027
|
+
workspaceId, invocationMutates: branch.mutates,
|
|
3028
|
+
});
|
|
2563
3029
|
} else if (action === 'save') {
|
|
2564
3030
|
const shopDomain = requiredShopifyDevDomain(args);
|
|
2565
3031
|
const label = requiredString(args, 'label');
|
|
@@ -2567,14 +3033,20 @@ export function createTools(config) {
|
|
|
2567
3033
|
response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slices`, {
|
|
2568
3034
|
label,
|
|
2569
3035
|
variantIds: requiredCatalogVariantIds(args),
|
|
2570
|
-
}, { workspaceId });
|
|
3036
|
+
}, { workspaceId, invocationMutates: branch.mutates });
|
|
2571
3037
|
} else {
|
|
2572
3038
|
const sliceId = requiredSavedSliceId(args);
|
|
2573
3039
|
const suffix = action === 'refresh'
|
|
2574
3040
|
? '/refresh'
|
|
2575
3041
|
: action === 'changes' ? '/changes/latest' : '';
|
|
2576
3042
|
const method = action === 'delete' ? 'DELETE' : action === 'refresh' ? 'POST' : 'GET';
|
|
2577
|
-
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
|
+
);
|
|
2578
3050
|
}
|
|
2579
3051
|
return 'error' in response ? response.error : textResult(secretSafe(response.value));
|
|
2580
3052
|
}
|
|
@@ -2637,8 +3109,9 @@ export function createTools(config) {
|
|
|
2637
3109
|
case 'run_status': {
|
|
2638
3110
|
const runId = requiredString(args, 'runId');
|
|
2639
3111
|
if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
|
|
2640
|
-
const status = await
|
|
2641
|
-
|
|
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) });
|
|
2642
3115
|
}
|
|
2643
3116
|
case 'run_ledger': {
|
|
2644
3117
|
const runId = requiredString(args, 'runId');
|
|
@@ -2649,7 +3122,8 @@ export function createTools(config) {
|
|
|
2649
3122
|
case 'run_report': {
|
|
2650
3123
|
const runId = requiredString(args, 'runId');
|
|
2651
3124
|
if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
|
|
2652
|
-
|
|
3125
|
+
const report = await apiRaw('GET', `/runs/${encodeURIComponent(runId)}/report`);
|
|
3126
|
+
return report.ok ? textResult(report.json) : historyReadErrorResult(report);
|
|
2653
3127
|
}
|
|
2654
3128
|
case 'run_resume': {
|
|
2655
3129
|
const runId = requiredString(args, 'runId');
|
|
@@ -2697,9 +3171,9 @@ export function createTools(config) {
|
|
|
2697
3171
|
const teaching = isGateReceiptUnavailable(response.status, response.json)
|
|
2698
3172
|
? gateReceiptUnavailableTeaching(name, receiptId)
|
|
2699
3173
|
: undefined;
|
|
2700
|
-
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3174
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
|
|
2701
3175
|
}
|
|
2702
|
-
return
|
|
3176
|
+
return configuredReceiptGateResult(secretSafe(response.json), enabled ? 'configuration saved' : 'disabled');
|
|
2703
3177
|
}
|
|
2704
3178
|
case 'gate_evaluate': {
|
|
2705
3179
|
const receiptId = args.receiptId === undefined
|
|
@@ -2716,9 +3190,9 @@ export function createTools(config) {
|
|
|
2716
3190
|
const teaching = receiptId && isGateReceiptUnavailable(response.status, response.json)
|
|
2717
3191
|
? gateReceiptUnavailableTeaching(name, receiptId)
|
|
2718
3192
|
: undefined;
|
|
2719
|
-
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
|
|
3193
|
+
return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
|
|
2720
3194
|
}
|
|
2721
|
-
return
|
|
3195
|
+
return configuredReceiptGateResult(secretSafe(response.json), 'evaluation recorded');
|
|
2722
3196
|
}
|
|
2723
3197
|
case 'gate_verdict': {
|
|
2724
3198
|
const runId = args.runId === undefined ? '' : requiredString(args, 'runId');
|
|
@@ -2730,7 +3204,7 @@ export function createTools(config) {
|
|
|
2730
3204
|
if (!latest.ok) return practiceErrorResult(latest.status, latest.json, latest.retryAfterSeconds);
|
|
2731
3205
|
const evaluation = latest.json?.evaluations?.[0];
|
|
2732
3206
|
if (!evaluation) {
|
|
2733
|
-
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.");
|
|
2734
3208
|
}
|
|
2735
3209
|
const verdict = evaluation.gateVerdict;
|
|
2736
3210
|
if (!verdict || verdict.schemaVersion !== 'meguro.gate-verdict.v1') {
|
|
@@ -2738,9 +3212,9 @@ export function createTools(config) {
|
|
|
2738
3212
|
const recovery = receiptId
|
|
2739
3213
|
? `gate_evaluate({ receiptId: ${JSON.stringify(receiptId)} })`
|
|
2740
3214
|
: 'gate_evaluate({})';
|
|
2741
|
-
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.`);
|
|
2742
3216
|
}
|
|
2743
|
-
return
|
|
3217
|
+
return configuredReceiptGateResult(secretSafe(verdict), 'verdict read');
|
|
2744
3218
|
}
|
|
2745
3219
|
const response = await practiceApi('GET', `/practice/evaluation-suites/${encodeURIComponent(runId)}`);
|
|
2746
3220
|
if (!response.ok && response.status === 404) return legacyGateArtifactNotFoundGuidance(runId);
|
|
@@ -2751,32 +3225,51 @@ export function createTools(config) {
|
|
|
2751
3225
|
}
|
|
2752
3226
|
// Exact pass-through is the parity contract: Console and MCP consume these same bytes as
|
|
2753
3227
|
// an object, with no local policy derivation in this registry.
|
|
2754
|
-
return
|
|
3228
|
+
return configuredReceiptGateResult(verdict, 'legacy verdict read');
|
|
2755
3229
|
}
|
|
2756
3230
|
case 'runs_list': {
|
|
2757
3231
|
const lifecycle = args.lifecycle === undefined ? undefined : requiredString(args, 'lifecycle');
|
|
2758
3232
|
if (lifecycle && !['running', 'paused', 'completed', 'failed', 'cleaning'].includes(lifecycle)) {
|
|
2759
3233
|
throw new Error('lifecycle must be running, paused, completed, failed, or cleaning');
|
|
2760
3234
|
}
|
|
3235
|
+
const query = new URLSearchParams({
|
|
3236
|
+
limit: '200',
|
|
3237
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
3238
|
+
});
|
|
2761
3239
|
return textResult(runsListProjection(
|
|
2762
|
-
await api('GET',
|
|
3240
|
+
await api('GET', `/runs?${query}`, undefined, { auth: true }),
|
|
2763
3241
|
lifecycle,
|
|
2764
3242
|
));
|
|
2765
3243
|
}
|
|
2766
|
-
case 'usage_read':
|
|
2767
|
-
|
|
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
|
+
}
|
|
2768
3265
|
case 'twin_diff': {
|
|
2769
3266
|
const baselineRunId = requiredString(args, 'baselineRunId');
|
|
2770
3267
|
const treatedRunId = requiredString(args, 'treatedRunId');
|
|
2771
3268
|
if (PRACTICE_ATTEMPT_ID.test(baselineRunId) || PRACTICE_ATTEMPT_ID.test(treatedRunId)) {
|
|
2772
3269
|
return practiceAttemptComparisonGuidanceFor(name, baselineRunId, treatedRunId);
|
|
2773
3270
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
`/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`,
|
|
2777
|
-
undefined,
|
|
2778
|
-
{ auth: true },
|
|
2779
|
-
)));
|
|
3271
|
+
const response = await apiRaw('GET', `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`);
|
|
3272
|
+
return response.ok ? textResult(twinDiffProjection(response.json)) : historyReadErrorResult(response);
|
|
2780
3273
|
}
|
|
2781
3274
|
case 'exam_preflight': {
|
|
2782
3275
|
const attemptId = requiredPracticeAttemptId(args);
|
|
@@ -2814,6 +3307,12 @@ export function createTools(config) {
|
|
|
2814
3307
|
expectedRevision: revision,
|
|
2815
3308
|
commandId: `mcp-exam-execute-${examId}-${revision}`,
|
|
2816
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);
|
|
2817
3316
|
} else {
|
|
2818
3317
|
break;
|
|
2819
3318
|
}
|
|
@@ -2834,18 +3333,14 @@ export function createTools(config) {
|
|
|
2834
3333
|
const response = await examRequest(name, 'GET', `/practice-exams/${encodeURIComponent(examId)}`);
|
|
2835
3334
|
if ('error' in response) return response.error;
|
|
2836
3335
|
const projected = examLifecycleProjection(response.value);
|
|
2837
|
-
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)) {
|
|
2838
3340
|
return errorResult(`Shopify Exam ${examId} is ${projected.exam.status}; call exam_status and continue bounded execution before requesting its immutable receipts`);
|
|
2839
3341
|
}
|
|
2840
3342
|
if (projected.receipts.retention?.status === 'expired') {
|
|
2841
|
-
|
|
2842
|
-
return errorResult(
|
|
2843
|
-
retention.tier === 'developer'
|
|
2844
|
-
? '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.'
|
|
2845
|
-
: retention.tier === 'solo'
|
|
2846
|
-
? '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.'
|
|
2847
|
-
: 'Builder keeps receipts available for 365 days. Start a new run to mint a fresh receipt.',
|
|
2848
|
-
);
|
|
3343
|
+
return errorResult(projected.receipts.retention.expiredMessage);
|
|
2849
3344
|
}
|
|
2850
3345
|
if (!projected.receipts.privateAvailable || !projected.receipts.publicAvailable) {
|
|
2851
3346
|
return errorResult(`Shopify Exam ${examId} does not yet carry both immutable receipts`);
|
|
@@ -2868,6 +3363,7 @@ export function createTools(config) {
|
|
|
2868
3363
|
}
|
|
2869
3364
|
case 'practice_run_start': {
|
|
2870
3365
|
const storeId = requiredPracticeStoreId(args);
|
|
3366
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2871
3367
|
const body = {
|
|
2872
3368
|
purpose: 'external-agent',
|
|
2873
3369
|
...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
|
|
@@ -2878,8 +3374,17 @@ export function createTools(config) {
|
|
|
2878
3374
|
...(args.assertionPlan !== undefined ? { assertionPlan: args.assertionPlan } : {}),
|
|
2879
3375
|
...(args.graphqlThrottleMode !== undefined ? { graphqlThrottleMode: args.graphqlThrottleMode } : {}),
|
|
2880
3376
|
};
|
|
2881
|
-
const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body);
|
|
2882
|
-
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
|
+
}
|
|
2883
3388
|
const started = response.json ?? {};
|
|
2884
3389
|
return textResult(secretSafe({
|
|
2885
3390
|
operation: 'start',
|
|
@@ -2905,16 +3410,23 @@ export function createTools(config) {
|
|
|
2905
3410
|
? undefined
|
|
2906
3411
|
: requiredPracticeAttemptId(args, 'receiptId');
|
|
2907
3412
|
if (receiptId) {
|
|
2908
|
-
const
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
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 ?? {};
|
|
2913
3422
|
const receiptStoreId = String(referenced.storeId ?? referenced.worldId ?? '');
|
|
2914
3423
|
if (requestedStoreId && requestedStoreId !== receiptStoreId) {
|
|
2915
3424
|
return practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId);
|
|
2916
3425
|
}
|
|
2917
|
-
|
|
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));
|
|
2918
3430
|
}
|
|
2919
3431
|
|
|
2920
3432
|
const fleet = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
|
|
@@ -2938,12 +3450,19 @@ export function createTools(config) {
|
|
|
2938
3450
|
listed.value,
|
|
2939
3451
|
selectedStoreId,
|
|
2940
3452
|
requestedStoreId ? 'explicit-store-id' : 'single-store',
|
|
3453
|
+
workspaceId,
|
|
2941
3454
|
));
|
|
2942
3455
|
}
|
|
2943
3456
|
case 'practice_run_status': {
|
|
2944
3457
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2945
|
-
const
|
|
2946
|
-
|
|
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
|
+
}
|
|
2947
3466
|
const state = response.json ?? {};
|
|
2948
3467
|
return textResult(secretSafe({
|
|
2949
3468
|
operation: 'status',
|
|
@@ -2954,7 +3473,8 @@ export function createTools(config) {
|
|
|
2954
3473
|
}
|
|
2955
3474
|
case 'practice_run_checkpoint': {
|
|
2956
3475
|
const attemptId = requiredPracticeAttemptId(args);
|
|
2957
|
-
const
|
|
3476
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3477
|
+
const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {}, { workspaceId });
|
|
2958
3478
|
if (!response.ok) {
|
|
2959
3479
|
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
2960
3480
|
? practiceRunUnavailableTeaching(name, attemptId)
|
|
@@ -2965,24 +3485,31 @@ export function createTools(config) {
|
|
|
2965
3485
|
operation: 'checkpoint',
|
|
2966
3486
|
attemptId,
|
|
2967
3487
|
alreadyCaptured: response.json?.alreadyCaptured === true,
|
|
3488
|
+
advanceCursor: response.json?.advanceCursor,
|
|
2968
3489
|
checkpoint: checkpointProjection(response.json?.checkpoint),
|
|
2969
3490
|
...consoleUrlFields(response.json?.consoleRef, response.json?.consoleRefExpiresAt),
|
|
2970
3491
|
}));
|
|
2971
3492
|
}
|
|
2972
3493
|
case 'practice_run_advance': {
|
|
2973
3494
|
const attemptId = requiredPracticeAttemptId(args);
|
|
3495
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
2974
3496
|
const hasDays = args.days !== undefined;
|
|
2975
3497
|
const hasUntil = args.until !== undefined;
|
|
2976
3498
|
if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
|
|
2977
|
-
const
|
|
2978
|
-
const expectedCallSeq = requiredNonNegativeInteger(args, 'expectedCallSeq', -1);
|
|
3499
|
+
const advanceCursor = args.advanceCursor;
|
|
2979
3500
|
if (hasDays && (!Number.isSafeInteger(args.days) || args.days < 1)) throw new Error('days must be a positive integer');
|
|
2980
3501
|
if (hasUntil && (!args.until || typeof args.until !== 'object' || Array.isArray(args.until))) throw new Error('until must be an object');
|
|
2981
3502
|
const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/advance`, {
|
|
2982
|
-
...(hasDays ? { days: args.days } : { until: args.until }),
|
|
3503
|
+
...(hasDays ? { days: args.days } : { until: args.until }),
|
|
3504
|
+
advanceCursor,
|
|
2983
3505
|
...(args.confirmConclusion === true ? { confirmConclusion: true } : {}),
|
|
2984
|
-
});
|
|
2985
|
-
if (!response.ok)
|
|
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
|
+
}
|
|
2986
3513
|
const advanced = response.json ?? {};
|
|
2987
3514
|
return textResult(secretSafe({
|
|
2988
3515
|
operation: 'advance',
|
|
@@ -2997,7 +3524,13 @@ export function createTools(config) {
|
|
|
2997
3524
|
}
|
|
2998
3525
|
case 'practice_run_finish': {
|
|
2999
3526
|
const attemptId = requiredPracticeAttemptId(args);
|
|
3000
|
-
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 });
|
|
3001
3534
|
if (!response.ok) {
|
|
3002
3535
|
const teaching = isPracticeRunUnavailable(response.status, response.json)
|
|
3003
3536
|
? practiceRunUnavailableTeaching(name, attemptId)
|
|
@@ -3013,8 +3546,14 @@ export function createTools(config) {
|
|
|
3013
3546
|
}
|
|
3014
3547
|
case 'practice_run_report': {
|
|
3015
3548
|
const attemptId = requiredPracticeAttemptId(args);
|
|
3016
|
-
const
|
|
3017
|
-
|
|
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
|
+
}
|
|
3018
3557
|
return textResult({
|
|
3019
3558
|
operation: 'report',
|
|
3020
3559
|
...receiptProjection(response.json ?? {}),
|
|
@@ -3023,17 +3562,24 @@ export function createTools(config) {
|
|
|
3023
3562
|
}
|
|
3024
3563
|
case 'practice_run_impact': {
|
|
3025
3564
|
const attemptId = requiredPracticeAttemptId(args);
|
|
3026
|
-
const
|
|
3027
|
-
|
|
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
|
+
}
|
|
3028
3573
|
// T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim
|
|
3029
3574
|
// (no array truncation) rather than re-projecting.
|
|
3030
3575
|
return textResult({ operation: 'impact', ...(response.json ?? {}) });
|
|
3031
3576
|
}
|
|
3032
3577
|
case 'get_connection_details': {
|
|
3033
3578
|
const requestedStoreId = requiredPracticeStoreId(args);
|
|
3579
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3034
3580
|
let details;
|
|
3035
3581
|
try {
|
|
3036
|
-
details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true });
|
|
3582
|
+
details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true, workspaceId });
|
|
3037
3583
|
} catch (error) {
|
|
3038
3584
|
if (error instanceof ToolResultError) throw error;
|
|
3039
3585
|
if (error instanceof Error && error.message.startsWith('404 ')) {
|
|
@@ -3041,8 +3587,8 @@ export function createTools(config) {
|
|
|
3041
3587
|
}
|
|
3042
3588
|
throw error;
|
|
3043
3589
|
}
|
|
3044
|
-
const storeId = details.storeId ??
|
|
3045
|
-
const shopDomain = details.host ?? `${
|
|
3590
|
+
const storeId = details.storeId ?? requestedStoreId;
|
|
3591
|
+
const shopDomain = details.host ?? `${requestedStoreId}.meguro.io`;
|
|
3046
3592
|
return textResult({
|
|
3047
3593
|
identity: details.identity,
|
|
3048
3594
|
// Canonical practice-store id first: pass it straight into practice_run_start({ storeId }).
|
|
@@ -3064,6 +3610,7 @@ export function createTools(config) {
|
|
|
3064
3610
|
}
|
|
3065
3611
|
case 'admin_probe': {
|
|
3066
3612
|
const worldId = requiredWorldId(args);
|
|
3613
|
+
const workspaceId = optionalWorkspaceId(args);
|
|
3067
3614
|
const body = {
|
|
3068
3615
|
query: typeof args.query === 'string' ? args.query : '',
|
|
3069
3616
|
...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
|
|
@@ -3073,7 +3620,7 @@ export function createTools(config) {
|
|
|
3073
3620
|
// MEG-34: always ask the server to mint a durable Console reference for a successful probe.
|
|
3074
3621
|
createConsoleReference: true,
|
|
3075
3622
|
};
|
|
3076
|
-
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 });
|
|
3077
3624
|
if (ok) {
|
|
3078
3625
|
// Replace the internal reference id with a ready-to-open Console URL (opaque ref only).
|
|
3079
3626
|
const { consoleRef, consoleRefExpiresAt, ...envelope } = json;
|
|
@@ -3086,17 +3633,27 @@ export function createTools(config) {
|
|
|
3086
3633
|
return { content: [{ type: 'text', text: JSON.stringify(secretSafe(json), null, 2) }], isError: true };
|
|
3087
3634
|
}
|
|
3088
3635
|
case 'admin_schema': {
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
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
|
+
};
|
|
3096
3651
|
return textResult(await api('POST', '/practice/admin-schema', body, { auth: true }));
|
|
3097
3652
|
}
|
|
3098
3653
|
case 'admin_recipes_list':
|
|
3099
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 }));
|
|
3100
3657
|
default:
|
|
3101
3658
|
return errorResult(`Unknown tool: ${name}`);
|
|
3102
3659
|
}
|
|
@@ -3109,24 +3666,32 @@ export function createTools(config) {
|
|
|
3109
3666
|
meaning: 'The fleet tool arguments were invalid or the request could not be sent.',
|
|
3110
3667
|
nextStep: 'Correct the arguments shown by the tool schema and retry.',
|
|
3111
3668
|
}],
|
|
3112
|
-
});
|
|
3669
|
+
}, undefined, CONDITIONAL_TOOL_BRANCHES.has(name) ? toolInvocationMutates(name, args) : undefined);
|
|
3113
3670
|
}
|
|
3114
|
-
|
|
3671
|
+
const failed = errorResult(error instanceof Error ? error.message : String(error));
|
|
3672
|
+
return typeof error?.operationCommitBoundary === 'string'
|
|
3673
|
+
? withOperationCommitBoundary(failed, error.operationCommitBoundary)
|
|
3674
|
+
: failed;
|
|
3115
3675
|
}
|
|
3116
3676
|
}
|
|
3117
3677
|
|
|
3118
3678
|
async function call(name, args = {}) {
|
|
3119
|
-
const mutates =
|
|
3679
|
+
const mutates = toolInvocationMutates(name, args);
|
|
3120
3680
|
try {
|
|
3121
3681
|
validateTopLevelArguments(name, args);
|
|
3122
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
|
+
}
|
|
3123
3688
|
const result = errorResult(error instanceof Error ? error.message : String(error));
|
|
3124
3689
|
return mutates ? withOperationCommitBoundary(result, 'pre-commit') : result;
|
|
3125
3690
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
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);
|
|
3130
3695
|
}
|
|
3131
3696
|
|
|
3132
3697
|
return { definitions, call };
|