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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tony Simons
2026-03-20 23:00:58 -05:00
committed by GitHub
parent 3572499f5d
commit a5b0895dd8
4 changed files with 26 additions and 4 deletions
+3 -1
View File
@@ -87,11 +87,13 @@ function createGatewayProxy(options) {
logError = (msg, err) => console.error(msg, err), logError = (msg, err) => console.error(msg, err),
} = options || {}; } = options || {};
const { verifyClient } = options || {};
if (typeof loadUpstreamSettings !== "function") { if (typeof loadUpstreamSettings !== "function") {
throw new Error("createGatewayProxy requires loadUpstreamSettings()."); throw new Error("createGatewayProxy requires loadUpstreamSettings().");
} }
const wss = new WebSocketServer({ noServer: true }); const wss = new WebSocketServer({ noServer: true, verifyClient });
wss.on("connection", (browserWs) => { wss.on("connection", (browserWs) => {
let upstreamWs = null; let upstreamWs = null;
+1 -1
View File
@@ -50,9 +50,9 @@ async function main() {
}, },
allowWs: (req) => { allowWs: (req) => {
if (resolvePathname(req.url) !== "/api/gateway/ws") return false; if (resolvePathname(req.url) !== "/api/gateway/ws") return false;
if (!accessGate.allowUpgrade(req)) return false;
return true; return true;
}, },
verifyClient: (info) => accessGate.allowUpgrade(info.req),
}); });
await app.prepare(); await app.prepare();
+21 -1
View File
@@ -6,7 +6,7 @@ import { stripUiMetadata } from "@/lib/text/message-extract";
// regex checks across chat, office, or scene code. // regex checks across chat, office, or scene code.
export type OfficeDeskDirective = "desk" | "release"; export type OfficeDeskDirective = "desk" | "release";
export type OfficeGithubDirective = "github" | "release"; export type OfficeGithubDirective = "github" | "release";
export type OfficeGymDirective = "gym"; export type OfficeGymDirective = "gym" | "release";
export type OfficeQaDirective = "qa_lab" | "release"; export type OfficeQaDirective = "qa_lab" | "release";
export type OfficeStandupDirective = "standup"; export type OfficeStandupDirective = "standup";
export type OfficeCallPhase = "needs_message" | "ready_to_call"; export type OfficeCallPhase = "needs_message" | "ready_to_call";
@@ -156,6 +156,16 @@ const resolveOfficeInteractionDirectiveFromNormalized = (
const resolveOfficeGymSkillDirectiveFromNormalized = ( const resolveOfficeGymSkillDirectiveFromNormalized = (
normalized: string, normalized: string,
): OfficeGymDirective | null => { ): 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 = [ const skillIntentPatterns = [
/\bskills?\b/, /\bskills?\b/,
/\bskills?\s+marketplace\b/, /\bskills?\s+marketplace\b/,
@@ -177,6 +187,16 @@ const resolveOfficeGymSkillDirectiveFromNormalized = (
const resolveOfficeGymCommandDirectiveFromNormalized = ( const resolveOfficeGymCommandDirectiveFromNormalized = (
normalized: string, normalized: string,
): OfficeGymDirective | null => { ): 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 = [ const gymCommandPatterns = [
/\b(?:lets|let's)\s+go\s+to\s+the\s+gym\b/, /\b(?:lets|let's)\s+go\s+to\s+the\s+gym\b/,
/\b(?:lets|let's)\s+go\s+to\s+gym\b/, /\b(?:lets|let's)\s+go\s+to\s+gym\b/,
+1 -1
View File
@@ -1133,7 +1133,7 @@ export const reconcileOfficeAnimationTriggerState = (params: {
}); });
if (skillGymDirective) { if (skillGymDirective) {
skillGymDirectiveKeyByAgentId[agentId] = skillGymDirective.key; skillGymDirectiveKeyByAgentId[agentId] = skillGymDirective.key;
if (skillGymDirective.directive !== "release") { if (skillGymDirective.directive === "gym") {
skillGymHoldByAgentId[agentId] = true; skillGymHoldByAgentId[agentId] = true;
} }
} else if (next.skillGymHoldByAgentId[agentId]) { } else if (next.skillGymHoldByAgentId[agentId]) {