From ac989b695092555bcdf59755120b4a96258f7c65 Mon Sep 17 00:00:00 2001 From: Ho Lim Date: Thu, 9 Jul 2026 15:44:03 -0700 Subject: [PATCH 1/2] fix(registry): drop legacy credential hash rows Signed-off-by: Ho Lim --- src/lib/state/registry.ts | 2 ++ test/registry.test.ts | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/lib/state/registry.ts b/src/lib/state/registry.ts index a3647e77486..c70180479de 100644 --- a/src/lib/state/registry.ts +++ b/src/lib/state/registry.ts @@ -443,10 +443,12 @@ function serializeSandboxEntryForDisk(entry: SandboxEntry): SandboxEntry { const { recoveredFromGateway: _recovered, livePhase: _phase, + providerCredentialHashes: _legacyProviderCredentialHashes, ...durable } = entry as SandboxEntry & { recoveredFromGateway?: boolean; livePhase?: string | null; + providerCredentialHashes?: unknown; }; const messaging = serializeSandboxMessagingStateForDisk(durable.messaging); const mcp = serializeSandboxMcpStateForDisk(durable.mcp); diff --git a/test/registry.test.ts b/test/registry.test.ts index 1e73e922996..f91dd4d8ca4 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -852,6 +852,62 @@ describe("registry", () => { expect(data.sandboxes.messaging.messagingChannelConfig).toBeUndefined(); }); + it("drops legacy providerCredentialHashes when rewriting messaging rows (#3631)", () => { + const basePlan = makeMessagingPlan("messaging", ["telegram"]); + const binding = { + channelId: "telegram", + credentialId: "telegramBotToken", + sourceInput: "botToken", + providerName: "messaging-telegram-bridge", + providerEnvKey: "TELEGRAM_BOT_TOKEN", + placeholder: "openshell:resolve:env:TELEGRAM_BOT_TOKEN", + credentialAvailable: true, + credentialHash: "new-hash", + }; + fs.mkdirSync(path.dirname(regFile), { recursive: true }); + fs.writeFileSync( + regFile, + `${JSON.stringify( + { + sandboxes: { + messaging: { + name: "messaging", + messaging: { + schemaVersion: 1, + plan: { + ...basePlan, + credentialBindings: [binding], + }, + }, + providerCredentialHashes: [ + { + channel: "telegram", + credentialHashes: { TELEGRAM_BOT_TOKEN: "old-hash" }, + }, + ], + }, + }, + defaultSandbox: "messaging", + }, + null, + 2, + )}\n`, + ); + + registry.updateSandbox("messaging", { model: "new-model" }); + + const data = JSON.parse(fs.readFileSync(regFile, "utf-8")); + expect(data.sandboxes.messaging.providerCredentialHashes).toBeUndefined(); + expect(data.sandboxes.messaging.messaging.plan.credentialBindings).toEqual([ + { + channelId: "telegram", + providerEnvKey: "TELEGRAM_BOT_TOKEN", + credentialAvailable: true, + credentialHash: "new-hash", + }, + ]); + }); + it("imageTag defaults to null when not provided", () => { registry.registerSandbox({ name: "no-tag" }); const sb = registry.getSandbox("no-tag"); From 3a09e94f7935f24eeb44bfd1e3fbd811a64088ba Mon Sep 17 00:00:00 2001 From: Ho Lim Date: Fri, 10 Jul 2026 11:35:15 -0700 Subject: [PATCH 2/2] fix(registry): document stripped credential hash state Signed-off-by: Ho Lim --- src/lib/state/registry.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/state/registry.ts b/src/lib/state/registry.ts index c70180479de..0898a2fd1b9 100644 --- a/src/lib/state/registry.ts +++ b/src/lib/state/registry.ts @@ -432,14 +432,13 @@ function normalizeSandboxEntryForRuntime(entry: SandboxEntry): SandboxEntry { /** * Prepare a sandbox entry for persistence: normalize messaging state and drop - * transient #5714 display-only markers (`recoveredFromGateway`, `livePhase`) + * transient #5714 display-only markers plus legacy provider credential hashes * that must never reach sandboxes.json. */ function serializeSandboxEntryForDisk(entry: SandboxEntry): SandboxEntry { - // #5714: defensively drop transient, display-only recovery markers so they - // can never reach sandboxes.json even if a caller force-passed one through - // updateSandbox(). These are not part of the durable SandboxEntry type; they - // live only on the ephemeral list-recovery rows. + // Defensively drop non-durable recovery markers and legacy + // providerCredentialHashes so they can never reach sandboxes.json even if a + // caller force-passed them through updateSandbox(). const { recoveredFromGateway: _recovered, livePhase: _phase,