Conversation
…t create-time The /rate_limit probe added under #411 succeeds with any valid OAuth token, including tokens whose grant lacks the scope GitHub's search engine requires (`public_repo` for public commit/issue search). That gap let agents persist with a healthy preflight, only to 422 every /search/* call at runtime — every scheduled daily report came back as `422 invalid user/permission` for shipped PRs / in-flight / reviews / issues, and commits degraded to "unrelated global results, not attributable to {user}" (issue #473 production reproduction). Widen `PreflightGitHubProxyAsync` to a two-step probe: keep /rate_limit for 401/403 (OAuth grant revocation), then /search/issues and /search/commits with the bound github_username for 422 (search-API-only failures). On 422 return a structured `github_search_unauthorized` envelope distinct from the existing `github_proxy_access_denied`, so operators can tell the two failure classes apart, and best-effort revoke the freshly minted api-key (same contract as the 403 path under PR #418). Sub-reason classification reads GitHub's body to map "cannot be searched" / "do not have permission" onto a stable `scope_insufficient_or_user_not_found` code; other 422 bodies fall through as `validation_failed`. Tests cover the /search/issues 422 fail-fast path (production-shape body wrapped in NyxIdApiClient's standard envelope), the commits-only 422 case (failure on the second probe alone, since prod #473 reported issues 200ing while commits 422'd), and the empty-200 success path so we don't regress into rejecting legitimate "no recent activity" creations. Closes #474. Related: #473 (user-facing reproduction), #439 (runtime classifier — complementary, not duplicated here). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 903b6c0be1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ($"/search/issues?q=author:{encodedUser}&per_page=1", "/search/issues"), | ||
| ($"/search/commits?q=author:{encodedUser}&per_page=1", "/search/commits"), |
There was a problem hiding this comment.
Probe repo-scoped permissions during GitHub preflight
The new preflight only validates global author queries (author:{username}) via /search/issues and /search/commits, but daily-report runtime switches to repo-qualified queries when a repository allowlist is provided (see AgentBuilderTemplates.cs repo-mode URLs around lines 163-170). This means a token that can search public data but lacks private-repo access can still pass preflight (200/empty on global search) and then fail at runtime for repo:{owner}/{repo} calls, so the change still persists broken agents for private-repo configurations. The preflight should include repo-qualified probes whenever repositories is configured.
Useful? React with 👍 / 👎.
eanzhao
left a comment
There was a problem hiding this comment.
LGTM. Thorough review — no blocking issues found.
What this does: Adds /search/issues and /search/commits probes (with the bound github_username) to the GitHub proxy preflight, catching the 422 failure mode where /rate_limit passes but the OAuth grant lacks public_repo/repo scope. This prevents persisting daily-report agents that would 422 every scheduled run.
Code quality:
IsErrorEnvelopecorrectly extracts the sharedNyxIdApiClient.SendAsyncerror envelope shape, deduplicating the parsing logic betweenClassifyRateLimitProbeFailureandClassifySearchProbeFailure.- Backward compatible: when
githubUsernameis null/empty, search probes are skipped and only/rate_limitruns — same behavior as before. - Error classification is conservative:
ClassifyGitHubSearch422Bodyfalls through tovalidation_failedfor unknown 422 bodies rather than guessing. - All three new tests pass and cover: 422 on
/search/issues, 422 on/search/commitsonly (issues succeed), and empty-200 success (no regression on legitimate "no recent activity" users).
Non-blocking nit: The detail variable in ClassifySearchProbeFailure shadows the local detail output from IsErrorEnvelope — the right-hand ternary uses the local, the left-hand is the anonymous type property. This is correct C# but slightly confusing to read. Consider renaming the local (e.g., envelopeDetail or nyxMessage) for clarity in a follow-up.
eanzhao
left a comment
There was a problem hiding this comment.
Overall: the fix is well-structured — the two-step probe, the refactoring into separate classifiers, and the test coverage for the main 422 paths are all solid. A few gaps worth addressing before merge:
1. Missing test: empty/null username skips search probes
normalizedUser empty returns null immediately, but no test verifies the search probes are never issued when username is absent. The existing 403 test short-circuits at Step 1, so it does not cover this. A dedicated test would protect against a future regression where someone removes the empty guard.
2. Missing test: validation_failed reason_code
Only scope_insufficient_or_user_not_found is exercised. A 422 with a body that does not contain "cannot be searched" (e.g. a query syntax error) should produce validation_failed, but this default branch is untested.
3. {username} in hint is a literal, not interpolated
The hint string contains https://github.com/{username} as a regular string literal (not $""), so it outputs the literal text {username} rather than the actual username. Since github_username is already a field in the response, this is cosmetic, but it would be clearer to either interpolate the actual value or drop the template and reference the field.
4. Sequential search probes (not blocking)
Both probes go through the NyxID proxy to GitHub, each adding a full round-trip. For the create-time path this is acceptable, but if latency becomes a concern, Task.WhenAll would halve the wait on the happy path.
Conflict was structural — dev's PreflightTwitterProxyAsync (#216 / #461) and my refactor of PreflightGitHubProxyAsync into per-step classifiers share the same probe-and-parse skeleton, so git interleaved them. Resolution keeps both methods unchanged: my GitHub classifiers (rate-limit + search) and helpers (IsErrorEnvelope, ClassifyGitHubSearch422Body) sit together, then dev's Twitter preflight, then the shared TryReadInt32Property. Twitter's inline parse is left as-is — DRYing it onto IsErrorEnvelope is out of scope for #474. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## dev #479 +/- ##
=======================================
Coverage 71.03% 71.03%
=======================================
Files 1230 1230
Lines 89062 89062
Branches 11654 11654
=======================================
+ Hits 63266 63269 +3
+ Misses 21259 21258 -1
+ Partials 4537 4535 -2
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
#479) Address review feedback on PR #479: - **Codex P1 (r3152148327)**: probe repo-scoped permissions when a repository allowlist is configured. The runtime daily report runs `repo:{owner}/{repo}+author:{username}` queries (per the repo-mode URL list in AgentBuilderTemplates.cs); the global preflight in this PR's first commit only validated `author:{username}` queries, so a token with public_repo could pass preflight and still 422 every repo-qualified call when the allowlist contained a private repo. After step 2 (global probes), step 3 loops every entry in the allowlist and runs the same /search/issues + /search/commits pair scoped to that repo, fail-fasting with the failing repo embedded in `github_path`. - **eanzhao on PR #479**: cover the fall-through reason_code branch with a dedicated test (422 body without "cannot be searched" markers → `validation_failed`), and pin via assertion that a /rate_limit failure short-circuits BEFORE issuing any /search/* probe (regression guard against future re-orderings of the preflight steps). - **eanzhao non-blocking nit**: rename `detail` local in classifiers to `envelopeMessage` so it no longer shadows the anonymous-type `detail` property; fix a stray `{username}` literal in the search-422 hint that was meant as an interpolation marker but rendered verbatim. `DailyReportTemplateSpec` gains a `Repositories` field so preflight can read the same normalized list the prompt template emits, instead of duplicating the comma-split logic at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed f77d83f7 addressing review feedback: Codex P1 (r3152148327) — repo-scoped permissions probe added. After the global eanzhao review ( eanzhao review (no test for eanzhao review ( eanzhao non-blocking nit ( Skipped ( Tests: 583 pass on |
|
Review follow-up verified on commit
No remaining blocking issues. LGTM. |
…#467) PR #467 (caller-scope unification) renamed IUserAgentCatalogQueryPort methods between when this branch was cut and when CI ran the latest commit: - GetStateVersionAsync(agentId, ct) → GetStateVersionForCallerAsync(agentId, OwnerScope, ct) - GetAsync(agentId, ct) → GetForCallerAsync(agentId, OwnerScope, ct) Migrate the five new tests added in PR #479 (search-422 paths + empty-200 success path + repo-scoped 422 + validation_failed fall-through) to the new contract, and register an ICallerScopeResolver substitute in the service collection so AgentBuilderTool can resolve the caller scope at runtime — same pattern as the existing dev tests that landed under #467. No behavior change. Tests: 638 pass on Aevatar.GAgents.ChannelRuntime.Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR Review: feat(lark-bot): swap typing reaction to done after replyScope Check: DRIFT DETECTEDStated intent (from PR title/description): Swap Lark typing reaction from "Typing" → "DONE" after reply lands. Delivered: The typing→DONE swap feature plus a full revert of the OwnerScope/caller-scoping infrastructure from issue #466, removal of Out-of-scope changes:
CRITICAL Findings[P1] (confidence: 8/10) The PR adds Location: [P1] (confidence: 9/10) var result = await _documentReader.QueryAsync(new ProjectionDocumentQuery { Take = 1000 }, ct);This silently truncates at 1000 entries. The old Location: [P2] (confidence: 8/10) var entries = await queryPort.QueryAllAsync(ct);
return entries
.Where(x => string.IsNullOrWhiteSpace(ownerFilter) || string.Equals(x.OwnerNyxUserId, ownerFilter, StringComparison.Ordinal))
...The old implementation pushed the owner filter into the projection store via Location: INFORMATIONAL Findings[P2] (confidence: 7/10)
[P2] (confidence: 7/10) The action ID constants are now scattered as string literals across [P3] (confidence: 6/10) private static bool Matches(UserAgentCatalogEntry entry, UserAgentCatalogUpsertCommand command) =>
...
&& string.Equals(entry.NyxApiKey, command.NyxApiKey, StringComparison.Ordinal);The [P3] (confidence: 5/10) Typing→DONE race on relay path The relay path does Positive Notes
VerdictThe typing→DONE feature is well-implemented. The OwnerScope revert is the main concern — it reintroduces the |
PR #479 Review — LGTM with minor notes核心修复正确: 确认的改进
测试覆盖6 个测试 pin 了关键行为:
小 note(不阻断)
无阻塞项。 可以合并。 |
Re-review after dev mergeThe three-dot diff (what this PR actually adds beyond Previous critical findings — status:
Remaining feature-only review:No new critical issues found. The typing→DONE implementation is clean:
Nit (informational, confidence 6/10):[P3] In Verdict: LGTM — ready to merge. |
Summary
PreflightGitHubProxyAsyncnow probes/search/issuesand/search/commitswith the boundgithub_usernameafter the existing/rate_limitstep. 422 responses (the "users... cannot be searched..." surface from [Bug] GitHub Daily Report 返回大量 422 invalid user/permission 且提交数据不准确 #473) trigger fail-fast asgithub_search_unauthorized, distinct from the existing 401/403github_proxy_access_deniedenvelope, with a stablereason_codefor operators (scope_insufficient_or_user_not_foundvs.validation_failed)./dailyretries don't accumulate orphan proxy-scoped keys./search/issues422 fail-fast (production-shape envelope), commits-only 422 (failure on the second probe, matching [Bug] GitHub Daily Report 返回大量 422 invalid user/permission 且提交数据不准确 #473's reported degradation), and empty-200 success (so we don't regress into rejecting legitimate "no recent activity" creations).Closes #474. Related: #473 (user-facing reproduction); complementary to #439 (runtime classifier — not duplicated here).
Why this is needed
/rate_limitis scope-light. It returns 200 for any valid OAuth token, including tokens whose grant lackspublic_repo/repo. That gap was invisible to the create-time preflight from #411 / #417, which only checked 401/403. So agents persisted with a healthy preflight, only to 422 every/search/*call at runtime — every scheduled daily report came back as422 invalid user/permissionacross all sections, and commits degraded to "unrelated global results, not attributable to {user}". Catching this at agent-create time is strictly better than catching it after the agent is scheduled, persisted, and has wasted a tool call + Lark notification on every cron tick.Test plan
dotnet build agents/Aevatar.GAgents.Authoring.Lark/Aevatar.GAgents.Authoring.Lark.csproj— cleandotnet test test/Aevatar.GAgents.ChannelRuntime.Tests/...— 561 passed (including 3 new tests for the 422 paths)dotnet test test/Aevatar.AI.Tests/...— 475 passed (coversNyxIdApiClientenvelope shape)bash tools/ci/test_stability_guards.sh— passedbash tools/ci/architecture_guards.sh— passes through the proto/projection/channel/workflow guards;playground_asset_drift_guardexits with the same 254 on a cleandevcheckout (environmental on this worktree, not caused by the change). Verified by stashing the diff and re-running./dailyagainst a NyxID account whose GitHub OAuth grant hasread:useronly — expectgithub_search_unauthorizedwithreason_code: scope_insufficient_or_user_not_foundinstead of a persisted-but-broken agent.🤖 Generated with Claude Code