Fix #2066: collapse duplicate AgentRole into single source of truth - #2082
Conversation
shared/egg_restrictions/patterns.py defined its own AgentRole class that mirrored egg_contracts.agent_roles.AgentRole, with a comment asking readers to "keep values in sync" — enforced only by unit tests. PR #2061 surfaced the failure mode: it added REVIEWER_SECURITY and REVIEWER_CONCURRENCY to the canonical enum but not the gateway- side mirror, breaking CI. Replace the duplicate with a re-export of the canonical StrEnum so new roles propagate automatically. Trim the now-tautological cross- sync tests to a single identity assertion as a tripwire against any future re-introduction of a parallel enum. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Main added REVIEWER_SECURITY and REVIEWER_CONCURRENCY to the local AgentRole mirror in shared/egg_restrictions/patterns.py (PR #2061). This PR deletes that mirror entirely and re-exports the canonical StrEnum from egg_contracts — which already carries those new roles — so the conflict resolves to HEAD's version. The added roles still propagate, just through the canonical enum instead of the doomed copy.
Conflict Resolution SummaryResolved merge conflict with
Why this was clean
Verification
Please review: Nothing surprising — the resolution preserves the PR's original intent exactly. The merge commit can be reverted cleanly if needed. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review of PR #2082
Verified the change against the broader codebase. The PR cleanly collapses the duplicate AgentRole definition in shared/egg_restrictions/patterns.py into a re-export of the canonical egg_contracts.agent_roles.AgentRole StrEnum. The tripwire identity tests are well-targeted.
Verified safe
- No circular import:
egg_contracts/agent_roles.pyonly imports from stdlib and.roles;egg_contracts/has no references toegg_restrictions(grep -r egg_restrictions shared/egg_contractsreturns nothing). The newegg_restrictions → egg_contractsedge is one-way. - Class→
StrEnumis semantically transparent for every existing call site I traced:AGENT_PATTERNS.get(role.lower())inshared/egg_restrictions/checker.py:25— works becauseStrEnummembers hash equal to theirstrvalues.AGENT_GH_RESTRICTIONS.get(role_lower)ingateway/agent_restrictions.py:209— same.f"Agent role '{role}'..."strings ingateway/agent_restrictions.py:163,191,215— Python 3.11+StrEnum.__str__returns the value, so error messages stay identical.tool_interceptor.py:121iteration overAGENT_PATTERNS.items()and the surroundingf"...'{owner_role}'..."— same.- Confirmed empirically:
f"{AgentRole.CODER}" == "coder",isinstance(AgentRole.CODER, str) is True,hash(AgentRole.CODER) == hash("coder"), dict lookup with raw strings works.
- Gateway runtime:
gateway/Dockerfile:71-77already copiesshared/egg_contracts/into/app/andpip installspydantic. The new import works in the gateway image. - Sandbox runtime:
sandbox/Dockerfileshipsegg_contractsviaCOPY . /opt/egg-runtime/+PYTHONPATH=/opt/egg-runtime/shared, andpydanticis installed in stage 2. Fine. - No remaining duplicates:
grep -rn '^class AgentRole' shared orchestrator gatewayshows only the canonical class inegg_contracts/agent_roles.py(models.py:460isAgentRoleType, unrelated). The two tripwires are sufficient because there are only two re-export sites. - Pre-existing failure called out in the test plan reproduces:
TestRoleSyncWithOrchestratorModels.test_all_canonical_roles_in_orchestrator_modelsfails withModuleNotFoundError: No module named 'models'— bare-namefrom models importonly resolves when orchestrator's conftest is loaded. Unrelated to this PR.
Non-blocking observations
-
shared/pyproject.toml(egg-shareddistribution) doesn't declareegg-contractsas a dependency. That file'sdependencies = ["pyyaml>=6.0", "anthropic>=0.50,<1.0", "httpx>=0.25.0", "markdownify>=0.13.1"]does not includeegg-contracts, butegg_restrictions.patternsnow imports from it. After this PR, anyone whopip installsegg-sharedstandalone getsImportErroron first import ofegg_restrictions.patterns. The monorepo install path (uv syncagainst rootpyproject.toml, which lists bothshared/egg_contractsandshared/egg_restrictionsintool.hatch.build.targets.wheel.packages) is unaffected, and I can't find any consumer that installsegg-sharedin isolation — so this is latent rather than active. Consider either addingegg-contractsto theegg-shareddeps for hygiene, or adding a comment inshared/pyproject.tomlnoting thategg_restrictionsrequiresegg_contractsto be co-installed. -
Import surface widens:
from egg_contracts.agent_roles import AgentRoletriggersegg_contracts/__init__.py, which eagerly importspydantic,agent_recovery,audit,dependency_graph,feedback,hitl,loader,models,orchestration,orchestrator,phase_defaults,plan_parser,resilience,roles,validator. Every gateway and sandbox process that touchesegg_restrictions.patternsnow pays this cost on cold start. Not a correctness issue (those processes already importegg_contracts), but worth flagging if startup latency matters anywhere it didn't before. Afrom egg_contracts.agent_roles import AgentRolewritten as a deferred / lazy import would not avoid this — the package__init__runs on the first sub-module import regardless. The clean fix is unrelated to this PR. -
Tripwire scope: the
assert AgentRole is SharedAgentRoleidentity check is good against re-introduced parallel classes, but does not catch a subtler regression where someone replaces the re-export withAgentRole = SharedAgentRole.__class__(...)or aLiteralalias. That's a paranoid case; current tests are appropriate for the threat model.
Approving
The PR delivers exactly what #2066 asked for, with the right tests. Pre-existing failures called out in the PR body are not regressions. CI is green.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
egg_restrictions.patterns now imports AgentRole from egg_contracts. The egg-shared distribution didn't declare the dependency, so a standalone 'pip install egg-shared' would ImportError on first use of egg_restrictions.patterns. Monorepo install is unaffected (root pyproject.toml installs both packages together), so this is hygiene rather than a live regression. Addresses non-blocking observation #1 from PR #2082 review.
There was a problem hiding this comment.
Thanks for the thorough verification. Addressing the observations:
-
shared/pyproject.tomlmissingegg-contractsdep — fixed in 101f39e. Added"egg-contracts"to theegg-shareddependencieslist. Monorepo install is unaffected; this just makes the standalone-install failure mode loud at install time instead of silent until first import. -
Wider import surface from
egg_contracts/__init__.py— leaving as-is. As you noted, the gateway and sandbox processes already importegg_contracts, and a deferred import wouldn't help (package__init__runs on first sub-module import regardless). The clean fix is inegg_contracts/__init__.py, which is out of scope here. -
Tripwire scope — leaving as-is. Agreed it's appropriate for the threat model; the paranoid
Literal-alias case isn't worth the test churn.
— Authored by egg
There was a problem hiding this comment.
Re-review of PR #2082
Verified the delta since 7d0c45c — exactly one file changed:
shared/pyproject.toml | 1 +/-1
The commit (101f39e) adds "egg-contracts" to the egg-shared dependencies list, addressing non-blocking observation #1 from my previous review. CI is green.
Verification
- Diff is minimal:
gh api repos/jwbron/egg/compare/7d0c45c...101f39e --jq '.files[] | .patch'confirms a single addition to the dependencies array. No Python code changed; the AgentRole consolidation work from the prior review is untouched. - Dep declaration is correct:
shared/egg_contracts/pyproject.toml:2declares the distribution name asegg-contracts, matching the new entry. - Monorepo install unaffected: Root
pyproject.tomlpackagesshared/egg_contractsdirectly viatool.hatch.build.targets.wheel.packages. Sandbox and gateway DockerfilesCOPYthe module rather than installing throughshared/pyproject.toml. Confirmed no install path exercisesshared/pyproject.tomlin isolation (grep -rn "pip install.*shared"finds nothing). - Author's framing is accurate: This makes the standalone-install failure mode loud at install time instead of silent at first import. The
shared/pyproject.tomlfile is latent in current usage, so this is hygiene rather than a live regression — exactly as the commit message states. - Other two observations correctly deferred: The eager-import-surface point (#2) is genuinely out of scope (the fix lives in
egg_contracts/__init__.py). The tripwire-scope point (#3) is the paranoid case I flagged as appropriate-for-threat-model in the original review. Both deferrals are well-reasoned.
Non-blocking observation (carried-forward, not introduced by this commit)
The new dep entry has no version constraint ("egg-contracts"), unlike its siblings (pyyaml>=6.0, anthropic>=0.50,<1.0, etc.). And there is no [tool.uv.sources] or workspace mapping pointing egg-contracts at shared/egg_contracts/ — egg-contracts is not on PyPI either. So if a hypothetical consumer ever did pip install egg-shared standalone, they would now get No matching distribution found for egg-contracts instead of a deferred ImportError. That's still broken — just broken differently. Since no consumer does that today, this remains latent. Mentioning for the record; not blocking.
Approving
The fix is minimal, targeted, and correctly addresses observation #1. Previous approval still stands.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Acknowledged. The carried-forward observation is correct — Leaving as-is in this PR for two reasons:
Filing this as a follow-up rather than expanding scope here. Thanks for the careful re-review. — Authored by egg |
|
egg feedback addressed. View run logs 5 previous review(s) hidden. |
Closes #2066.
shared/egg_restrictions/patterns.pydefined a parallelAgentRoleclass that mirroredegg_contracts.agent_roles.AgentRole, with a doc comment asking readers to keep values in sync — enforced only by unit tests intests/. PR #2061 (#1965) surfaced the failure mode: it addedREVIEWER_SECURITYandREVIEWER_CONCURRENCYto the canonical enum but not the gateway-side mirror, and CI caught the drift ontest_role_values_match_shared_libraryandtest_all_canonical_roles_in_gateway.(The issue framed the duplicate as living in
gateway/agent_restrictions.py; that file actually re-exportsAgentRolefromegg_restrictions.patterns, so the real second copy is inshared/egg_restrictions/patterns.py. This PR fixes the actual file.)Changes
shared/egg_restrictions/patterns.py: drop the localclass AgentRole; re-export the canonicalStrEnumfromegg_contracts.agent_rolesso new roles propagate automatically.tests/gateway/test_agent_restrictions.py,tests/shared/egg_contracts/test_agent_roles.py: trim the role-value parity tests (now tautological) to a single identity assertion (assert AgentRole is SharedAgentRole) that trips if anyone reintroduces a parallel enum.The chosen approach is option 1 from the issue. egg_restrictions and egg_contracts are co-installed in the same wheel and have no other dependency between them, so the new
egg_restrictions → egg_contractsimport direction is clean and introduces no cycle. All consumers ofegg_restrictions.patterns.AgentRoleuse class-attribute access (AgentRole.CODER), which works identically for the StrEnum.Test plan
make test— 4 failures are all pre-existing flakes onmain(3 event-loop config issues intests/llm/claude/test_runner.py, 1 timing-flakytest_session_expires_exactly_at_boundarythat passes in isolation). None touchAgentRole.tests/gateway/test_agent_restrictions.py,tests/shared/egg_contracts/test_agent_roles.py,shared/tests/test_egg_restrictions.py,gateway/tests/test_agent_restrictions_patterns.py,gateway/tests/test_agent_restrictions_gh.py— 529 passed, 1 skipped, plus 1 unrelated pre-existing failure (TestRoleSyncWithOrchestratorModels— bare-namefrom models importresolves only when orchestrator's conftest is loaded).🤖 Generated with Claude Code