@langchain/langgraph-cli 1.5.3 → 1.5.5

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.
@@ -18,11 +18,12 @@ import { withAnalytics } from "./utils/analytics.mjs";
18
18
  import { getProjectPath } from "./utils/project.mjs";
19
19
  import { getConfig } from "../utils/config.mjs";
20
20
  import { assembleLocalDeps, configToDocker, getBaseImage, } from "../docker/docker.mjs";
21
+ import { activeDockerHost } from "../docker/compose.mjs";
21
22
  import { DOCKER_NOT_INSTALLED, DOCKER_NOT_RUNNING, isBinaryNotFound, } from "../docker/errors.mjs";
22
23
  import { HostBackendClient, HostBackendError, } from "./utils/host-backend.mjs";
23
24
  import { Emitter, Spinner } from "./utils/deploy-output.mjs";
24
25
  import { createArchive, BYTES_PER_MIB } from "./utils/archive.mjs";
25
- import { API_KEY_ENV_NAMES, DEFAULT_HOST_URL, DEPLOYMENT_NAME_ENV, TERMINAL_STATUSES, envWithoutDeploymentName, findDeploymentIdByName, formatDeploymentsTable, formatLogEntry, formatRevisionsTable, getDeploymentStatusUrl, hasDisallowedBuildCommandContent, isPathWithin, levelColor, normalizeImageTag, normalizeName, secretsFromEnv, smithDashboardBaseUrl, } from "./utils/deploy-helpers.mjs";
26
+ import { API_KEY_ENV_NAMES, DEFAULT_HOST_URL, DEPLOYMENT_NAME_ENV, TERMINAL_STATUSES, envWithoutDeploymentName, findDeploymentIdByName, formatDeploymentsTable, formatLogEntry, formatRevisionsTable, getDeploymentStatusUrl, hasDisallowedBuildCommandContent, isPathWithin, levelColor, normalizeImageTag, normalizeName, resolvePushTarget, secretsFromEnv, smithDashboardBaseUrl, } from "./utils/deploy-helpers.mjs";
26
27
  // ---------------------------------------------------------------------------
27
28
  // Module-level state (mirrors the Python globals `_emitter` / `_no_input`).
28
29
  // ---------------------------------------------------------------------------
@@ -477,8 +478,8 @@ async function printDeploymentResult(client, deploymentId, lastStatus, options)
477
478
  // ---------------------------------------------------------------------------
478
479
  // Docker push auth + digest resolution
479
480
  // ---------------------------------------------------------------------------
480
- async function dockerConfigForToken(registryHost, token) {
481
- const authB64 = Buffer.from(`oauth2accesstoken:${token}`).toString("base64");
481
+ async function dockerConfigForToken(registryHost, token, username) {
482
+ const authB64 = Buffer.from(`${username}:${token}`).toString("base64");
482
483
  const data = { auths: { [registryHost]: { auth: authB64 } } };
483
484
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "langgraph-docker-"));
484
485
  await fs.writeFile(path.join(dir, "config.json"), JSON.stringify(data));
@@ -660,16 +661,17 @@ async function runLocalBuild(args) {
660
661
  throw new Error("Push token response missing token or registry_url");
661
662
  }
662
663
  step += 1;
663
- let normalizedRegistry = registryUrl.replace(/\/+$/, "");
664
- if (normalizedRegistry.includes("://")) {
665
- normalizedRegistry = normalizedRegistry.slice(normalizedRegistry.indexOf("//") + 2);
666
- }
667
664
  const repoSeed = args.imageName || args.name || path.basename(projectDir);
668
- const repoName = normalizeName(repoSeed);
669
- const tagValue = normalizeImageTag(args.tag);
670
- const remoteImage = `${normalizedRegistry}/${repoName}:${tagValue}`;
671
- const registryHost = normalizedRegistry.split("/")[0];
672
- const { dir: cfgDir, cleanup } = await dockerConfigForToken(registryHost, deploymentToken);
665
+ const { image: remoteImage, registryHost, username, } = resolvePushTarget(pushData, normalizeName(repoSeed), normalizeImageTag(args.tag));
666
+ // A config directory has no current context, so Docker would fall back to
667
+ // /var/run/docker.sock; keep talking to the daemon the build used.
668
+ const dockerHost = await activeDockerHost();
669
+ const { dir: cfgDir, cleanup } = await dockerConfigForToken(registryHost, deploymentToken, username);
670
+ const dockerArgs = [
671
+ "--config",
672
+ cfgDir,
673
+ ...(dockerHost ? ["--host", dockerHost] : []),
674
+ ];
673
675
  try {
674
676
  // -- Login --
675
677
  emitter.step(step, `Logging into ${registryHost}`);
@@ -680,7 +682,7 @@ async function runLocalBuild(args) {
680
682
  ...baseOpts,
681
683
  ...stdio,
682
684
  input: tokenInput,
683
- }) `docker --config ${cfgDir} login -u oauth2accesstoken --password-stdin ${registryHost}`;
685
+ }) `docker ${dockerArgs} login -u ${username} --password-stdin ${registryHost}`;
684
686
  step += 1;
685
687
  // -- Tag + push --
686
688
  emitter.step(step, `Pushing image ${remoteImage}`);
@@ -697,7 +699,7 @@ async function runLocalBuild(args) {
697
699
  await $({
698
700
  ...baseOpts,
699
701
  ...stdio,
700
- }) `docker --config ${cfgDir} push ${remoteImage}`;
702
+ }) `docker ${dockerArgs} push ${remoteImage}`;
701
703
  pushSpinner.stop();
702
704
  break;
703
705
  }
@@ -153,6 +153,40 @@ export function normalizeImageTag(value) {
153
153
  }
154
154
  return tag;
155
155
  }
156
+ // Artifact Registry's username for access tokens. Control planes whose registry
157
+ // needs another one return it as `username`.
158
+ const DEFAULT_REGISTRY_USERNAME = "oauth2accesstoken";
159
+ function stripUrlScheme(url) {
160
+ const reference = url.replace(/\/+$/, "");
161
+ return reference.includes("://")
162
+ ? reference.slice(reference.indexOf("//") + 2)
163
+ : reference;
164
+ }
165
+ /**
166
+ * Resolve the push target from a push-token response.
167
+ *
168
+ * When the control plane returns `repository_url`, the image goes to exactly
169
+ * that repository: an ECR repository is a single image name, so nothing may be
170
+ * appended to it. Otherwise the image goes under `registry_url`.
171
+ *
172
+ * @param pushData - The push-token response; `registry_url` must be a string.
173
+ * @param repoName - Normalized image name used under `registry_url`.
174
+ * @param tag - Validated image tag.
175
+ * @returns The image reference, login host, and username.
176
+ */
177
+ export function resolvePushTarget(pushData, repoName, tag) {
178
+ const repository = typeof pushData.repository_url === "string" && pushData.repository_url
179
+ ? stripUrlScheme(pushData.repository_url)
180
+ : `${stripUrlScheme(String(pushData.registry_url))}/${repoName}`;
181
+ const username = typeof pushData.username === "string" && pushData.username
182
+ ? pushData.username
183
+ : DEFAULT_REGISTRY_USERNAME;
184
+ return {
185
+ image: `${repository}:${tag}`,
186
+ registryHost: repository.split("/")[0],
187
+ username,
188
+ };
189
+ }
156
190
  /**
157
191
  * Convert an env dict to a deployment secrets list, dropping reserved and
158
192
  * empty-valued variables.
@@ -209,11 +209,17 @@ export class HostBackendClient {
209
209
  * Request a short-lived registry push token for a local-build deployment
210
210
  * (`POST /v2/deployments/{id}/push-token`).
211
211
  *
212
+ * Tells the control plane this client logs in with the returned `username`
213
+ * and pushes to `repository_url`. LangSmith AWS requires it.
214
+ *
212
215
  * @param deploymentId - Deployment ID.
213
- * @returns A response containing the push `token` and `registry_url`.
216
+ * @returns A response containing the push `token` and `registry_url`, plus
217
+ * `username` and `repository_url` when the registry needs them.
214
218
  */
215
219
  requestPushToken(deploymentId) {
216
- return this.request("POST", `/v2/deployments/${deploymentId}/push-token`);
220
+ return this.request("POST", `/v2/deployments/${deploymentId}/push-token`, {
221
+ payload: { supports_registry_username: true },
222
+ });
217
223
  }
218
224
  /**
219
225
  * Request a signed GCS upload URL for the source tarball used by remote
@@ -185,3 +185,23 @@ export function createCompose(capabilities, options) {
185
185
  }
186
186
  return yaml.stringify(compose, { blockQuote: "literal" });
187
187
  }
188
+ /**
189
+ * The daemon endpoint of the user's active Docker context.
190
+ *
191
+ * Undefined when `DOCKER_HOST` is set, which Docker honors with any config
192
+ * directory. Also undefined when the lookup fails (for example, `DOCKER_CONTEXT`
193
+ * names a context that doesn't exist) or the context has no Docker endpoint
194
+ * (Docker prints `<no value>`); that keeps Docker's own default in place.
195
+ */
196
+ export async function activeDockerHost() {
197
+ if (process.env.DOCKER_HOST)
198
+ return undefined;
199
+ try {
200
+ const { stdout } = await $ `docker context inspect --format ${"{{.Endpoints.docker.Host}}"}`;
201
+ const host = String(stdout).trim();
202
+ return host.includes("://") ? host : undefined;
203
+ }
204
+ catch {
205
+ return undefined;
206
+ }
207
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-cli",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -39,7 +39,7 @@
39
39
  "winston-console-format": "^1.0.8",
40
40
  "yaml": "^2.8.3",
41
41
  "zod": "^3.25.76 || ^4",
42
- "@langchain/langgraph-api": "1.5.3",
42
+ "@langchain/langgraph-api": "1.5.5",
43
43
  "create-langgraph": "1.1.5"
44
44
  },
45
45
  "devDependencies": {