Repository navigation
1.9.79: a key refused for a scope names it and what grants it - #303
Merged
Merged
Conversation
A key keeps the scope it was made with, so every AI connection approved before a release let connections do more (the text ad and redirector writes #260 and #265 add) is refused there with a 403 whose body said nothing a program could read, and the CLI's hint, the one the MCP tools hand the assistant, was to mint a key with --scope write, which a hosted connection cannot use. API: Auth::requireScope() and the staged-change write check throw InsufficientScopeException (an AuthException, so every existing catch is unchanged). The dispatcher answers it through Support\ScopeRefusal: the body keeps error, message and status, the message keeps its first sentence and ends with the remedy (so a reader that knows no field, an older CLI or the hosted engine, passes it on), and it adds code (insufficient_scope), required_scope, key_scope, key_source and remedy, and for a connection connect_access. key_source is read from the refused key's label once the request is refused (AiConnect is the only writer of a label; a test holds that), so requests that pass pay nothing; a failed read is "unknown" and gets both remedies, never "a plain key". connect_access is the least access whose key covers the scope, or the key's own access when that is more, so a write connection is never told to choose less; null when no connection's key can hold it (users, system, sync). A role's 403 answers as before. Go CLI: api.ScopeRefusalOf reads the fields, or an older install's message, and HintFor names the scope and either `p202 user apikey create <user_id> --scope <this key's scope plus the missing one>` or connecting the app again with the access the install names. Under the hosted engine every scope refusal says to connect again, since its key is the connection's. `p202 mcp` and the hosted engine hand that envelope to the assistant. Tests: ScopeRefusalTest (unit), ScopeRefusalServedIntegrationTest (through index.php under php -S: a plain key and an old-scope connection on PUT /ppc-networks, a staged write, text-ads and users writes, and a role refusal), StagedChangesControllerTest; go: scope_refusal_test.go in api and cmd (exit code and hint through a command, a hosted mcp call, the stdio MCP server, labels held to AiConnect.php), and TestHostedScopeRefusalSaysConnectAgain under js/wasm (added to CI's list). An agent-eval case (setup-source-002) and its reference-agent branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
Contributor
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog: E0GQ
Why
A key keeps the scope it was made with. 1.9.79 adds write areas an AI connection may hold (#260 text ads, #265 redirectors), so every hosted connection approved before them is refused on its first such write. The 403 named the scope only in prose, the CLI's hint (the one
p202 mcpand the hosted engine hand the assistant) was to mint a key with--scope write, which a hosted connection cannot use, and nothing said to connect again.What changed
API (
api/v3)Auth::requireScope()and the staged-change write check (an area that cannot be told) throwInsufficientScopeException, a subclass ofAuthException, so every existingcatch (AuthException)behaves as before.hasScope()'s rule moved to a staticAuth::scopesSatisfy()it calls, so the same rule can be asked of what a connection would mint.Support\ScopeRefusal. The body keepserror,messageandstatus. The message keeps its first sentence (Insufficient API key scope for this operation: requires '…' (key has: …).), so a reader matching it still matches, and ends with the remedy, so a reader that knows none of the fields (an older CLI, the PHP CLI, the hosted engine at any version) passes the remedy on. Added fields:code: insufficient_scope,required_scope,key_scope,key_source(ai_connection/api_key/unknown),remedy, and unlesskey_sourceisapi_key,connect_access.key_sourceis read from the refused key's own row (label, which onlyAiConnectwrites:AI: <app> via mcp.tracking202.com), scoped to the user it authenticated, and only once a request is refused, so requests that pass pay no query. A failed read logs and answersunknown, which gets both remedies, never "a plain key".connect_access(AiConnect::accessToApprove()) is the least access whose key covers the scope, or the key's own access when that is more, so a "Read and change" connection is never told to choose "Read only";nullwhen no connection's key can hold the scope (users, system, sync,write), and the remedy then says reconnecting would not add it.AiConnect::ACCESS_LABELSnames the three accesses as the approval page does; the page now reads them from there.Admin access required., a missing permission) is unchanged:error,message,statusonly.Nothing is answered that the caller does not already hold: the scope it needed, its own key's scope (already in the message), whether its own key is a connection, and the install's rule for what a connection may hold. No other key or account is named.
Go CLI
api.ScopeRefusalOf()reads the fields, or an older install's message (every version words itrequires '<scope>' (key has: …)).api.HintFornames the scope and eitherp202 user apikey create <user_id> --scope <this key's scope plus the missing one>(a key's scope cannot be changed) or connecting the app again with the access the install names;unknowngets both. Exit code stays 2 (auth).mcpHostedRun) every scope refusal's hint is to connect again, whatever the install said of the key: its key is the connection's, and that is the one remedy its user has.p202 mcpand the hosted engine hand that envelope (message with the install's remedy, hint with the CLI's) to the assistant unchanged.Hosted relay. The relay's own code (the Cloudflare Worker at mcp.tracking202.com) is not in this repository. What this repository can do is done: the engine the relay runs (
p202 mcp call, js/wasm) puts the remedy in the error envelope it writes, and the install puts it in the message too, for an engine of any version. What it cannot do: make the relay pass that envelope through unchanged (the docs say the relay records "the error when one fails", so it reads it), or have the relay offer a reconnect link fromconnect_access.Docs:
docs/openapi.yaml(aScopeRefusalschema for the sharedForbiddenresponse, the scopes intro),documentation/api/00-api-integrations.md(a "A key without the scope" section, the 403 row the status table lacked, the error-response section),docs/cli.md(errors),docs/cli-agent.md(the auth row),documentation/cli/10-go-cli.md(errors and the hosted MCP paragraph), both changelogs. In passing,docs/cli.md's example hint anddocs/cli-agent.md's auth row saidp202 config get, which is not a command; they sayp202 config show, as the CLI does.Tests
tests/Api/V3/ScopeRefusalTest.php(unit): the exception carries scope and key scopes; a role refusal is not one; each source's remedy and fields; never a lesser access; scopes no connection can hold;unknown;keySource()from the label, before the label column, for a removed key and a failed read;accessCovering()against what each access mints; the labels are the page's; and a structural check thatAiConnectis the only writer of a key's label (the assumptionkey_sourcerests on).tests/Api/V3/ScopeRefusalServedIntegrationTest.php(@group integration, throughapi/v3/index.phpunderphp -Son a scratch database): a plaincampaigns:readkey and an old-scope connection (approved "Read and change" before traffic sources could be changed) onPUT /ppc-networks/{id}, the same connection staging it, a connection onPOST /text-adsandPOST /users, a current connection's write passing, and a role refusal answered as before. The text-ads case readsAiConnectfor what to expect, so it holds before and after 1.9.79: text_ad_create, text_ad_update and text_ad_delete in the MCP connector #260 (run both ways here, by addingtext-adstoWRITE_AREASin a scratch copy).StagedChangesControllerTest: the "area cannot be told" refusal is anInsufficientScopeExceptionnamingwrite.internal/api/scope_refusal_test.go: reading fields and an older message, other 403s not read as one, the hint per source, the class hint kept when the 403 names no scope.cmd/scope_refusal_test.go:p202 ppc-network updateagainst a fake install answering the 1.9.79 bodies and an older one:exitCodeForError2,hintFor, the envelope; a hostedmcp call(mcpHostedRun) says connect again for all three; the labels held toAiConnect.php.mcp_serve_test.go: the stdio MCP server hands the assistant the envelope.hosted_js_test.goTestHostedScopeRefusalSaysConnectAgain: the relay's whole entry under js/wasm, added togo-cli.yml's list.setup-source-002-a-key-without-the-scope-is-told-which-and-howand its reference-agent branch.cmp): PHP 1) the dispatcher's branch, 2)requireScopethrowing a plainAuthException(unit and served), 3)keySourceanswering every key as plain (unit and served), 4) a failed read answeringapi_key, 5) the remedy left out of the message, 6) the access to choose ignoring what the key holds, 7) a connection told to make a key, 8) the staged-change check throwing a plainAuthException. Go 1)HintForwithout the refusal's hint, 2) the hosted override, 3) a connection told to make a key, 4) an older install's message not read, 5) the key's scope not carried into the new one, 6) a label that differs from the page's, 7)connect_access: nullread as not said, 8) a non-install 403 withrequires '…'read as a refusal (this one first went uncaught; a case was added).Verification
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed(schema tier on a scratch database), verbatim:Unit: 6118 tests, 5 skipped (the 4 "User class requires DB singleton" skips, and
CronEntryPointsFailLoudlyTest, which skips in a checkout holding a202-config.php, as this one did for the live pass). Outside the script:ScopeRefusalServedIntegrationTest(--group integration, 5 tests, against the live instance's database), and the js/wasm tests CI runs (go-cli.yml's two-runlists,TestHostedScopeRefusalSaysConnectAgainincluded) under Node withgo_js_wasm_exec.Live pass
On a local instance of this branch (
php -S, MariaDB 10.11), installed byinstall-instance.sh's own steps without its licence form (the licence stub is down): the schema, the account and its REST key made by the same callsinstall.phpmakes once the key is confirmed.campaigns:readkey and a labelled old-scope connection onPUT /ppc-networks/1(403 with the fields above,api_key/ai_connection,connect_access: write), the connection onPOST /text-ads(connect_access: nullon this base, before 1.9.79: text_ad_create, text_ad_update and text_ad_delete in the MCP connector #260) and staging the rename (ppc-networks:stage, still "Read and change").p202 --profile narrow ppc-network update 1 …exit 2 with the scope hint; following it,p202 user apikey create 1 --scope campaigns:read,ppc-networks:read,ppc-networks:writewith the admin key,config set-key, and the same update succeeded. With the connection's key:p202 ppc-network updateandp202 mcp call ppc_network_updategave the connect-again hint.GOOS=js GOARCH=wasmbuild run under Node withP202_HOSTED_URL/P202_HOSTED_API_KEY,mcp call ppc_network_updateagainst the instance: exit 2, envelope with the connect-again hint (and with a plain key, the hosted override).p202 eval run(same proxy): mechanical checks pass,needs_judgewithout a judge; with a judge that answers PASS, 1/1, reply quoting the scope and the hint.Could not run
actionlint(tier SKIP: not installed; running a downloaded copy was refused here). The workflow change is one test name added to a-runlist; the file parses as YAML.For the reviewer
key_sourcerests on the label. A key AI connect minted between deploying 1.9.78's files and running its upgrade has no label and reads asapi_key; in the hosted engine the hint is to connect again anyway.connect_accessis computed from the install's currentAiConnectlists, so on this base (before 1.9.79: text_ad_create, text_ad_update and text_ad_delete in the MCP connector #260) a connection writing text ads is told no connection can holdtext-ads:write; once 1.9.79: text_ad_create, text_ad_update and text_ad_delete in the MCP connector #260 merges it is told to connect again with "Read and change".Generated by Claude Code