commit - aefa0f7b7f4b2b1a1e2882c03ef8fb5f2f4c32aa
commit + c227bd7f6c0f7d41565ed3a1fd4a788ff9476e76
blob - 8014b4dbbe132b6bad147ecf8b5c02a8c1fc7153
blob + b5023a2431dac3c9ef2c8c84710e5f84baf8e910
--- docs/adr/0010-experimental-codex-subscription-compatibility.md
+++ docs/adr/0010-experimental-codex-subscription-compatibility.md
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
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
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"
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);
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);
"{\"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,"
"\"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;
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));
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
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)
{
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"
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
}
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);
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;
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);
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);
{
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) ||
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 ||
(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
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);
}
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
.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