From a5b0895dd8e18ba40704234102927fb3e3b4cd3d Mon Sep 17 00:00:00 2001 From: Tony Simons Date: Fri, 20 Mar 2026 23:00:58 -0500 Subject: [PATCH] Fix WS auth + gym release directive TypeScript error (#16) * Fix WS auth: wire accessGate.allowUpgrade via verifyClient The allowWs callback was never actually calling accessGate.allowUpgrade during the WS handshake - the ws library passes (info) not (req), and verifyClient must be set on the WebSocketServer constructor options. Fix: pass verifyClient to WebSocketServer constructor and wrap allowUpgrade to extract info.req. Co-Authored-By: Claude Opus 4.6 * Fix TypeScript error and add gym release directive support Add "release" value to OfficeGymDirective type for symmetry with OfficeQaDirective ("qa_lab" | "release"). Previously OfficeGymDirective was only "gym" with no release state, making the "!== 'release'" check in eventTriggers.ts dead code that TypeScript flagged as an unintentional comparison. Changes: - deskDirectives.ts: add "release" to OfficeGymDirective type - deskDirectives.ts: add gym release patterns to skill and command directive resolvers (e.g. "leave the gym", "done with skills") - eventTriggers.ts: change !== "release" to === "gym" for clarity and consistency with reduceOfficeGymHoldState pattern This fixes: https://github.com/iamlukethedev/Claw3D/issues/15 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- server/gateway-proxy.js | 4 +++- server/index.js | 2 +- src/lib/office/deskDirectives.ts | 22 +++++++++++++++++++++- src/lib/office/eventTriggers.ts | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/server/gateway-proxy.js b/server/gateway-proxy.js index 79f124f..40ffc15 100644 --- a/server/gateway-proxy.js +++ b/server/gateway-proxy.js @@ -87,11 +87,13 @@ function createGatewayProxy(options) { logError = (msg, err) => console.error(msg, err), } = options || {}; + const { verifyClient } = options || {}; + if (typeof loadUpstreamSettings !== "function") { throw new Error("createGatewayProxy requires loadUpstreamSettings()."); } - const wss = new WebSocketServer({ noServer: true }); + const wss = new WebSocketServer({ noServer: true, verifyClient }); wss.on("connection", (browserWs) => { let upstreamWs = null; diff --git a/server/index.js b/server/index.js index 1f689de..06c965d 100644 --- a/server/index.js +++ b/server/index.js @@ -50,9 +50,9 @@ async function main() { }, allowWs: (req) => { if (resolvePathname(req.url) !== "/api/gateway/ws") return false; - if (!accessGate.allowUpgrade(req)) return false; return true; }, + verifyClient: (info) => accessGate.allowUpgrade(info.req), }); await app.prepare(); diff --git a/src/lib/office/deskDirectives.ts b/src/lib/office/deskDirectives.ts index f20deac..5ec462b 100644 --- a/src/lib/office/deskDirectives.ts +++ b/src/lib/office/deskDirectives.ts @@ -6,7 +6,7 @@ import { stripUiMetadata } from "@/lib/text/message-extract"; // regex checks across chat, office, or scene code. export type OfficeDeskDirective = "desk" | "release"; export type OfficeGithubDirective = "github" | "release"; -export type OfficeGymDirective = "gym"; +export type OfficeGymDirective = "gym" | "release"; export type OfficeQaDirective = "qa_lab" | "release"; export type OfficeStandupDirective = "standup"; export type OfficeCallPhase = "needs_message" | "ready_to_call"; @@ -156,6 +156,16 @@ const resolveOfficeInteractionDirectiveFromNormalized = ( const resolveOfficeGymSkillDirectiveFromNormalized = ( normalized: string, ): OfficeGymDirective | null => { + const gymSkillReleasePatterns = [ + /\bleave\s+(?:the\s+)?gym\b/, + /\bexit\s+(?:the\s+)?gym\b/, + /\bdone\s+(?:with\s+(?:the\s+)?)?(?:gym|skill(?:\s+building)?)\b/, + /\bstop\s+(?:working\s+on\s+)?skills?\b/, + /\bleave\s+the\s+skill(?:\s+building)?(?:\s+room)?\b/, + ]; + if (gymSkillReleasePatterns.some((pattern) => pattern.test(normalized))) { + return "release"; + } const skillIntentPatterns = [ /\bskills?\b/, /\bskills?\s+marketplace\b/, @@ -177,6 +187,16 @@ const resolveOfficeGymSkillDirectiveFromNormalized = ( const resolveOfficeGymCommandDirectiveFromNormalized = ( normalized: string, ): OfficeGymDirective | null => { + const gymCommandReleasePatterns = [ + /\bleave\s+(?:the\s+)?gym\b/, + /\bexit\s+(?:the\s+)?gym\b/, + /\bdone\s+(?:with\s+(?:the\s+)?)?gym\b/, + /\bstop\s+gym(?:ning)?\b/, + /\bleave\s+skill(?:\s+building)?(?:\s+room)?\b/, + ]; + if (gymCommandReleasePatterns.some((pattern) => pattern.test(normalized))) { + return "release"; + } const gymCommandPatterns = [ /\b(?:lets|let's)\s+go\s+to\s+the\s+gym\b/, /\b(?:lets|let's)\s+go\s+to\s+gym\b/, diff --git a/src/lib/office/eventTriggers.ts b/src/lib/office/eventTriggers.ts index c132a8b..76b16f9 100644 --- a/src/lib/office/eventTriggers.ts +++ b/src/lib/office/eventTriggers.ts @@ -1133,7 +1133,7 @@ export const reconcileOfficeAnimationTriggerState = (params: { }); if (skillGymDirective) { skillGymDirectiveKeyByAgentId[agentId] = skillGymDirective.key; - if (skillGymDirective.directive !== "release") { + if (skillGymDirective.directive === "gym") { skillGymHoldByAgentId[agentId] = true; } } else if (next.skillGymHoldByAgentId[agentId]) {