Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
21 changes: 21 additions & 0 deletions src/client/transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 16 additions & 4 deletions src/client/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@
// <NAME>_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://<app>.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://<app>.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.
Expand Down Expand Up @@ -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_<APP>_API_URL +
// HASNA_<APP>_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.
Expand Down
2 changes: 1 addition & 1 deletion src/schemas.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading