diff --git a/Dockerfile b/Dockerfile index 3bd500a..50d7ed6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ARG OPENCLAW_VERSION=2026.7.1 ARG OPENCLAW_BASE_IMAGE=ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} ARG BROKERKIT_PLUGIN_VERSION=0.3.4 ARG BROKERKIT_VERSION=hf-broker/v0.4.2 -ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/huggingface/mlclaw:0.4.8-openclaw-2026.7.1 +ARG MLCLAW_RUNTIME_IMAGE=ghcr.io/huggingface/mlclaw:0.4.9-openclaw-2026.7.1 FROM golang:1.26.5-bookworm AS hf-broker-build ARG BROKERKIT_VERSION diff --git a/dist/mlclaw.mjs b/dist/mlclaw.mjs index 59b57ff..72bcf0e 100755 --- a/dist/mlclaw.mjs +++ b/dist/mlclaw.mjs @@ -15383,7 +15383,7 @@ function nextLink(header) { // src/mlclaw/release-config.generated.ts var RELEASE_CONFIG = { - "packageVersion": "0.4.8", + "packageVersion": "0.4.9", "openclawVersion": "2026.7.1", "brokerkitVersion": "hf-broker/v0.4.2", "brokerkitPluginVersion": "0.3.4", @@ -23346,8 +23346,9 @@ async function update(repoId, opts, hub, hfToken, runtime) { } const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env); const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw"; + let localManifest; if (!canonicalTemplate) { - await ensureUpdateCredentials({ + localManifest = await ensureUpdateCredentials({ repoId, agentName, model: variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL2, @@ -23362,8 +23363,9 @@ async function update(repoId, opts, hub, hfToken, runtime) { token: hfToken, ...runtimeImage ? { runtimeImage } : {} }); + const effectiveRuntimeImage = runtimeImage ?? bundledSpaceRuntimeRef(templateRev); await hub.addSpaceVariable(repoId, "MLCLAW_TEMPLATE_REV", templateRev); - await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_IMAGE", runtimeImage ?? bundledSpaceRuntimeRef(templateRev)); + await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_IMAGE", effectiveRuntimeImage); if (canonicalTemplate) { await hub.addSpaceVariable(repoId, "MLCLAW_CANONICAL_SPACE_ID", canonicalTemplateSpaceId(runtime.env)); await doctor(repoId, { fix: true }, hub, runtime); @@ -23382,8 +23384,36 @@ async function update(repoId, opts, hub, hfToken, runtime) { await ensureSpaceStateVolume(hub, repoId, bucket); } await doctor(repoId, { fix: true }, hub, runtime); + if (!localManifest) { + throw new Error(`local deployment metadata is unavailable for ${repoId}`); + } + await reconcileUpdatedDeployment(localManifest, effectiveRuntimeImage, hub, runtime); runtime.stdout.log(`Space deployment triggered: ${repoId}`); } +async function reconcileUpdatedDeployment(localManifest, runtimeImage, hub, runtime) { + const secrets = await readSecretEnv(runtime.configRoot, localManifest.agent); + await reconcileManifest({ + manifest: { + ...localManifest, + gatewayLocation: "space", + runtimeImage, + updatedAt: runtime.now().toISOString() + }, + bucketPrefix: persistedBucketPrefix(secrets), + hub, + runtime, + apply: async ({ manifest, assertLease }) => { + await assertLease(); + const currentSecrets = await readSecretEnv(runtime.configRoot, manifest.agent); + await writeLocalDeployment(runtime.configRoot, manifest, { + ...currentSecrets, + MLCLAW_GATEWAY_LOCATION: "space", + MLCLAW_RUNTIME_IMAGE: runtimeImage, + MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent) + }); + } + }); +} async function ensureUpdateCredentials(params) { const hasExplicitOverride = params.opts.routerToken !== void 0 || params.opts.routerTokenFile !== void 0; const hasManifest = await manifestExists(params.runtime.configRoot, params.agentName); @@ -23417,6 +23447,7 @@ async function ensureUpdateCredentials(params) { MLCLAW_BROKER_HF_TOKEN: brokerCredential.token, ...routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {} }); + return localManifest; } async function doctor(repoId, opts, hub, runtime) { if (!repoId.includes("/") && await manifestExists(runtime.configRoot, repoId)) { diff --git a/package-lock.json b/package-lock.json index 46d3ab7..80732d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mlclaw", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mlclaw", - "version": "0.4.8", + "version": "0.4.9", "license": "MIT", "dependencies": { "@clack/prompts": "^1.4.0", diff --git a/package.json b/package.json index c0ee614..5456905 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mlclaw", - "version": "0.4.8", + "version": "0.4.9", "license": "MIT", "config": { "openclawVersion": "2026.7.1", diff --git a/src/mlclaw/cli.ts b/src/mlclaw/cli.ts index aa1910a..8a381c1 100644 --- a/src/mlclaw/cli.ts +++ b/src/mlclaw/cli.ts @@ -3790,8 +3790,9 @@ async function update( } const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env); const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw"; + let localManifest: DeploymentManifest | undefined; if (!canonicalTemplate) { - await ensureUpdateCredentials({ + localManifest = await ensureUpdateCredentials({ repoId, agentName, model: variables.get("OPENCLAW_MODEL")?.value ?? DEFAULT_MODEL, @@ -3806,8 +3807,9 @@ async function update( token: hfToken, ...(runtimeImage ? { runtimeImage } : {}), }); + const effectiveRuntimeImage = runtimeImage ?? bundledSpaceRuntimeRef(templateRev); await hub.addSpaceVariable(repoId, "MLCLAW_TEMPLATE_REV", templateRev); - await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_IMAGE", runtimeImage ?? bundledSpaceRuntimeRef(templateRev)); + await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_IMAGE", effectiveRuntimeImage); if (canonicalTemplate) { await hub.addSpaceVariable(repoId, "MLCLAW_CANONICAL_SPACE_ID", canonicalTemplateSpaceId(runtime.env)); await doctor(repoId, { fix: true }, hub, runtime); @@ -3826,9 +3828,43 @@ async function update( await ensureSpaceStateVolume(hub, repoId, bucket); } await doctor(repoId, { fix: true }, hub, runtime); + if (!localManifest) { + throw new Error(`local deployment metadata is unavailable for ${repoId}`); + } + await reconcileUpdatedDeployment(localManifest, effectiveRuntimeImage, hub, runtime); runtime.stdout.log(`Space deployment triggered: ${repoId}`); } +async function reconcileUpdatedDeployment( + localManifest: DeploymentManifest, + runtimeImage: string, + hub: HubApi, + runtime: Required, +): Promise { + const secrets = await readSecretEnv(runtime.configRoot, localManifest.agent); + await reconcileManifest({ + manifest: { + ...localManifest, + gatewayLocation: "space", + runtimeImage, + updatedAt: runtime.now().toISOString(), + }, + bucketPrefix: persistedBucketPrefix(secrets), + hub, + runtime, + apply: async ({ manifest, assertLease }) => { + await assertLease(); + const currentSecrets = await readSecretEnv(runtime.configRoot, manifest.agent); + await writeLocalDeployment(runtime.configRoot, manifest, { + ...currentSecrets, + MLCLAW_GATEWAY_LOCATION: "space", + MLCLAW_RUNTIME_IMAGE: runtimeImage, + MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent), + }); + }, + }); +} + async function ensureUpdateCredentials(params: { repoId: string; agentName: string; @@ -3836,7 +3872,7 @@ async function ensureUpdateCredentials(params: { opts: UpdateOptions; hub: HubApi; runtime: Required; -}): Promise { +}): Promise { const hasExplicitOverride = params.opts.routerToken !== undefined || params.opts.routerTokenFile !== undefined; const hasManifest = await manifestExists(params.runtime.configRoot, params.agentName); if (!hasManifest) { @@ -3871,6 +3907,7 @@ async function ensureUpdateCredentials(params: { MLCLAW_BROKER_HF_TOKEN: brokerCredential.token, ...(routerToken ? { MLCLAW_ROUTER_TOKEN: routerToken } : {}), }); + return localManifest; } async function doctor(repoId: string, opts: DoctorOptions, hub: HubApi, runtime: Required): Promise { diff --git a/src/mlclaw/release-config.generated.ts b/src/mlclaw/release-config.generated.ts index fddd538..6d2125f 100644 --- a/src/mlclaw/release-config.generated.ts +++ b/src/mlclaw/release-config.generated.ts @@ -1,6 +1,6 @@ // Generated from package.json by scripts/sync-release-config.mjs. Do not edit. export const RELEASE_CONFIG = { - "packageVersion": "0.4.8", + "packageVersion": "0.4.9", "openclawVersion": "2026.7.1", "brokerkitVersion": "hf-broker/v0.4.2", "brokerkitPluginVersion": "0.3.4", diff --git a/test/mlclaw.cli.test.ts b/test/mlclaw.cli.test.ts index 9da5390..383aa44 100644 --- a/test/mlclaw.cli.test.ts +++ b/test/mlclaw.cli.test.ts @@ -3108,6 +3108,20 @@ describe("mlclaw CLI", () => { expect(allowedUsersIndex).toBeGreaterThanOrEqual(0); expect(adminsIndex).toBeGreaterThanOrEqual(0); expect(hub.calls.some((call) => call.name === "restartSpace")).toBe(false); + await expect(readManifest(runtime.configRoot, "research")).resolves.toMatchObject({ + desiredGeneration: 1, + gatewayLocation: "space", + runtimeImage: DEFAULT_RUNTIME_IMAGE, + }); + await expect(readSecretEnv(runtime.configRoot, "research")).resolves.toMatchObject({ + MLCLAW_GATEWAY_LOCATION: "space", + MLCLAW_RUNTIME_IMAGE: DEFAULT_RUNTIME_IMAGE, + MLCLAW_RUNTIME_ID: "space-research", + }); + expect(JSON.parse(hub.bucketObjects.get(".mlclaw/desired-state.json") ?? "null")).toMatchObject({ + generation: 1, + runtimeImage: DEFAULT_RUNTIME_IMAGE, + }); }); it("upgrades a legacy Router Space to the broker credential during update", async () => { @@ -3239,6 +3253,13 @@ describe("mlclaw CLI", () => { name: "addSpaceVariable", args: ["alice/research", "MLCLAW_RUNTIME_IMAGE", "bundled:test-template"], }); + await expect(readManifest(runtime.configRoot, "research")).resolves.toMatchObject({ + desiredGeneration: 1, + runtimeImage: "bundled:test-template", + }); + await expect(readSecretEnv(runtime.configRoot, "research")).resolves.toMatchObject({ + MLCLAW_RUNTIME_IMAGE: "bundled:test-template", + }); }); it("honors an explicit runtime image override during update", async () => { @@ -3273,6 +3294,13 @@ describe("mlclaw CLI", () => { name: "addSpaceVariable", args: ["alice/research", "MLCLAW_RUNTIME_ID", "space-research"], }); + await expect(readManifest(runtime.configRoot, "research")).resolves.toMatchObject({ + desiredGeneration: 1, + runtimeImage: "registry.example/mlclaw:new", + }); + await expect(readSecretEnv(runtime.configRoot, "research")).resolves.toMatchObject({ + MLCLAW_RUNTIME_IMAGE: "registry.example/mlclaw:new", + }); }); it("updates the canonical template Space without deployment-only repairs", async () => {