From f3b9895867481b978d0963f69e9e0559e24d9949 Mon Sep 17 00:00:00 2001 From: rmichaelthomas Date: Thu, 28 May 2026 21:06:00 -0700 Subject: [PATCH] fix: retarget Receipts host from receipts.liminate.dev to liminate.dev Receipts was consolidated from the receipts.liminate.dev subdomain onto the apex liminate.dev (single deployment per the liminate-dev backend); API paths are unchanged (/save, /api/v1/export, /keys, /c/) and the product UI now lives at liminate.dev/receipts. The live fix: helper/contract_lifecycle.py hardcoded the dead host in SAVE_URL and RECEIPTS_BASE, so the consent-gated upload POSTed a Bearer token to a host that no longer resolves. Retargeted, with a test pinning the constants and a guard that the helper source carries no dead subdomain. Also swept SKILL.md, README, save-procedure.md, helper/README, docs/LOCAL-ONLY, docs/TRUST-BOUNDARY, the benchmark endpoint, and the bench workflow step name. Bare product-UI references became liminate.dev/receipts; path references just dropped the subdomain. The dated TRUST-BOUNDARY provenance footer keeps the historical receipts.liminate.dev name with a "since consolidated" note rather than rewriting history. liminate-site is superseded by liminate-dev and out of scope here. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/bench.yml | 2 +- README.md | 2 +- SKILL.md | 8 ++++---- benchmarks/bench_list_seeding.py | 2 +- docs/LOCAL-ONLY.md | 2 +- docs/TRUST-BOUNDARY.md | 8 ++++---- helper/README.md | 2 +- helper/contract_lifecycle.py | 6 +++--- references/save-procedure.md | 22 +++++++++++----------- tests/test_contract_lifecycle.py | 15 +++++++++++++-- 10 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 2322954..c2baa2d 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -34,5 +34,5 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Run live roundtrip against receipts.liminate.dev + - name: Run live roundtrip against liminate.dev run: python3 benchmarks/bench_list_seeding.py diff --git a/README.md b/README.md index cd2f024..68f51d4 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ When the user corrects the model's approach — "don't defer," "check the actual At the end of the session, the accumulated `.limn` file is yours. Save it, diff it, hand it to another agent, run it through the interpreter. -The contracts this skill produces can be scanned at [receipts.liminate.dev](https://receipts.liminate.dev) — paste a `.limn` contract (or open a session-end permalink) and Receipts runs it through the interpreter, rendering reasoning state, citation checks, tracked decisions, open questions, and an annotated source view. That's the one-click path from a working session to a rendered inspection. +The contracts this skill produces can be scanned at [liminate.dev/receipts](https://liminate.dev/receipts) — paste a `.limn` contract (or open a session-end permalink) and Receipts runs it through the interpreter, rendering reasoning state, citation checks, tracked decisions, open questions, and an annotated source view. That's the one-click path from a working session to a rendered inspection. ### The session pack diff --git a/SKILL.md b/SKILL.md index aebf1da..56f8cd4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -276,13 +276,13 @@ Supporting reference material: ## Receipts — inspection surface -Receipts (`https://receipts.liminate.dev`) is the hosted inspection surface for session contracts. It runs the contract through the Liminate interpreter with the session pack loaded and renders the result as a seven-section inspection view: reasoning state, warnings, session corrections, tracked decisions, open questions, citation checks, and annotated source. +Receipts (`https://liminate.dev/receipts`) is the hosted inspection surface for session contracts. It runs the contract through the Liminate interpreter with the session pack loaded and renders the result as a seven-section inspection view: reasoning state, warnings, session corrections, tracked decisions, open questions, citation checks, and annotated source. Three ways to use it: -1. **Click the session-end permalink.** The agent saves the contract to Receipts via `POST /save` and presents a short permalink (e.g., `receipts.liminate.dev/c/a7x9k2Bf`). At Tier 1 (no tools), or if a host classifier blocks the agent's call, the agent provides a self-contained `save_receipt.py` for the user to run instead (not a paste-ready curl — pasting a multi-line curl out of chat corrupts the JSON body; see [`references/save-procedure.md`](references/save-procedure.md)). The request uses `$RECEIPTS_API_KEY` to authenticate. If the user hasn't set this up, direct them to receipts.liminate.dev/keys. -2. **Paste manually.** Go to `receipts.liminate.dev`, paste the `.limn` contract, click Run. -3. **Save for later.** After running a contract, click Save to get a short permalink (e.g., `receipts.liminate.dev/c/a7x9k2Bf`) that loads the contract from storage. +1. **Click the session-end permalink.** The agent saves the contract to Receipts via `POST /save` and presents a short permalink (e.g., `liminate.dev/c/a7x9k2Bf`). At Tier 1 (no tools), or if a host classifier blocks the agent's call, the agent provides a self-contained `save_receipt.py` for the user to run instead (not a paste-ready curl — pasting a multi-line curl out of chat corrupts the JSON body; see [`references/save-procedure.md`](references/save-procedure.md)). The request uses `$RECEIPTS_API_KEY` to authenticate. If the user hasn't set this up, direct them to liminate.dev/keys. +2. **Paste manually.** Go to `liminate.dev/receipts`, paste the `.limn` contract, click Run. +3. **Save for later.** After running a contract, click Save to get a short permalink (e.g., `liminate.dev/c/a7x9k2Bf`) that loads the contract from storage. The inspection surface checks `cite` statements by running them through the Liminate interpreter's `substring_check` execution type. The interpreter checks — not the model. A failing `cite` shows as a red ✗ with the interpreter's error message. diff --git a/benchmarks/bench_list_seeding.py b/benchmarks/bench_list_seeding.py index c2028a3..9b9bff0 100644 --- a/benchmarks/bench_list_seeding.py +++ b/benchmarks/bench_list_seeding.py @@ -31,7 +31,7 @@ import urllib.request from typing import Any -DEFAULT_ENDPOINT = "https://receipts.liminate.dev/save" +DEFAULT_ENDPOINT = "https://liminate.dev/save" ADD_RE = re.compile(r'^\s*add\s+"[^"]*"\s+to\s+([a-zA-Z][\w-]*)', re.MULTILINE) REMEMBER_LIST_RE = re.compile( diff --git a/docs/LOCAL-ONLY.md b/docs/LOCAL-ONLY.md index 4a074c1..00d4226 100644 --- a/docs/LOCAL-ONLY.md +++ b/docs/LOCAL-ONLY.md @@ -80,7 +80,7 @@ No server involved. The chain lives in your filesystem or your repository. ## When to add Receipts -Receipts (`receipts.liminate.dev`) adds three things that local mode does not provide: +Receipts (`liminate.dev/receipts`) adds three things that local mode does not provide: - **Permalinks.** A short URL that loads the contract in a rendered inspection view. - **Lineage.** Parent/child relationships between contracts, queryable via API. diff --git a/docs/TRUST-BOUNDARY.md b/docs/TRUST-BOUNDARY.md index 2e52879..91914a7 100644 --- a/docs/TRUST-BOUNDARY.md +++ b/docs/TRUST-BOUNDARY.md @@ -40,7 +40,7 @@ Nothing leaves the machine. No network calls. No telemetry. No phone-home. The i ``` ┌─────────────┐ ┌──────────────────────────┐ ┌───────────────┐ │ .limn file │ ───▶ │ POST /save │ ───▶ │ SQLite on │ -│ (your disk) │ │ receipts.liminate.dev │ │ Railway vol │ +│ (your disk) │ │ liminate.dev │ │ Railway vol │ └─────────────┘ │ + Bearer token │ └───────────────┘ └──────────────────────────┘ │ @@ -49,7 +49,7 @@ Nothing leaves the machine. No network calls. No telemetry. No phone-home. The i (e.g. /c/a7x9k2Bf) ``` -The agent or user sends the full contract source text to `receipts.liminate.dev/save` with a Bearer token (API key) or session cookie (GitHub OAuth). The server runs the contract through its bundled copy of the interpreter, stores the source and results in SQLite, and returns a permalink. +The agent or user sends the full contract source text to `liminate.dev/save` with a Bearer token (API key) or session cookie (GitHub OAuth). The server runs the contract through its bundled copy of the interpreter, stores the source and results in SQLite, and returns a permalink. **What the server receives and stores:** @@ -133,7 +133,7 @@ Session contracts are agent-facing artifacts. When an agent reads a contract — ## Backend security posture -This section states exactly what the Receipts backend (`receipts.liminate.dev`) does and does not provide as of the date at the bottom of this document. It is written for a security reviewer evaluating the hosted service. +This section states exactly what the Receipts backend (`liminate.dev`) does and does not provide as of the date at the bottom of this document. It is written for a security reviewer evaluating the hosted service. **Tenant isolation.** Per-user, not per-organization. Each contract has an `owner_id`. Private contracts are visible only to their owner. There is no team, organization, or workspace model. The `team_id` column exists in the database schema but is unused — it is reserved for future multi-tenant features. @@ -181,4 +181,4 @@ The Receipts server records `liminate_version` and `pack_version` on every saved --- -*This document describes the system as deployed at `receipts.liminate.dev` on May 23, 2026. Interpreter version: 0.10.0. Session pack version: 0.3.0. Receipts API version: 0.3.0.* +*This document describes the system as deployed on May 23, 2026 (then served at `receipts.liminate.dev`, since consolidated to `liminate.dev`). Interpreter version: 0.10.0. Session pack version: 0.3.0. Receipts API version: 0.3.0.* diff --git a/helper/README.md b/helper/README.md index c47f0ff..77364e0 100644 --- a/helper/README.md +++ b/helper/README.md @@ -94,6 +94,6 @@ actually happened. - No `--session-id` → one is generated, recorded in the contract, and printed. - No consent signal → unattended → local-only. - No `$RECEIPTS_API_KEY` → local persistence still succeeds; only the upload - path reports the key is unset (see `receipts.liminate.dev/keys`). + path reports the key is unset (see `liminate.dev/keys`). - `liminate` not importable → `init` validation degrades to a parse check; the contract is still written. diff --git a/helper/contract_lifecycle.py b/helper/contract_lifecycle.py index e7d4b50..16c974c 100644 --- a/helper/contract_lifecycle.py +++ b/helper/contract_lifecycle.py @@ -36,8 +36,8 @@ # Constants # -------------------------------------------------------------------------- -SAVE_URL = "https://receipts.liminate.dev/save" -RECEIPTS_BASE = "https://receipts.liminate.dev" +SAVE_URL = "https://liminate.dev/save" +RECEIPTS_BASE = "https://liminate.dev" # Distinct exit code: an attended save reached the consent gate but no # explicit `--consent upload` was given. The caller (the model, in prose) @@ -452,7 +452,7 @@ def _cmd_save(args) -> int: return 0 if result["decision"] == UploadDecision.LOCAL_ONLY_NO_KEY.value: print("upload skipped: RECEIPTS_API_KEY not set — local-only. " - "Generate a key at receipts.liminate.dev/keys") + "Generate a key at liminate.dev/keys") return 0 if result["needs_confirmation"]: extra = " (sensitive content detected)" if result["sensitive"] else "" diff --git a/references/save-procedure.md b/references/save-procedure.md index 018dec4..8c0eb1f 100644 --- a/references/save-procedure.md +++ b/references/save-procedure.md @@ -50,11 +50,11 @@ the lineage chain. **Tier 2+ (bash/file tools available):** the helper's `save --attended true --consent upload` makes exactly this call for you. Shown here as the contract it fulfils and the manual fallback if the helper is unavailable: ```bash -curl -s -X POST https://receipts.liminate.dev/save \ +curl -s -X POST https://liminate.dev/save \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $RECEIPTS_API_KEY" \ -d '{"source": "", "label": "", "agent_id": "", "session_id": "", "parent_id": ""}' \ - | python3 -c "import sys,json; print('https://receipts.liminate.dev' + json.load(sys.stdin)['contract']['permalink'])" + | python3 -c "import sys,json; print('https://liminate.dev/receipts' + json.load(sys.stdin)['contract']['permalink'])" ``` **Save payload fields:** @@ -69,9 +69,9 @@ curl -s -X POST https://receipts.liminate.dev/save \ All three identity fields are nullable. Omitting them is safe — the contract saves without them. But including them is what makes the receipt answer "who generated this" (agent_id), "in which session" (session_id), and "inheriting from what" (parent_id). A contract without these fields is an orphan — inspectable but not traceable. -`$RECEIPTS_API_KEY` is an environment variable the user sets up once. If the variable is not set, tell the user: "To save contracts to your account, generate an API key at receipts.liminate.dev/keys and run the setup command shown there." +`$RECEIPTS_API_KEY` is an environment variable the user sets up once. If the variable is not set, tell the user: "To save contracts to your account, generate an API key at liminate.dev/keys and run the setup command shown there." -Present the resulting permalink (e.g., `https://receipts.liminate.dev/c/a7x9k2Bf`). +Present the resulting permalink (e.g., `https://liminate.dev/c/a7x9k2Bf`). **Classifier / permission note (Tier 2+).** Whether this call goes through depends on the host's permission mode. The behavior is mode-specific, not universal — do not assume every user must edit settings: @@ -81,7 +81,7 @@ Present the resulting permalink (e.g., `https://receipts.liminate.dev/c/a7x9k2Bf If you hit the auto-mode denial, do **not** try to tunnel around it (wrapping the curl in a python script, a heredoc, or a test runner — the classifier flags that as an Auto-Mode Bypass and the user loses trust). Instead, tell the user to add a one-line allow rule scoped to this exact endpoint: ```json -"Bash(curl -s * https://receipts.liminate.dev/* *)" +"Bash(curl -s * https://liminate.dev/* *)" ``` This single rule covers both the `POST /save` call and the @@ -116,14 +116,14 @@ via `/permissions` or in their `settings.local.json` `allow` array, then restart if session_id: payload["session_id"] = session_id if parent_id: payload["parent_id"] = parent_id body = json.dumps(payload).encode() - req = urllib.request.Request("https://receipts.liminate.dev/save", data=body, method="POST") + req = urllib.request.Request("https://liminate.dev/save", data=body, method="POST") req.add_header("Content-Type", "application/json") req.add_header("Authorization", "Bearer " + (key or "")) try: with urllib.request.urlopen(req, timeout=30) as r: print("HTTP", r.status) data = json.loads(r.read().decode()) - print("PERMALINK:", "https://receipts.liminate.dev" + data["contract"]["permalink"]) + print("PERMALINK:", "https://liminate.dev/receipts" + data["contract"]["permalink"]) except urllib.error.HTTPError as e: print("HTTP", e.code); print(e.read().decode()) ``` @@ -132,7 +132,7 @@ via `/permissions` or in their `settings.local.json` `allow` array, then restart Avoid heredocs (`python3 - <<'PY' … PY`) for the user-run path — a paste that drops the closing delimiter leaves the shell hanging at a `heredoc>` prompt. A file the user runs by path is the robust path. -`$RECEIPTS_API_KEY` is an environment variable the user sets up once. If the variable is not set, tell the user: "To save contracts to your account, generate an API key at receipts.liminate.dev/keys and run the setup command shown there." +`$RECEIPTS_API_KEY` is an environment variable the user sets up once. If the variable is not set, tell the user: "To save contracts to your account, generate an API key at liminate.dev/keys and run the setup command shown there." **Do NOT generate fragment-encoded URLs (`#contract=`) for contracts longer than 5 lines.** The encoding is token-expensive, produces unwieldy URLs, and takes minutes to generate. Fragment URLs are acceptable only for very short demo contracts. For any real session contract, use `POST /save`. @@ -163,7 +163,7 @@ is included, and the agent is the only one who can perform the lookup. 2. Query the user's contract history: ```bash - curl -s https://receipts.liminate.dev/api/v1/export \ + curl -s https://liminate.dev/api/v1/export \ -H "Authorization: Bearer $RECEIPTS_API_KEY" \ | python3 -c " import sys, json @@ -182,7 +182,7 @@ is included, and the agent is the only one who can perform the lookup. prior contract first if its source is available: ```bash - curl -s -X POST https://receipts.liminate.dev/save \ + curl -s -X POST https://liminate.dev/save \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $RECEIPTS_API_KEY" \ -d '{"source": "", "label": ""}' \ @@ -193,7 +193,7 @@ is included, and the agent is the only one who can perform the lookup. **Tier 1 (conversation only):** If a permalink was generated in a prior session, extract the ID from the URL (e.g., -`receipts.liminate.dev/c/HW496KG7` → `HW496KG7`). If no permalink +`liminate.dev/c/HW496KG7` → `HW496KG7`). If no permalink exists and the user cannot run the export query, the lineage link cannot be established — omit `parent_id`. diff --git a/tests/test_contract_lifecycle.py b/tests/test_contract_lifecycle.py index f7d80e5..1f6cf54 100644 --- a/tests/test_contract_lifecycle.py +++ b/tests/test_contract_lifecycle.py @@ -263,7 +263,7 @@ def test_save_only_uploads_on_attended_plus_consent(mod, tmp_path, monkeypatch): posted = [] monkeypatch.setattr(mod, "_upload", lambda src, key, **k: posted.append((src, key)) or - "https://receipts.liminate.dev/c/TESTID") + "https://liminate.dev/c/TESTID") env = {"HOME": str(tmp_path), "XDG_DATA_HOME": "", "LIMINATE_CONTRACTS_DIR": "", "RECEIPTS_API_KEY": "secret-key-present"} contract = 'remember a string called source-state with "verified"\n' @@ -271,7 +271,7 @@ def test_save_only_uploads_on_attended_plus_consent(mod, tmp_path, monkeypatch): consent_upload=True, contract_src=contract, isatty=False) assert len(posted) == 1 assert result["uploaded"] is True - assert result["permalink"] == "https://receipts.liminate.dev/c/TESTID" + assert result["permalink"] == "https://liminate.dev/c/TESTID" def test_save_no_key_never_blocks_local_persist(mod, tmp_path, monkeypatch): @@ -315,6 +315,17 @@ def test_sensitivity_scan_clears_benign_content(mod): # Hygiene: no coupling to any non-public tool, stdlib-only # -------------------------------------------------------------------------- +def test_helper_targets_the_current_receipts_host(mod): + # Receipts moved from the receipts.liminate.dev subdomain to the apex + # liminate.dev (API paths unchanged, UI now at /receipts). + assert mod.RECEIPTS_BASE == "https://liminate.dev" + assert mod.SAVE_URL == "https://liminate.dev/save" + + +def test_helper_has_no_dead_receipts_subdomain(): + assert "receipts.liminate.dev" not in HELPER_PATH.read_text() + + def test_helper_does_not_reference_domain_loader(): text = HELPER_PATH.read_text().lower() assert "domain-loader" not in text