Skip to content

Commit ded0ce1

Browse files
fix(vtex): read MESH_REQUEST_CONTEXT per-request, not from cached factory env (#431)
* fix(vtex): bump @decocms/runtime to ^1.6.2 to restore state propagation The kubernetes-bun rollback in #429 dropped @decocms/runtime from ^1.6.2 back to 1.3.1. With 1.3.1, requests reach the pod with a populated MESH_REQUEST_CONTEXT envelope (token/connectionId/meshUrl all set) but state arrives as an empty object — so state.accountName is null and every tool call fails with "VTEX accountName is missing". Confirmed in the deployed pod logs: hasMeshContext: true, hasToken: true, hasConnectionId: true, hasMeshUrl: true, stateKeys: [], stateAccountNamePresent: false The Workers latency that prompted the revert was startup-CPU-budget specific to Cloudflare Workers, not a Bun problem, so this only bumps the runtime/bindings/sdk versions and keeps the kubernetes-bun deploy and serve()-style entrypoint intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(vtex): read MESH_REQUEST_CONTEXT from runtimeContext, not factory closure Every tool was capturing `env` in its factory closure and reading `env.MESH_REQUEST_CONTEXT` from inside execute. The @decocms/runtime caches tool registrations after the first request (see tools.ts: `let cached: Registrations | null`) and creates a fresh `bindings` env per request — so the captured env is the FIRST request's snapshot, frozen for the lifetime of the pod. When a pod's first request happened to carry an `x-mesh-token` with populated state, every subsequent call reused that captured state (seemingly worked). When the first request was an unauthenticated `tools/list` (e.g. just after a Knative scale-up), every later call saw `state: {}` and failed with "VTEX accountName is missing" — even though studio was correctly forwarding the JWT with the connection's configuration_state. Verified end-to-end: studio's `buildRequestHeaders` mints a JWT containing `state: { accountName, appKey, appToken }` for this connection, the JWT reaches the pod, but the cached tool closure ignores it. The runtime expects `execute` to read per-request env from `runtimeContext.env` (filled from AsyncLocalStorage on every call) — see the comment in @decocms/runtime tools.ts:821 ("Tool *execution* reads per-request context from State (AsyncLocalStorage), so reusing definitions is safe"). Switch all four execute paths (createToolFromOperation + the three custom tools) to read from `runtimeContext.env` and discard the captured factory env. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0eebfc2 commit ded0ce1

4 files changed

Lines changed: 29 additions & 23 deletions

File tree

vtex/server/lib/tool-adapter.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -291,28 +291,22 @@ export interface ToolFromOperationConfig {
291291
export function createToolFromOperation(config: ToolFromOperationConfig) {
292292
const flatInput = flattenRequestSchema(config.requestSchema);
293293

294-
return (env: Env) =>
294+
// The factory's `env` is captured ONCE when the runtime resolves tool
295+
// registrations on the first request, then cached for the process lifetime
296+
// (see @decocms/runtime tools.ts: `let cached: Registrations | null`).
297+
// Reading `env.MESH_REQUEST_CONTEXT` from this closure pins every tool call
298+
// to the FIRST request's context — so an unauthenticated tools/list at pod
299+
// start would poison every subsequent state read with `state: {}`.
300+
// Read per-request env from `runtimeContext` instead — the runtime fills
301+
// it from AsyncLocalStorage on every execute call.
302+
return (_env: Env) =>
295303
createTool({
296304
id: config.id,
297305
description: config.description,
298306
annotations: config.annotations,
299307
inputSchema: flatInput,
300-
execute: async ({ context }) => {
301-
const meshCtx = env.MESH_REQUEST_CONTEXT;
302-
console.log(
303-
`[VTEX] tool=${config.id} mesh-context-shape:`,
304-
JSON.stringify({
305-
hasMeshContext: Boolean(meshCtx),
306-
hasToken: Boolean(meshCtx?.token),
307-
hasConnectionId: Boolean(meshCtx?.connectionId),
308-
hasMeshUrl: Boolean(meshCtx?.meshUrl),
309-
stateKeys: meshCtx?.state ? Object.keys(meshCtx.state) : [],
310-
stateAccountNamePresent: Boolean(
311-
(meshCtx?.state as { accountName?: unknown } | undefined)
312-
?.accountName,
313-
),
314-
}),
315-
);
308+
execute: async ({ context, runtimeContext }) => {
309+
const meshCtx = (runtimeContext.env as Env).MESH_REQUEST_CONTEXT;
316310
const creds = resolveCredentials(meshCtx?.state);
317311
assertValidCredentials(creds, config.id);
318312
const factory = config.clientFactory ?? createVtexClient;

vtex/server/tools/custom/reorder-collection.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,14 +267,18 @@ async function uploadCollectionFile(params: {
267267
}
268268
}
269269

270-
export const reorderCollection = (env: Env) =>
270+
// Read per-request env from `runtimeContext` — see comment in
271+
// lib/tool-adapter.ts for why the factory's captured env is unsafe to read
272+
// inside execute (cached registrations + fresh per-request bindings).
273+
export const reorderCollection = (_env: Env) =>
271274
createTool({
272275
id: "VTEX_REORDER_COLLECTION",
273276
description:
274277
"Overwrite collection contents by removing current SKUs and importing a new ordered SKU list via spreadsheet file.",
275278
inputSchema: reorderCollectionInputSchema,
276279
outputSchema: reorderCollectionOutputSchema,
277-
execute: async ({ context }) => {
280+
execute: async ({ context, runtimeContext }) => {
281+
const env = runtimeContext.env as Env;
278282
const reorderPromise = (async () => {
279283
const credentials = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state);
280284
assertValidCredentials(credentials, "VTEX_REORDER_COLLECTION");

vtex/server/tools/custom/search-collections.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { createTool } from "@decocms/runtime/tools";
22
import { z } from "zod";
33
import type { Env } from "../../types/env.ts";
44

5-
export const searchCollections = (env: Env) =>
5+
// Read per-request env from `runtimeContext` — see comment in
6+
// lib/tool-adapter.ts for why the factory's captured env is unsafe to read
7+
// inside execute (cached registrations + fresh per-request bindings).
8+
export const searchCollections = (_env: Env) =>
69
createTool({
710
id: "VTEX_SEARCH_COLLECTIONS",
811
description: "Search collections by name or other terms.",
912
annotations: { readOnlyHint: true },
1013
inputSchema: z.object({
1114
searchTerms: z.string().describe("Search terms to find collections"),
1215
}),
13-
execute: async ({ context }) => {
16+
execute: async ({ context, runtimeContext }) => {
17+
const env = runtimeContext.env as Env;
1418
const credentials = env.MESH_REQUEST_CONTEXT.state;
1519
const { accountName, appKey, appToken } = credentials;
1620

vtex/server/tools/custom/update-product-specifications.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ const outputSchema = z.object({
3939
productId: z.number().int().positive(),
4040
});
4141

42-
export const updateProductSpecifications = (env: Env) =>
42+
// Read per-request env from `runtimeContext` — see comment in
43+
// lib/tool-adapter.ts for why the factory's captured env is unsafe to read
44+
// inside execute (cached registrations + fresh per-request bindings).
45+
export const updateProductSpecifications = (_env: Env) =>
4346
createTool({
4447
id: "VTEX_UPDATE_PRODUCT_SPECIFICATIONS",
4548
description:
4649
"Replace all specifications for a product. Pass the complete set — values not included are removed. Caller does GET → merge → POST for partial updates. Counterpart of VTEX_GET_PRODUCT_SPECIFICATIONS.",
4750
inputSchema,
4851
outputSchema,
49-
execute: async ({ context }) => {
52+
execute: async ({ context, runtimeContext }) => {
53+
const env = runtimeContext.env as Env;
5054
const credentials = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state);
5155
assertValidCredentials(credentials, "VTEX_UPDATE_PRODUCT_SPECIFICATIONS");
5256
const url = `https://${credentials.accountName}.vtexcommercestable.com.br/api/catalog_system/pvt/products/${context.productId}/specification`;

0 commit comments

Comments
 (0)