Skip to content
Draft
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 11 additions & 15 deletions hasna.contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,21 @@
"class": "service",
"contractVersion": "v1",
"kitVersion": "0.5.2",
"description": "Persistent loop and workflow runner for deterministic commands and headless AI coding agents. CLI + MCP + HTTP API (loops-serve) + generated SDK. Canonical deployment modes are local, self_hosted, and cloud; self_hosted runs loops-serve against operator-owned Postgres with API-key auth.",
"description": "Persistent loop and workflow runner for deterministic commands and headless AI coding agents. CLI + MCP + HTTP API (loops-serve) + generated SDK. loops-serve runs against operator-owned PostgreSQL with API-key auth; the CLI client reads either a local SQLite file or the HTTP API.",
"bins": [
"loops",
"loops-mcp",
"loops-serve",
"loops-daemon",
"loops-runner"
],
"deploymentModes": [
"local",
"self-hosted",
"cloud"
],
"serviceSurfaces": [
{
"name": "http",
"status": "supported",
"bin": "loops-serve",
"mcpBin": "loops-mcp",
"authMode": "api-key",
"deploymentModes": [
"self-hosted"
],
"health": {
"method": "GET",
"path": "/health",
Expand Down Expand Up @@ -66,9 +58,17 @@
}
],
"storage": {
"mode": "local",
"backend": "postgresql",
"engines": [
"sqlite",
"postgresql"
],
"envPrefix": "HASNA_LOOPS_",
"sqlitePath": "~/.hasna/loops/loops.db"
"sqlitePath": "~/.hasna/loops/loops.db",
"pgTestGate": {
"envVar": "LOOPS_TEST_DATABASE_URL",
"command": "bun test src/lib/storage/postgres-loop-storage.test.ts"
}
},
"metadata": {
"service": {
Expand All @@ -80,10 +80,6 @@
"apiVersion": "v1",
"auth": "api-key",
"signingSecretSecretRef": "hasna/oss/loops/api-key-signing-secret",
"deploymentModeMapping": {
"contract": "self-hosted",
"runtime": "self_hosted"
},
"databaseDsnBindings": [
{
"purpose": "runtime",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.1083.0",
"@hasna/contracts": "0.5.2",
"@hasna/contracts": "0.10.0",
"@hasna/events": "^0.1.9",
"@modelcontextprotocol/sdk": "^1.29.0",
"@openrouter/ai-sdk-provider": "2.9.1",
Expand Down
35 changes: 18 additions & 17 deletions scripts/check-contract-conformance.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
LOOP_DEPLOYMENT_MODES,
normalizeLoopDeploymentMode,
} from "../src/lib/mode.ts";
import {
formatContractConformance,
repoRoot,
Expand Down Expand Up @@ -47,7 +43,7 @@
({ id }) => id === "bins_match_package",
);

expect(report).toMatchObject({

Check failure on line 46 in scripts/check-contract-conformance.test.mjs

View workflow job for this annotation

GitHub Actions / bun (ubuntu-latest)

error: expect(received).toMatchObject(expected)

{ + "adjudications": [], + "checks": [ + { + "detail": "hasna.contract.json valid for loops (service)", + "id": "manifest_valid", + "status": "pass", + }, + { + "detail": "bins allowlisted: loops, loops-mcp, loops-serve, loops-daemon, loops-runner", + "id": "bins_allowlisted", + "status": "pass", + }, + { + "detail": "declared bins match package.json bin", + "id": "bins_match_package", + "status": "pass", + }, + { + "detail": "missing supported surface declarations or eligible waivers: sdk, cli", + "id": "surface_matrix", + "status": "fail", + }, + { + "detail": "declared surface bins and SDK exports match package.json", + "id": "surface_bindings", + "status": "pass", + }, + { + "detail": "supported API declares GET /health, GET /ready, and GET /version", + "id": "service_api_topology", + "status": "pass", + }, + { + "detail": "self-host deployment artifact present: docker-compose.yml, Dockerfile", + "id": "self_host_artifact", + "status": "pass", + }, + { + "detail": "sqlite and postgresql capabilities plus live-PG gate declared", + "id": "storage_capabilities", + "status": "pass", + }, + { + "detail": "private infrastructure references at metadata.service.signingSecretSecretRef (secret-ref), metadata.service.databaseDsnBindings[0].secretRef (secret-ref), metadata.service.databaseDsnBindings[1].secretRef (secret-ref), metadata.service.databaseDsnBindings[2].secretRef (secret-ref)", + "id": "public_manifest_safety", + "status": "fail", + }, + { + "detail": "user-hosted product story declared", + "id": "hosting_story", + "status": "pass", + }, + { + "detail": "HASNA_LOOPS_STORAGE_MODE was removed. Delete the storage-mode variable; set HASNA_LOOPS_DATABASE_URL to select the postgresql server backend, or leave it unset for sqlite.", + "id": "server_backend_configuration", + "status": "fail", + }, + { + "detail": "health payload invalid: backend Required; <root> Unrecognized key(s) in object: 'mode'", + "id": "health_shape", + "status": "fail", + }, + { + "detail": "metadata.release.artifactScan.script is required for a published package: name the script that scans the PACKED artifact, then wire it into prepack", + "id": "published_artifact_gate", + "status": "fail", + }, + { this is a vendored copy of the @hasna/contracts client seam, not a use of it. A fork does not receive credential-resolution fixes, so it keeps resolving keys from the process environment however many times the shared package is corrected. Import it from @hasna/contracts/client instead.", + "id": "credential_seam_compliance", + "status": "fail", + }, + { + "detail": "no forbidden shared cloud runtime edges", + "id": "no_cloud_guard", + "status": "pass", + }, + ], "class": "service", "name": "loops", - "ok": true, + "official": { + "checks": [ + { + "detail": "hasna.contract.json valid for loops (service)", + "id": "manifest_valid", + "status": "pass", + }, + { + "detail": "bins allowlisted: loops, loops-mcp, loops-serve, loops-daemon, loops-runner", + "id": "bins_allowlisted", + "status": "pass", + }, + { + "detail": "declared bins match package.json bin", + "id": "bins_match_package", + "status": "pass", + }, + { + "detail": "missing supported surface declarations or eligible waivers: sdk, cli", + "id": "surface_matrix", + "status": "fail", + }, + { + "detail": "declared surface bins and SDK exports match package.json", + "id": "surface_bindings", + "status": "pass", + }, + { + "detail": "supported API declares GET /health, GET /ready, and GET /version", +

Check failure on line 46 in scripts/check-contract-conformance.test.mjs

View workflow job for this annotation

GitHub Actions / bun (macos-latest)

error: expect(received).toMatchObject(expected)

{ + "adjudications": [], + "checks": [ + { + "detail": "hasna.contract.json valid for loops (service)", + "id": "manifest_valid", + "status": "pass", + }, + { + "detail": "bins allowlisted: loops, loops-mcp, loops-serve, loops-daemon, loops-runner", + "id": "bins_allowlisted", + "status": "pass", + }, + { + "detail": "declared bins match package.json bin", + "id": "bins_match_package", + "status": "pass", + }, + { + "detail": "missing supported surface declarations or eligible waivers: sdk, cli", + "id": "surface_matrix", + "status": "fail", + }, + { + "detail": "declared surface bins and SDK exports match package.json", + "id": "surface_bindings", + "status": "pass", + }, + { + "detail": "supported API declares GET /health, GET /ready, and GET /version", + "id": "service_api_topology", + "status": "pass", + }, + { + "detail": "self-host deployment artifact present: docker-compose.yml, Dockerfile", + "id": "self_host_artifact", + "status": "pass", + }, + { + "detail": "sqlite and postgresql capabilities plus live-PG gate declared", + "id": "storage_capabilities", + "status": "pass", + }, + { + "detail": "private infrastructure references at metadata.service.signingSecretSecretRef (secret-ref), metadata.service.databaseDsnBindings[0].secretRef (secret-ref), metadata.service.databaseDsnBindings[1].secretRef (secret-ref), metadata.service.databaseDsnBindings[2].secretRef (secret-ref)", + "id": "public_manifest_safety", + "status": "fail", + }, + { + "detail": "user-hosted product story declared", + "id": "hosting_story", + "status": "pass", + }, + { + "detail": "HASNA_LOOPS_STORAGE_MODE was removed. Delete the storage-mode variable; set HASNA_LOOPS_DATABASE_URL to select the postgresql server backend, or leave it unset for sqlite.", + "id": "server_backend_configuration", + "status": "fail", + }, + { + "detail": "health payload invalid: backend Required; <root> Unrecognized key(s) in object: 'mode'", + "id": "health_shape", + "status": "fail", + }, + { + "detail": "metadata.release.artifactScan.script is required for a published package: name the script that scans the PACKED artifact, then wire it into prepack", + "id": "published_artifact_gate", + "status": "fail", + }, + { this is a vendored copy of the @hasna/contracts client seam, not a use of it. A fork does not receive credential-resolution fixes, so it keeps resolving keys from the process environment however many times the shared package is corrected. Import it from @hasna/contracts/client instead.", + "id": "credential_seam_compliance", + "status": "fail", + }, + { + "detail": "no forbidden shared cloud runtime edges", + "id": "no_cloud_guard", + "status": "pass", + }, + ], "class": "service", "name": "loops", - "ok": true, + "official": { + "checks": [ + { + "detail": "hasna.contract.json valid for loops (service)", + "id": "manifest_valid", + "status": "pass", + }, + { + "detail": "bins allowlisted: loops, loops-mcp, loops-serve, loops-daemon, loops-runner", + "id": "bins_allowlisted", + "status": "pass", + }, + { + "detail": "declared bins match package.json bin", + "id": "bins_match_package", + "status": "pass", + }, + { + "detail": "missing supported surface declarations or eligible waivers: sdk, cli", + "id": "surface_matrix", + "status": "fail", + }, + { + "detail": "declared surface bins and SDK exports match package.json", + "id": "surface_bindings", + "status": "pass", + }, + { + "detail": "supported API declares GET /health, GET /ready, and GET /version", +
ok: true,
repoRoot,
name: "loops",
Expand Down Expand Up @@ -100,7 +96,7 @@
expect(manifest.metadata.conformance?.binCompatibilityWaivers).toBeUndefined();
});

test("declares the exact package, deployment, and split DSN contracts", () => {
test("declares the exact package, storage, and split DSN contracts", () => {
const packageJson = readJson(join(repoRoot, "package.json"));
const manifest = readJson(join(repoRoot, "hasna.contract.json"));
const httpSurface = manifest.serviceSurfaces.find(
Expand All @@ -124,18 +120,23 @@
types: "./dist/api/index.d.ts",
import: "./dist/api/index.js",
});
expect(httpSurface.deploymentModes).toEqual(["self-hosted"]);
expect(serviceMetadata.deploymentModeMapping).toEqual({
contract: "self-hosted",
runtime: "self_hosted",
});
expect(
LOOP_DEPLOYMENT_MODES.includes(
normalizeLoopDeploymentMode(
serviceMetadata.deploymentModeMapping.runtime,
),
),
).toBe(true);
// The local|self_hosted|cloud placement axis was removed from the contract
// schema. These assert the dead vocabulary stays gone rather than pinning it.
expect(manifest.deploymentModes).toBeUndefined();
expect(httpSurface.deploymentModes).toBeUndefined();
expect(httpSurface.deploymentMode).toBeUndefined();
expect(serviceMetadata.deploymentModeMapping).toBeUndefined();

// The only technical switch is the SERVER data backend. `loops-serve` opens
// Postgres unconditionally — src/serve/index.ts resolves the runtime DSN and
// throws when it is absent, and imports no sqlite storage — so the active
// backend is postgresql. sqlite remains a supported engine for the CLI client,
// which is what `engines` records.
expect(manifest.storage.mode).toBeUndefined();
expect(manifest.storage.backend).toBe("postgresql");
expect(manifest.storage.engines).toEqual(["sqlite", "postgresql"]);
expect(serveSource).toContain("HASNA_LOOPS_DATABASE_URL");
expect(serveSource).not.toContain("storage/sqlite");

expect(manifest.storage.databaseUrlSecretRef).toBeUndefined();
expect(
Expand Down
Loading