Repository navigation
feat(readers): read returned tool factories with constructor ownership checks - #964
Conversation
|
Review (2026-10-05): holding this PR, not merging. It is stacked on #963; see the review there.
|
229e0cd to
2977342
Compare
1db868c to
e8f997a
Compare
Resolve conflicts with #985 (tools bound as objects) and the CHANGELOG. - SDK reader: record per-member construction sites (#964) for object bindings too, through one note_sites helper, and keep object names in the published tool_sites. - ADK reader: record a returned list's factory call sites for object bindings as well as function tools. - Both readers: an unproved constructor identity now marks every binding of the agent, object bindings included, so an object row is never established under a constructor gap. - CHANGELOG: keep both sides' entries.
…or checks (#964) The constructor-ownership proof predates #985 and refused every framework object a tool list can hold as an opaque use of the framework namespace, which withheld the whole agent. Each shape is now accepted by its role, resolved by import identity, and leaves an owner obligation the list reader discharges: - ADK built-ins (google_search, load_memory, built_in_code_execution) as a member of tools=, or of a list that is followed to its uses - ADK McpToolset with its connection-parameter class (data arguments only); the toolset's import path is not its identity - hosted tools, MCP servers (also as the target of an async with) and AgentTool as the documented call with data arguments; SDK function_tool(f) - an agent held by AgentTool(agent=...) or exposed by agent.as_tool(...), across modules and through a helper returning it from its parameter - eager annotations of the agent class or a tool object on an undecorated function, when no module of the census reads annotation dictionaries - an inert dataclass of environment defaults and its instance (a new instance_data owner), os.environ[...] read into a local or a tool object's configuration, and a method call whose result is dropped on an Agent instance - a name an installed package provides is not shadowed by a sibling package of the module's own (src/app/agents is app.agents, not agents) Exports of a handle are read through their importers' uses only for a plain from-import and only for agent, instance, tool-object and wrapped-function owners. Adversarial controls for each route are in test_object_tool_ownership.
…sed import (#964) _unused_reflection_import discharged a builtins/inspect import when only the importing module's own Name loads were checked. A module that does 'from builtins import eval as execute' and is imported by a sibling that calls execute(...) was therefore read as free of executable machinery, and a returned-tool renamed through it was silent. The discharge now also requires that no other module of the read scope imports the module (from m import name, import m [as x], from m import *, or through a re-export chain), using the caller census's own retaining set; a census cut short does not discharge. Covers the __all__ + star export and three bridge-module re-export routes.
…l, and withhold only absence (#964) A remote McpToolset is never statically enumerable, so its graph is always partial, and the binding-comparison gate switched the whole capability comparison off: an ADK endpoint moved from a read-only host to an admin one (#538) was 'broadened' on main and silent here. Two causes. - The ownership proof refused 'os.environ[...]' inside connection-parameter data. A read that is data of an exact ADK or mcp connection class is now accepted, following enclosing calls only through such classes up to an accepted toolset's connection_params, including the nested StdioConnectionParams(server_params=StdioServerParameters(env={...})) form. Every other placement of the read stays an unread handle. - The gate. A toolset whose own construction was read and whose leaves cannot be listed now carries its own issue identity (source framework_toolset_inventory); it, and the graph_status:partial it alone causes, are not binding-graph incompleteness. And when a graph really is incomplete the gate withholds only what depends on absence (removed and narrowed rows and members, the controls of an absent tool, resolved findings) and still reports added, changed, broadened and unknown-direction rows. The binding diff stays withheld, and the requested-comparison gap still stands, found through the diff notes. Consumers checked: tool_surface compute, the binding diff in sanitization, release_decision's incomplete-comparison gap, the capability_change projection and the verify capability review notes.
… 'unresolved' (#964) The constructor-gap pass copied every already-matched sub-agent name into unresolved_sub_agents, which made the binding graph say the scan 'could not match' that agent to a definition. The names were read; the record's 'unread' already makes their edge incomplete, and the constructor issue is its own partial. Stop the copy, and bring the three stale list-expression assertions to the contract the PR chose: readable candidate edges survive a constructor gap, incomplete, with the constructor issue named (#964)
…ow stop at (#964) Two imported-tool review cases that store sys itself into the module table are still withheld as constructor candidates, but the import-search check reads the store first. Their expected reason is now that check's text; the namespace-read case keeps the reflective-access reason.
…filesystem (#964) _unprovided_root checked the absence of operator / operator.py with lstat, which finds Operator.py on a case-insensitive filesystem (macOS) and not on a case-sensitive one (Linux): the same tree was a proof of absence on one and not the other, and three tests failed only on hosted Linux. Absence now reads the exact directory listing the resolver already holds: any entry that case-folds to the provider or provider.py blocks, on every platform. The same fix is applied to the other probes whose answer depended on the host: a missing import-root component beside a differently cased entry, the above-scope namespace directory, and the pyvenv.cfg marker that decides whether a directory is read at all (and its currency check).
…ls (#964) 120 parametrizations across 15 test functions in test_builder_bindings.py expect a clean read of a namespace write the ownership proof does not yet attribute to an unrelated attribute, another module or a fresh/foreign dictionary. The reader refuses them, fail-closed, which is an over-refusal and never a missed change. The precision is not implemented in this PR; it is tracked in #1005, and exactly the failing parametrizations are strict xfails so closing the gap is noticed. No refusing parametrization is marked.
Over half of reader time in a constructor-ownership test was the import-search check (_no_import_patch -> _import_search_limit -> _import_search_issue), recomputed for every construction: about 90 calls a case, mostly for the same runner list or the same module. The answer is now remembered within one resolver read, by runner list (path and parsed tree) for _import_search_limit and by (parsed tree, sys/site carrier set) for the per-module refusal. Nothing is stored once any read bound has been reached, for a refusal that is itself a bound, or when a stop was raised, so a limit is met again rather than remembered. Behavior is unchanged: a plugin that recomputed every call with empty caches and compared it with the remembered answer found no difference across 113,669 calls in the reader test files.
Replace the projected shard and gate budgets with what the first rehearsals of the sharded layout measured on 2026-10-09: Advisory Release Rehearsal run 37871044408 took 290-542 s per shard job (slowest 9.0 min) and 42 s for the gate; Release Rehearsal run 37869869033 reached the qualification download in about 50 s and stopped there, because this repository sets no qualification variables. The 35- and 10-minute timeouts are unchanged; re-derive them from the first rehearsal after #964 lands.
…ring subset (#964) The Agents SDK and Google ADK readers share one constructor-ownership engine; a test's framework argument selects the reader. Crossing it with a route matrix (patch x layout x change x factory, ...) ran the same engine over the same routes twice, in the per-PR run. Per test function in the 20 engine test files, the ADK matrix stays whole and the Agents SDK keeps a covering subset: every value of every parameter is met at least once on the SDK reader, so each route and each reader-specific branch is still exercised there. The other SDK cases (1,291 of 22,803) are marked slow at collection (tests/framework_matrix.py, applied from conftest before -m), which the per-PR run excludes and the nightly run executes with -m slow. Nothing is deleted, and the marks are added at collection so the parameter ids other tests key on do not change. Registers the slow marker in pyproject.
#964) tests/shard_seconds.json is re-measured from one CI-mirror run (-n auto, not perf and not slow, 21,522 cases, 424 files): the PR's new files had no weight and were guessed at by item count, which put git-fixture files in a shard as if they were pure and overran the 15-minute cap. At the measured weights the suite packs into 5 equal shards of about 3,573 weighted seconds each, or 6 of about 2,978. test_shard_partition.py now fails when a file the CI suite collects has no measured weight, naming the re-measure command; the two files that run outside the sharded suite are an explicit allow-list. The unsharded collection is taken once per process instead of twice.
The 'does another module import this one' question asked by the unused reflection-import discharge built a fresh caller census (directory inventory and source reads) for each module that asked. One census per resolver answers all of them (#964)
… verification (#1006) * ci: split the suite into six shards, add a slow marker and a nightly run for it The slowest of CI's five shards took about 11 of its 15 minutes on main (Test steps of 311, 405, 623, 508 and 538 seconds, about 40 minutes in all), and #964, which adds roughly 45% more test time, had a shard cancelled at the cap. The partition is computed from the collection and measured seconds, so a sixth shard needs no other change to conftest.py; the matrix, the shard count and the coverage job's expected fragment count move together and a test holds them equal. Simulated from tests/shard_seconds.json, each of six shards carries 1,846 weighted seconds against 2,215 for each of five. Register a `slow` marker. Pull-request shards select `-m "not perf and not slow"`; a new Slow Tests workflow runs `-m slow` nightly and on dispatch with the suite job's checkout and locked install, treating pytest's exit status 5 (nothing collected, today's state) as success and every other non-zero status as failure. Release verification keeps the full selection. The coverage job now starts after a failed shard and fails by name. A skipped required check counts as passing, and the ruleset requires suite (1) to suite (3) only, so a failure in shard 4, 5 or 6 left the job skipped and the pull request mergeable. It also requires exactly one fragment per shard. * ci: shard release verification's suite instead of raising its single-runner timeout Both release verifications ran the whole suite on one runner inside 60 minutes, a figure derived on 2026-09-14 from a ~25 minute suite. CI's five shards now take about 40 minutes of test time on main (1.5x headroom, not 2.4x) and #964 adds roughly 45%, about 58 minutes. A timeout there fails verification after the tag exists, and a budget of 2.5x that would be one job of about two and a half hours. Run the suite the way CI does instead: a six-shard `suite` job in each of release-verify.yml and release-advisory-verify.yml, partitioned by the same conftest.py from the same tests/shard_seconds.json, selecting `-m "not perf"` so the `slow` tests CI leaves out still run here. The `tests` job stays the gate and keeps its outputs (source_sha, qualified_wheel_sha256, qualification_sha256) and every integrity property: it needs `suite`, starts after a failed shard (`!cancelled()`) so its first step can fail it by name instead of being skipped, requires exactly one evidence record per shard each naming the commit it gates, and enforces the 85% coverage floor over the combined fragments. The artifact job is unchanged and still needs `tests`. Budgets are projected from CI's timings: 35 minutes per shard (about 14 projected, 2.5x) and 10 for the gate (about 3 projected). The runbook records both, and the derivation; replace them with a finished rehearsal's figures. Tests follow the move (the suite step is found in `jobs.suite`), pin the shard count in all three matrices and both fragment counts, hold the two release files' `suite` jobs identical, and run the gate's shell against built evidence (failed, skipped and cancelled results, a missing or surplus shard, a wrong commit, and real coverage fragments that clear the floor only together). * docs(release): record the first sharded rehearsal's timings Replace the projected shard and gate budgets with what the first rehearsals of the sharded layout measured on 2026-10-09: Advisory Release Rehearsal run 37871044408 took 290-542 s per shard job (slowest 9.0 min) and 42 s for the gate; Release Rehearsal run 37869869033 reached the qualification download in about 50 s and stopped there, because this repository sets no qualification variables. The 35- and 10-minute timeouts are unchanged; re-derive them from the first rehearsal after #964 lands.
Bring in #1006 (six shards, the slow marker and sharded release verification). The slow marker description now covers both the framework-matrix cases this branch marks and the nightly/release runs.
…ery host (#964) test_operator_receiver_proof_reads_import_roots_above_the_caller_scope derived its expectation from the host filesystem, so it contradicted test_builder_calls' case providers once absence was decided from the listing with case folded: it passed on macOS and failed on Linux CI (suite (6), case_root and case_provider). Both now expect the same host-independent refusal. Checked under a simulated case-sensitive filesystem, which reproduces CI's four failures with the old expectation.
Bounded local tool factories retain source members and construction locations in the OpenAI Agents SDK and Google ADK readers. Unproved constructor ownership remains a coverage gap; it never establishes deployed wiring or permits an inferred removal.
Review status: on hold; not ready to merge. This update publishes the reviewed source fixes. It does not carry forward the previous head's full-suite, development-corpus, coverage, or hosted timing qualification.
The follow-up addresses the review as follows:
from . import agent. SDK Runner consumers and other opaque carriers remain coverage limits. Restoring the remaining canonical forms and requalifying the development corpus are still required before merge.suite (4)andsuite (5).v1.2.0output from current source coverage limits.Validation of the latest ordinary source overlay:
Remaining before merge:
Parent #963 is merged. This remains the second bounded increment of #874; returned closures (#965), foreign target-agent surfaces and typed runtime retention remain separate work. Application code is never imported or executed. No declarations, waivers, suppressions or frozen qualification rules are changed.