commit c227bd7f6c0f7d41565ed3a1fd4a788ff9476e76 from: Isaac Meerleo date: Tue Jul 21 14:23:13 2026 UTC Accept current Codex tokens and fix named config dump commit - aefa0f7b7f4b2b1a1e2882c03ef8fb5f2f4c32aa commit + c227bd7f6c0f7d41565ed3a1fd4a788ff9476e76 blob - 8014b4dbbe132b6bad147ecf8b5c02a8c1fc7153 blob + b5023a2431dac3c9ef2c8c84710e5f84baf8e910 --- docs/adr/0010-experimental-codex-subscription-compatibility.md +++ docs/adr/0010-experimental-codex-subscription-compatibility.md @@ -41,6 +41,14 @@ drift, expiry, routing regimes, and header injection, one bounded account id. This is not signature verification: the fixed TLS endpoint remains authoritative for token authenticity and entitlement. +As of 2026-07-21, current personal access tokens encode the exact API audience +as either a string or a singleton array and may carry +`chatgpt_compute_residency: "no_constraint"` in the namespaced auth claim. +Those two equivalent no-routing shapes are accepted. Any other audience-array +shape, residency value or location, FedRAMP marker, or routing claim still +fails before network use; Fugu does not derive or guess a routing header from +the token. + The first profile performs no login or refresh. Expiry and HTTP 401/403 fail once with a bounded manual-replacement hint. Automatic refresh is rejected until a separate design can atomically persist rotated credentials without blob - 62ba0f58e01ba2a656f2a119f9ac37a0a7c89274 blob + c22dd7c09bb3884d673c9a8d485196b39b36e143 --- docs/design/codex-subscription.md +++ docs/design/codex-subscription.md @@ -134,10 +134,14 @@ decodes the bounded JWT payload and extracts the one C carried by the token; absence, ambiguity, an unsupported claim shape, or an expired token fails closed before inference. -The first profile supports an ordinary personal account only. If token claims -indicate FedRAMP, data-residency, compute-residency, or another routing regime, -Fugu reports that the account profile is unsupported and sends nothing. It -does not guess or omit a routing header required by that account. +The first profile supports an ordinary personal account only. The exact +audience may be a string or a singleton array containing the same one profile +audience. The namespaced auth claim may omit compute residency or contain the +exact string `chatgpt_compute_residency: "no_constraint"`; both mean that no +routing header is required. Any other audience-array shape, FedRAMP marker, +data-residency claim, compute-residency value or location, or other routing +regime is unsupported and sends nothing. Fugu does not guess or omit a routing +header required by an account. This local JWT parse is not signature verification and must never be described as authentication. Fugu has no reason to import an RSA/JWK stack or use raw blob - 0dcc78adfcf3de859794e024c16eea86f5ea2e1e blob + 96efc6a322c04ca4a0d98b6178ca4dba1959c005 --- regress/codex_subscription/codex_subscription_test.c +++ regress/codex_subscription/codex_subscription_test.c @@ -1378,11 +1378,17 @@ static void test_synthetic_jwt_and_exact_headers(void) { static const char valid_payload[] = - "{\"iss\":\"https://auth.openai.com\",\"aud\":" - "\"https://api.openai.com/v1\",\"exp\":4102444800," + "{\"iss\":\"https://auth.openai.com\",\"aud\":[" + "\"https://api.openai.com/v1\"],\"exp\":4102444800," "\"https://api.openai.com/auth\":{\"chatgpt_plan_type\":\"plus\"," "\"chatgpt_account_id\":\"acct_personal\"," + "\"chatgpt_compute_residency\":\"no_constraint\"," "\"chatgpt_account_is_fedramp\":false}}"; + static const char legacy_payload[] = + "{\"iss\":\"https://auth.openai.com\",\"aud\":" + "\"https://api.openai.com/v1\",\"exp\":4102444800," + "\"https://api.openai.com/auth\":{" + "\"chatgpt_account_id\":\"acct_personal\"}}"; static const char want_suffix[] = "\r\nChatGPT-Account-ID: acct_personal\r\n" "User-Agent: fugu/0.3\r\n" @@ -1413,6 +1419,7 @@ test_synthetic_jwt_and_exact_headers(void) CHECK(strstr(headers, "Cookie:") == NULL && strstr(headers, "originator:") == NULL && strstr(headers, "OpenAI-Beta:") == NULL && + strstr(headers, "x-openai-internal-codex-residency:") == NULL && strstr(headers, "x-openai-internal-codex-responses-lite:") == NULL && strstr(headers, "session") == NULL && strstr(headers, "request-id") == NULL); CHECK(codex_sub_build_headers(small, sizeof(small), &auth, &wire) == -1); @@ -1445,6 +1452,10 @@ test_synthetic_jwt_and_exact_headers(void) CHECK(codex_sub_auth_parse(&auth, malformed.data, malformed.len, 2000000000) == 0); buf_free(&malformed); + synthetic_jwt(&malformed, legacy_payload); + CHECK(codex_sub_auth_parse(&auth, malformed.data, malformed.len, + 2000000000) == 0); + buf_free(&malformed); strlcpy(auth.account_id, "acct", sizeof(auth.account_id)); CHECK(!codex_sub_auth_valid(&auth, 2000000000)); buf_free(&want); @@ -1462,10 +1473,14 @@ test_jwt_rejection_matrix(void) "{\"iss\":\"https://auth.openai.com\",\"aud\":\"other\"," "\"exp\":4102444800,\"https://api.openai.com/auth\":{" "\"chatgpt_account_id\":\"acct\"}}"; - static const char audience_array[] = + static const char audience_array_multiple[] = "{\"iss\":\"https://auth.openai.com\",\"aud\":[" - "\"https://api.openai.com/v1\"],\"exp\":4102444800," + "\"https://api.openai.com/v1\",\"other\"],\"exp\":4102444800," "\"https://api.openai.com/auth\":{\"chatgpt_account_id\":\"acct\"}}"; + static const char audience_array_empty[] = + "{\"iss\":\"https://auth.openai.com\",\"aud\":[]," + "\"exp\":4102444800,\"https://api.openai.com/auth\":{" + "\"chatgpt_account_id\":\"acct\"}}"; static const char expired[] = "{\"iss\":\"https://auth.openai.com\",\"aud\":" "\"https://api.openai.com/v1\",\"exp\":100," @@ -1527,6 +1542,21 @@ test_jwt_rejection_matrix(void) "\"https://api.openai.com/v1\",\"exp\":4102444800," "\"https://api.openai.com/auth\":{\"chatgpt_account_id\":\"acct\"," "\"compute_region\":\"us\"}}"; + static const char required_compute_residency[] = + "{\"iss\":\"https://auth.openai.com\",\"aud\":" + "\"https://api.openai.com/v1\",\"exp\":4102444800," + "\"https://api.openai.com/auth\":{\"chatgpt_account_id\":\"acct\"," + "\"chatgpt_compute_residency\":\"us\"}}"; + static const char misplaced_compute_residency[] = + "{\"iss\":\"https://auth.openai.com\",\"aud\":" + "\"https://api.openai.com/v1\",\"exp\":4102444800," + "\"chatgpt_compute_residency\":\"no_constraint\"," + "\"https://api.openai.com/auth\":{\"chatgpt_account_id\":\"acct\"}}"; + static const char wrong_compute_residency_type[] = + "{\"iss\":\"https://auth.openai.com\",\"aud\":" + "\"https://api.openai.com/v1\",\"exp\":4102444800," + "\"https://api.openai.com/auth\":{\"chatgpt_account_id\":\"acct\"," + "\"chatgpt_compute_residency\":false}}"; struct codex_sub_auth auth; struct buf payload; struct json_out out; @@ -1534,7 +1564,8 @@ test_jwt_rejection_matrix(void) CHECK(auth_rejected(wrong_issuer, 2000000000)); CHECK(auth_rejected(wrong_audience, 2000000000)); - CHECK(auth_rejected(audience_array, 2000000000)); + CHECK(auth_rejected(audience_array_multiple, 2000000000)); + CHECK(auth_rejected(audience_array_empty, 2000000000)); CHECK(auth_rejected(expired, 2000000000)); CHECK(auth_rejected(missing_account, 2000000000)); CHECK(auth_rejected(short_account, 2000000000)); @@ -1548,6 +1579,9 @@ test_jwt_rejection_matrix(void) CHECK(auth_rejected(lf_account, 2000000000)); CHECK(auth_rejected(wrong_account_type, 2000000000)); CHECK(auth_rejected(compute_residency, 2000000000)); + CHECK(auth_rejected(required_compute_residency, 2000000000)); + CHECK(auth_rejected(misplaced_compute_residency, 2000000000)); + CHECK(auth_rejected(wrong_compute_residency_type, 2000000000)); CHECK(codex_sub_auth_parse(&auth, "one.two", 7, 0) == -1); CHECK(codex_sub_auth_parse(&auth, "one.***.three", 13, 0) == -1); CHECK(codex_sub_auth_parse(&auth, "one.two.three.four", 18, 0) == -1); blob - 11cedc584df9a6b0bb4378ac0b72711f6ed68109 blob + a32b30ae1e7d9f8f90ecaba0fef229f563f73f8c --- regress/conf/conf_test.c +++ regress/conf/conf_test.c @@ -108,6 +108,18 @@ parse_fails(const char *content) return (parse_str(content, &alice, NULL) == -1); } +static size_t +count_substr(const char *haystack, const char *needle) +{ + size_t count = 0, len = strlen(needle); + + while ((haystack = strstr(haystack, needle)) != NULL) { + count++; + haystack += len; + } + return (count); +} + /* Churn the heap so freed memory gets junked (verification.md). */ static void churn(void) @@ -527,6 +539,9 @@ test_providers(void) { struct fugu_conf *c; struct fugu_provider *p; + FILE *f; + char *dump = NULL, *work, *fast; + size_t dumpsz = 0; CHECK(parse_ok( "provider \"work\" {\n" @@ -550,6 +565,18 @@ test_providers(void) CHECK(p != NULL && strcmp(p->name, "fast") == 0); CHECK(p != NULL && p->type == PROVIDER_ANTHROPIC); CHECK(p != NULL && strcmp(p->api_port, "8443") == 0); + if ((f = open_memstream(&dump, &dumpsz)) == NULL) + err(1, "open_memstream"); + conf_dump(c, f); + fclose(f); + work = strstr(dump, "provider \"work\" {\n"); + fast = strstr(dump, "provider \"fast\" {\n"); + CHECK(strncmp(dump, "model ", 6) != 0); + CHECK(strstr(dump, "provider \"openai\"\n") == NULL); + CHECK(count_substr(dump, "provider \"work\" {\n") == 1); + CHECK(count_substr(dump, "provider \"fast\" {\n") == 1); + CHECK(work != NULL && fast != NULL && work < fast); + free(dump); conf_free(c); /* a usable flat provider remains slot zero even with named blocks */ blob - 1a3f8a3803e7f54682686df9a69e84053e17b26e blob + 1c51cf6a13e1a8d05141abcc49e134dbbfbd688b --- src/common/codex_sub_wire.c +++ src/common/codex_sub_wire.c @@ -133,6 +133,21 @@ copy_string(const struct json *json, int token, char * } static int +claim_string_equal(const struct json *json, int token, const char *expected) +{ + char value[128]; + int item; + + if (copy_string(json, token, value, sizeof(value)) == 0) + return (strcmp(value, expected) == 0 ? 0 : -1); + if (json_arr_len(json, token) != 1 || + (item = json_arr_get(json, token, 0)) < 0 || + copy_string(json, item, value, sizeof(value)) == -1) + return (-1); + return (strcmp(value, expected) == 0 ? 0 : -1); +} + +static int key_equal(const char *key, size_t len, const char *want) { return (strlen(want) == len && memcmp(key, want, len) == 0); @@ -166,7 +181,7 @@ routing_key(const char *key, size_t len) static int scan_claims(const struct json *json, int token, int depth, int *accounts, - int *routed, int *fedramp_claims) + int *routed, int *fedramp_claims, int *compute_residency_claims) { char *key; size_t keylen; @@ -184,11 +199,14 @@ scan_claims(const struct json *json, int token, int de if (key_equal(key, keylen, "chatgpt_account_is_fedramp")) (*fedramp_claims)++; - if (routing_key(key, keylen)) + if (key_equal(key, keylen, + "chatgpt_compute_residency")) + (*compute_residency_claims)++; + else if (routing_key(key, keylen)) *routed = 1; free(key); if (scan_claims(json, value, depth + 1, accounts, - routed, fedramp_claims) == -1) + routed, fedramp_claims, compute_residency_claims) == -1) return (-1); } return (0); @@ -196,7 +214,8 @@ scan_claims(const struct json *json, int token, int de if ((n = json_arr_len(json, token)) >= 0) { for (i = 0; i < n; i++) if (scan_claims(json, json_arr_get(json, token, i), - depth + 1, accounts, routed, fedramp_claims) == -1) + depth + 1, accounts, routed, fedramp_claims, + compute_residency_claims) == -1) return (-1); } return (0); @@ -233,9 +252,9 @@ parse_claims(struct codex_sub_auth *auth, const void * { const struct codex_sub_profile *profile = codex_sub_profile(); struct json json; - char issuer[128], audience[128]; + char issuer[128], compute_residency[32]; int root, token, claim, accounts = 0, routed = 0, fedramp; - int fedramp_claims = 0, valid = -1; + int fedramp_claims = 0, compute_residency_claims = 0, valid = -1; int64_t expires; if (now < 0 || !json_valid_object(data, len) || @@ -243,14 +262,13 @@ parse_claims(struct codex_sub_auth *auth, const void * return (-1); root = json_root(&json); if (scan_claims(&json, root, 0, &accounts, &routed, - &fedramp_claims) == -1 || + &fedramp_claims, &compute_residency_claims) == -1 || accounts != 1 || routed || (token = obj_unique(&json, root, "iss")) < 0 || copy_string(&json, token, issuer, sizeof(issuer)) == -1 || strcmp(issuer, profile->oauth_issuer) != 0 || (token = obj_unique(&json, root, "aud")) < 0 || - copy_string(&json, token, audience, sizeof(audience)) == -1 || - strcmp(audience, profile->oauth_audience) != 0 || + claim_string_equal(&json, token, profile->oauth_audience) == -1 || (token = obj_unique(&json, root, "exp")) < 0 || json_get_num(&json, token, &expires) == -1 || expires <= now || (claim = obj_unique(&json, root, profile->auth_claim)) < 0 || @@ -265,6 +283,14 @@ parse_claims(struct codex_sub_auth *auth, const void * (token >= 0 && ((fedramp = json_get_bool(&json, token)) == -1 || fedramp))) goto done; + token = obj_unique(&json, claim, "chatgpt_compute_residency"); + if (token == -2 || + compute_residency_claims != (token >= 0 ? 1 : 0) || + (token >= 0 && + (copy_string(&json, token, compute_residency, + sizeof(compute_residency)) == -1 || + strcmp(compute_residency, "no_constraint") != 0))) + goto done; auth->expires_at = expires; valid = 0; blob - 1ef14f37825d275c150fa9163bc9ae33c6a2aaf6 blob + 7be5e751328f33735714ca3cb04dbae3d3588929 --- src/fugu/conf.c +++ src/fugu/conf.c @@ -283,34 +283,37 @@ void conf_dump(const struct fugu_conf *c, FILE *f) { const struct fugu_provider *p; - const struct fugu_provider *active = NULL; const char *host, *port, *path, *key; - if (c->initial_provider_slot > 0) - active = TAILQ_FIRST(&c->providers); - host = active != NULL ? conf_prov_host(active) : conf_api_host(c); - port = active != NULL ? conf_prov_port(active) : conf_api_port(c); - path = active != NULL ? conf_prov_path(active) : conf_api_path(c); - key = active != NULL ? (conf_provider_uses_oauth(active->type) ? - active->oauth_token : active->api_key) : conf_effective_key(c); - - fprintf(f, "model \"%s\"\n", c->model); - fprintf(f, "provider \"%s\"\n", conf_provider_name(c->provider_type)); - if (!conf_provider_endpoint_fixed(c->provider_type)) { - fprintf(f, "api_host \"%s\"\n", host); - fprintf(f, "api_port \"%s\"\n", port); - fprintf(f, "api_path \"%s\"\n", path); + /* A named-only configuration has no implicit slot zero to print. Its + * first block is selected initially, and the blocks below retain source + * order; synthesizing that block here made -n display it twice. */ + if (c->initial_provider_slot == 0) { + host = conf_api_host(c); + port = conf_api_port(c); + path = conf_api_path(c); + key = conf_effective_key(c); + fprintf(f, "model \"%s\"\n", c->model); + fprintf(f, "provider \"%s\"\n", + conf_provider_name(c->provider_type)); + if (!conf_provider_endpoint_fixed(c->provider_type)) { + fprintf(f, "api_host \"%s\"\n", host); + fprintf(f, "api_port \"%s\"\n", port); + fprintf(f, "api_path \"%s\"\n", path); + } + fprintf(f, "%s %s\n", + conf_provider_uses_oauth(c->provider_type) ? + "oauth_token" : "api_key", key != NULL ? REDACTED : + "(not set)"); + if (c->provider_type == PROVIDER_CLAUDE) + fprintf(f, "# experimental Claude subscription profile %s " + "(snapshot %s)\n", CLAUDE_SUB_PROFILE_ID, + CLAUDE_SUB_PROFILE_DATE); + else if (c->provider_type == PROVIDER_CODEX) + fprintf(f, "# experimental Codex subscription profile %s " + "(snapshot %s)\n", codex_sub_profile()->id, + codex_sub_profile()->snapshot_date); } - fprintf(f, "%s %s\n", conf_provider_uses_oauth(c->provider_type) ? - "oauth_token" : "api_key", key != NULL ? REDACTED : "(not set)"); - if (c->provider_type == PROVIDER_CLAUDE) - fprintf(f, "# experimental Claude subscription profile %s " - "(snapshot %s)\n", CLAUDE_SUB_PROFILE_ID, - CLAUDE_SUB_PROFILE_DATE); - else if (c->provider_type == PROVIDER_CODEX) - fprintf(f, "# experimental Codex subscription profile %s " - "(snapshot %s)\n", codex_sub_profile()->id, - codex_sub_profile()->snapshot_date); fprintf(f, "kagi_token %s\n", c->kagi_token != NULL ? REDACTED : "(not set)"); fprintf(f, "max_tokens %lld\n", (long long)c->max_tokens); @@ -370,6 +373,14 @@ conf_dump(const struct fugu_conf *c, FILE *f) } fprintf(f, "\t%s %s\n", conf_provider_uses_oauth(p->type) ? "oauth_token" : "api_key", REDACTED); + if (p->type == PROVIDER_CLAUDE) + fprintf(f, "\t# experimental Claude subscription profile " + "%s (snapshot %s)\n", CLAUDE_SUB_PROFILE_ID, + CLAUDE_SUB_PROFILE_DATE); + else if (p->type == PROVIDER_CODEX) + fprintf(f, "\t# experimental Codex subscription profile " + "%s (snapshot %s)\n", codex_sub_profile()->id, + codex_sub_profile()->snapshot_date); fprintf(f, "}\n"); } } blob - bfc4f08bda7c3ccfcec4f400e53c9daa68b7f6e7 blob + c67bb9e8f4d39179fe5b07178ccb7244c97a758c --- src/fugu/fugu.1 +++ src/fugu/fugu.1 @@ -93,6 +93,8 @@ Parse the configuration, resolve defaults and the firs .Cm match block for the invoking user, print the effective configuration with all secrets redacted, and exit. +When only named Provider blocks are configured, they are printed once in +declaration order; the first remains the initial Provider. .It Fl o Ar format Select machine-readable output. The supported formats are