From ca10c3dc3ee4778cb92352a94b5dc947e8ec1fa8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 16:50:01 +0000 Subject: [PATCH 1/3] Lock credit copy to fail-open until 1 Jan 2027 Document that remaining is nonnegative, credits_low is live below 1000, and production does not reject billed commands or return HTTP 402 until LaunchCreditEnforcementAt. Keep 402 mapping and payment_required for after that instant. Do not claim screens stop for empty remaining. Co-authored-by: Gersham Meharg --- AGENTS.md | 16 ++++++++++++---- README.md | 19 +++++++++++++------ src/cli.test.ts | 23 +++++++++++++++++++++++ src/commands.ts | 8 ++++++++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b68f6055..12e81b4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -253,6 +253,8 @@ installation, players, backend services, the site, or production deployment. `passkeys_disabled` belong to that origin; the CLI cannot receive them and must not pretend to map them. The mint call returns `invalid_request`, `unauthorized`, `payment_required`, `rate_limited`, or `not_ready`. + Production does not send `payment_required` (HTTP 402) until LaunchCreditEnforcementAt + (2027-01-01 08:00 UTC / midnight PT). - Writes carry `Idempotency-Key`. An exact retry returns the original link and expiry for twenty-four hours, so a retry is safe and does not mint a second live link. @@ -423,12 +425,18 @@ installation, players, backend services, the site, or production deployment. simply causes the server to derive `hvc1.*` instead. - The default plan has no product storage cap (`account_content_bytes` and `content_limit_bytes` are 0). A custom storage ceiling, when present, is - checked first and rejected with `quota_exceeded`. Remaining prepaid credit of - below 1 whole credit rejects billed `/api/v1` work with `payment_required`. - Keep the 1 GiB local check as a plan-independent transport bound. Keep - `quota_exceeded` and `payment_required` guidance pointing at `account show`. + checked first and rejected with `quota_exceeded`. Remaining prepaid credit is + a nonnegative whole integer; empty remaining displays `0`, never negative. `account show` prints integer `credit_remaining` and does not debit. Remaining below 1000 credits adds `credits_low` to envelope `warnings[]`. + Until LaunchCreditEnforcementAt (2027-01-01 08:00 UTC / midnight PT), + production fails open: billed `/api/v1` work is not rejected for empty + remaining and does not return HTTP 402. Empty remaining does not stop or + shut off screens in this window. After that instant, remaining below 1 + whole credit rejects billed `/api/v1` work with `payment_required`. Keep + mapping 402 to `payment_required` in the client; do not weaken handling of + a real 402. Keep the 1 GiB local check as a plan-independent transport bound. + Keep `quota_exceeded` and `payment_required` guidance pointing at `account show`. Screen toast and screenshot are exempt from the 1-credit API meter. Do not add pay, Stripe, or x402 commands. - Screenshotting is in v1. `screen screenshot ` blocks on a still WebP and diff --git a/README.md b/README.md index 81ccafaf..9e721764 100644 --- a/README.md +++ b/README.md @@ -258,12 +258,18 @@ days first. Filter with `--screen-id`, `--media-id`, and `--day YYYY-MM-DD`. Those identifiers select the caller's own rows and are never a cross-account lookup. -Authenticated responses may carry remaining prepaid credits as an integer. +Authenticated responses may carry remaining prepaid credits as a nonnegative +whole integer. Remaining never displays negative; empty remaining is `0`. Remaining below 1000 credits adds a `credits_low` warning to the envelope -`warnings[]` array; the message includes the integer remaining. A 402 -`payment_required` problem means remaining is below 1 credit and billed -control-plane commands are rejected. Both signals can appear together on a -402 envelope. `account show`, `agent status`, `agent disconnect --yes`, +`warnings[]` array; the message includes the integer remaining. Until +1 January 2027 08:00 UTC (midnight Pacific Time), production fails open: +billed commands are not rejected for empty remaining, and the server does +not return HTTP 402. Empty remaining does not stop or shut off screens in +this window. After that instant, remaining below 1 credit rejects billed +control-plane commands with `payment_required` (HTTP 402). The CLI still +maps a 402 to `payment_required` if the server sends one. Both +`credits_low` and `payment_required` can appear together on a 402 envelope. +`account show`, `agent status`, `agent disconnect --yes`, `auth status`, `auth revoke --yes [--allow-lockout]`, `screen toast`, `screen screenshot`, `compose catalog`, and `compose render` do not debit that meter. Opening `events follow` costs 1 credit. There is no pay command. @@ -307,7 +313,8 @@ dashboard origin, so the CLI never presents a link token and never sees `dashboard_link_expired` or `dashboard_link_consumed`. What the mint call can return is `invalid_request`, `unauthorized`, `payment_required`, `rate_limited`, and `not_ready`; the CLI renders each with the server's own -detail and guidance. +detail and guidance. Production does not send `payment_required` (HTTP 402) +until 1 January 2027 08:00 UTC. Production links require HTTPS. Local development has one exact exception: `http://api.screenrig.localhost:8088` may return and open diff --git a/src/cli.test.ts b/src/cli.test.ts index 5430500a..2b7e7e8b 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,8 +1,10 @@ import assert from "node:assert/strict"; import { createCipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync } from "node:crypto"; import { PassThrough } from "node:stream"; +import { readFileSync } from "node:fs"; import { mkdir, open, readFile, rename, chmod, stat, writeFile, rm } from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { EVENT_STREAM_BACKOFF_CAP_MS, EVENT_STREAM_BACKOFF_MS, USAGE, formatEventLine } from "./commands.js"; import { run, type CliRuntime } from "./main.js"; @@ -3123,6 +3125,27 @@ test("media upload warns on a low-information filename without blocking the uplo } }); +test("customer-facing credit copy is fail-open until 1 Jan 2027", () => { + const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); + const documents: Array<[string, string]> = [ + ["USAGE", USAGE], + ["README.md", readFileSync(path.join(repoRoot, "README.md"), "utf8")], + ["AGENTS.md", readFileSync(path.join(repoRoot, "AGENTS.md"), "utf8")], + ]; + for (const [name, text] of documents) { + assert.match(text, /nonnegative/, `${name} must say remaining is nonnegative`); + assert.match(text, /credits_low/, `${name} must name the live credits_low warning`); + assert.match(text, /1000 credits/, `${name} must name the live 1000-credit warning threshold`); + assert.match(text, /1 Jan(?:uary)? 2027|2027-01-01/, `${name} must name the 1 Jan 2027 cutoff`); + assert.match(text, /fails open/, `${name} must say production fails open until the cutoff`); + assert.match(text, /not rejected for empty remaining/, `${name} must say billed work is not rejected yet`); + assert.match(text, /payment_required/, `${name} must keep payment_required as the after-cutoff code`); + assert.match(text, /402/, `${name} must name HTTP 402 as after-cutoff, not current`); + assert.match(text, /does not stop or shut off screens/, `${name} must not claim screens stop for empty remaining`); + } + assert.match(USAGE, /feedback list \[--kind bug\|feature\]/); +}); + test("credits_low appends beside generic_filename instead of replacing it", async () => { const transport = memoryBackend(); transport.extraResponseHeaders = { "ScreenRig-Credits-Remaining": "999" }; diff --git a/src/commands.ts b/src/commands.ts index 99e38b03..b222cb9c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -215,6 +215,14 @@ Commands: feedback list [--kind bug|feature] doctor [--repair-config] version + +Credits: + Remaining is a nonnegative whole number (never negative; empty remaining + displays 0). Below 1000 credits, authenticated responses may warn + credits_low. Until 1 Jan 2027 08:00 UTC (midnight PT), production fails open: + billed commands are not rejected for empty remaining and do not return + HTTP 402. After that instant, remaining below 1 credit is payment_required. + Empty remaining does not stop or shut off screens in this window. `; export interface CommandResult { From 1c897e1906beb5a48f6167b939cc552ec8c0c07e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 16:50:58 +0000 Subject: [PATCH 2/3] Allow wrapped phrases in the credit copy lock test AGENTS.md wraps 'empty remaining' and 'shut off screens' across lines. Co-authored-by: Gersham Meharg --- src/cli.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 2b7e7e8b..4363aa30 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -3138,7 +3138,7 @@ test("customer-facing credit copy is fail-open until 1 Jan 2027", () => { assert.match(text, /1000 credits/, `${name} must name the live 1000-credit warning threshold`); assert.match(text, /1 Jan(?:uary)? 2027|2027-01-01/, `${name} must name the 1 Jan 2027 cutoff`); assert.match(text, /fails open/, `${name} must say production fails open until the cutoff`); - assert.match(text, /not rejected for empty remaining/, `${name} must say billed work is not rejected yet`); + assert.match(text, /not rejected for empty\s+remaining/, `${name} must say billed work is not rejected yet`); assert.match(text, /payment_required/, `${name} must keep payment_required as the after-cutoff code`); assert.match(text, /402/, `${name} must name HTTP 402 as after-cutoff, not current`); assert.match(text, /does not stop or shut off screens/, `${name} must not claim screens stop for empty remaining`); From a3e11e609a21770e74eadb1b1672c1e4361ad053 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 16:51:08 +0000 Subject: [PATCH 3/3] Match wrapped shut-off phrasing in the credit copy lock test Co-authored-by: Gersham Meharg --- src/cli.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 4363aa30..ceba3941 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -3141,7 +3141,7 @@ test("customer-facing credit copy is fail-open until 1 Jan 2027", () => { assert.match(text, /not rejected for empty\s+remaining/, `${name} must say billed work is not rejected yet`); assert.match(text, /payment_required/, `${name} must keep payment_required as the after-cutoff code`); assert.match(text, /402/, `${name} must name HTTP 402 as after-cutoff, not current`); - assert.match(text, /does not stop or shut off screens/, `${name} must not claim screens stop for empty remaining`); + assert.match(text, /does not stop or\s+shut off screens/, `${name} must not claim screens stop for empty remaining`); } assert.match(USAGE, /feedback list \[--kind bug\|feature\]/); });