diff --git a/package.json b/package.json index 3893c71..9ace844 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hasna/contracts", - "version": "0.5.0", + "version": "0.5.1", "description": "Shared schemas and validators for Hasna open-source agent infrastructure contracts.", "type": "module", "license": "Apache-2.0", diff --git a/src/client/transport.test.ts b/src/client/transport.test.ts index 82f02c8..9a49536 100644 --- a/src/client/transport.test.ts +++ b/src/client/transport.test.ts @@ -41,6 +41,27 @@ describe("resolveClientTransport — the client-flip contract", () => { expect(JSON.stringify(r)).not.toContain("hasna_todos_abc"); }); + test("FLIP: url+key with NO mode env => inferred cloud-http (fleet-flip contract)", () => { + const r = resolveClientTransport("todos", { + HASNA_TODOS_API_URL: "https://todos.hasna.xyz", + HASNA_TODOS_API_KEY: "hasna_todos_flip", + }); + expect(r.transport).toBe("cloud-http"); + expect(r.mode).toBe("cloud"); + expect(r.baseUrl).toBe("https://todos.hasna.xyz/v1"); + expect(r.modeSource).toBe("HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"); + expect(JSON.stringify(r)).not.toContain("hasna_todos_flip"); + }); + + test("FLIP revert: url present but key removed => back to local (not misconfigured)", () => { + const r = resolveClientTransport("todos", { + HASNA_TODOS_API_URL: "https://todos.hasna.xyz", + }); + expect(r.transport).toBe("local"); + expect(r.mode).toBe("local"); + expect(r.misconfigured).toBe(false); + }); + test("self_hosted alias normalizes to cloud and defaults the host", () => { const r = resolveClientTransport("knowledge", { HASNA_KNOWLEDGE_MODE: "self_hosted", diff --git a/src/client/transport.ts b/src/client/transport.ts index aa32830..ac80185 100644 --- a/src/client/transport.ts +++ b/src/client/transport.ts @@ -25,10 +25,14 @@ // _API_KEY (alias) // // DECISION: transport is `cloud-http` IFF the resolved mode is `cloud` AND an API -// key is present. The base URL defaults to `https://.hasna.xyz` when a key is -// present but no URL is set. If mode is `cloud` but the API key is MISSING, we do -// NOT silently serve wrong local data — we return `local` with a loud `warning` -// and `misconfigured: true` so the caller can hard-fail instead of drifting. +// key is present. The mode is `cloud` when either (a) an explicit mode env resolves +// to cloud, OR (b) no mode env is set but BOTH the API URL and API key are present — +// the fleet env-flip writes exactly those two vars (no STORAGE_MODE), so their joint +// presence is inferred as self_hosted intent. The base URL defaults to +// `https://.hasna.xyz` when a key is present but no URL is set. If mode is +// `cloud` but the API key is MISSING, we do NOT silently serve wrong local data — we +// return `local` with a loud `warning` and `misconfigured: true` so the caller can +// hard-fail instead of drifting. // // SAFETY: this module never returns, logs, or embeds the API key value. Callers // receive only presence flags and env-key names. @@ -143,6 +147,14 @@ export function resolveClientTransport(name: string, env: Env = process.env): Cl `Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`, ); } + } else if (urlHit && keyHit) { + // Flip signal: the fleet env-flip writes EXACTLY HASNA__API_URL + + // HASNA__API_KEY per app and NO explicit STORAGE_MODE (see machines + // FLEET-FLIP.md). Their joint presence IS the self_hosted intent, so infer + // `cloud`. Revert removes both vars, so the client falls back to local. Without + // this, a flipped client with only url+key silently kept reading its local store. + mode = "cloud"; + modeSource = `${urlHit.key}+${keyHit.key}`; } // Local mode: never route to the network, regardless of URL/key presence. diff --git a/src/schemas.ts b/src/schemas.ts index 2b0a493..6c29d0b 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const CONTRACTS_PACKAGE_NAME = "@hasna/contracts"; -export const CONTRACTS_PACKAGE_VERSION = "0.5.0"; +export const CONTRACTS_PACKAGE_VERSION = "0.5.1"; export const SCHEMA_IDS = { actorRef: "hasna.actor_ref.v1",