FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Comparing Hunter79-stack:main...a2aproject:main · Hunter79-stack/a2a-python · GitHub

Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Hunter79-stack/a2a-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
Could not load branches
Nothing to show
{{ refName }}
...
head repository: a2aproject/a2a-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Could not load branches
Nothing to show
{{ refName }}
Checking mergeability… Don’t worry, you can still create the pull request.
  • 8 commits
  • 22 files changed
  • 9 contributors

Commits on Aug 21, 2026

  1. fix(server): let subscriber taps evict on full instead of wedging dis…

    …patch (a2aproject#1137)
    
    Fixes a2aproject#1136 (the runtime issue split out of a2aproject#1101).
    
    ### Problem
    
    One undrained sink stalls `EventQueueSource._dispatch_loop` for every
    sink. The dispatcher's asyncio.gather awaits a blocking put on each
    sink, so a single full queue (default cap 1024) whose consumer went away
    blocks the gather forever. The incoming queue then fills, producers
    wedge in enqueue_event, and the task's event flow never recovers; see
    the task dumps and repro in a2aproject#1101.
    
    ### Change: split the sink semantics
    
    The two kinds of sink want different fullness behavior.
    
    The default sink is flow control. Its consumer drives task state, so a
    full queue that back-pressures the dispatcher (and, transitively, the
    producer) is correct, and it stays exactly as it is. A slow task store
    slows the pipeline down rather than losing events.
    
    Tapped subscriber sinks are broadcast observers. Their consumers are
    remote and can be abandoned, most simply by a non-blocking message/send
    whose HTTP response has already returned. So `tap()` gains
    `evict_on_full: bool = False`. When an evict-on-full sink is full at
    delivery time, the dispatcher force-closes it (close(immediate=True),
    warning logged) and it detaches through the existing remove_sink path;
    dispatch to the remaining sinks continues, and any producer parked on
    the incoming queue recovers. The consumer of an evicted sink sees
    `QueueShutDown` on its next dequeue, same as any closed queue. There is
    no new exception type and no API surface beyond the keyword.
    
    The test is fullness at delivery time and nothing else. An abandoned
    consumer is the case that motivated this, but a consumer that is alive
    and reading and has simply fallen `max_queue_size` events behind hits
    the same predicate and is evicted the same way. At the production
    default that is 1024 events behind, and at max_queue_size=2 a consumer
    sleeping five milliseconds between reads is evicted after draining one
    event. The warning text and the `_deliver_to_sink` docstring currently
    say the consumer "is not draining it" and call it an abandoned
    subscriber, which claims something the check never tested, and a reword
    is coming that names the queue bound instead and makes no claim about
    the consumer.
    
    A sink that already closed since the dispatch snapshot was taken is
    skipped before the fullness check, so a graceful close(immediate=False)
    whose consumer is still draining is never upgraded to immediate=True and
    keeps its trailing events.
    
    The full() pre-check is race-free: the dispatcher is the only writer to
    a sink queue and consumers only read, so a queue observed non-full
    cannot become full before the put.
    
    `tap()` defaults to False, so the documented blocking contract is
    unchanged for existing callers. ActiveTask's subscriber tap opts in
    explicitly, and that is the one production site that wedges, so the fix
    reaches existing deployments without changing the default of the public
    primitive.
    
    ### Tests
    
    Five tests. test_evict_on_full_sink_is_evicted_and_dispatch_continues
    puts a full capacity-1 evict sink through dispatch and asserts it is
    closed and detached while the default sink still receives every event.
    test_producer_unblocked_after_evict_on_full is the a2aproject#1101 runtime shape
    in miniature: producers blocked on a full incoming queue recover once
    the sink is evicted. test_default_tap_keeps_backpressure pins the
    existing flow-control contract, a default tap still blocking dispatch
    when full. test_graceful_close_not_upgraded_to_immediate_by_eviction
    covers the skip above, a gracefully closing full sink keeping its
    trailing events instead of being force-closed.
    test_subscriber_taps_are_evict_on_full asserts that ActiveTask.subscribe
    taps its subscriber sinks with the keyword set.
    
    The eviction and producer-unblock tests fail without the fix. Full
    suite: 1761 passed, 90 skipped, 3 xfailed, 1 xpassed.
    
    ### Out of scope
    
    The deeper root cause is subscriber-sink lifecycle: a sink tapped for a
    consumer that has gone away should be closed by its owner at connection
    teardown, not discovered full later. That needs its own look at the
    request-handler layer. This PR keeps the dispatch layer from letting any
    such sink take the task down in the meantime.
    
    Edited 2026-08-20 to say what the eviction check actually tests. The
    description read as though only an abandoned consumer is evicted; the
    predicate is queue fullness, so a live consumer more than
    `max_queue_size` events behind is evicted too, and the warning wording
    that claims otherwise is being reworded.
    
    ---------
    
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    astrogilda and mykytanetipa authored Aug 21, 2026
    Configuration menu
    Copy the full SHA
    0c2126f View commit details
    Browse the repository at this point in the history
  2. fix: owner-scope cancel/subscribe and write terminal state on cancel (a…

    …2aproject#1159, a2aproject#1170) (a2aproject#1172)
    
    ## Summary
    
    Two related fixes to `DefaultRequestHandlerV2`, shipped together because
    the first is a prerequisite for the second to be safe.
    
    **Fixes a2aproject#1159 (owner-scoping, the security base).** `on_cancel_task` and
    `on_subscribe_to_task` resolved a live task via
    `ActiveTaskRegistry.get_or_create(task_id)` by `task_id` alone, skipping
    the owner-aware `task_store.get(task_id, context)` that `on_get_task`
    and the send path already perform. A caller who knows another tenant's
    `task_id` could subscribe to its live stream and cancel it (CWE-639).
    Both handlers now consult the owner-aware store first and fail closed
    with `TaskNotFoundError` (masking existence, matching `on_get_task`).
    
    **Fixes a2aproject#1170 (cancel writes a terminal state).** `ActiveTask.cancel`
    cancelled the producer before `AgentExecutor.cancel`, so the component
    that owns the terminal state could not write it; and a task parked
    non-terminal (e.g. `input-required`) reported cancel-success without
    transitioning. The executor cancel now runs before the producer cancel
    (producer still cancelled on the error path), and cancellation that
    leaves the task non-terminal is closed out as `CANCELED`.
    
    ## Why one PR
    
    a2aproject#1170 alone makes a2aproject#1159 worse: for a parked task, a cross-tenant cancel
    via the a2aproject#1159 bypass changes from a harmless no-op into an actual
    cross-tenant `CANCELED` write. Landing a2aproject#1159's owner check together (as
    the base commit) removes that window. Merging them together, or a2aproject#1159
    before a2aproject#1170, is safe; a2aproject#1170 must not land first.
    
    ## Tests
    
    - Owner-scope regressions: non-owner cancel and non-owner subscribe are
    rejected (`TaskNotFoundError`), including a parked task; the owner still
    succeeds.
    - Cancel-terminal regressions: mid-run cancel reaches a terminal state;
    cancel of a parked task does not silently succeed (the maintainer repros
    from a2aproject#1171, converted from xfail to passing).
    
    ## Validation
    
    - Owner-scope tests: reverting the a2aproject#1159 handler change fails all three;
    restore passes. Full request-handler module: 62 passed.
    - Cancel-terminal tests: reverting the a2aproject#1170 change fails both
    scenario_19 tests; restore passes. Full integration module: no
    regression.
    - Combined: with both applied, a cross-tenant cancel of a parked task is
    rejected rather than writing `CANCELED`.
    - `ruff` clean at the pinned version.
    
    ---------
    
    Signed-off-by: AlgoVoi <chopmob@gmail.com>
    Co-authored-by: Josh Nichols <joshua.nichols@gmail.com>
    chopmob-cloud and technicalpickles authored Aug 21, 2026
    Configuration menu
    Copy the full SHA
    ddbf853 View commit details
    Browse the repository at this point in the history

Commits on Aug 22, 2026

  1. fix(server): warn when queue_manager is ignored in DefaultRequestHand…

    …lerV2 (a2aproject#1153)
    
    ## Summary
    
    Fixes a2aproject#1135.
    
    `DefaultRequestHandlerV2.__init__` accepts a `queue_manager` argument
    (kept for signature compatibility with `DefaultRequestHandler`) but
    never stores or uses it. As @rohityan confirmed on the issue, v2
    delegates event streaming to an in-memory `ActiveTaskRegistry`, so any
    custom or distributed `QueueManager` — e.g. a Redis-backed one used for
    multi-replica stream reconnection — is silently dropped. After
    upgrading, multi-replica streaming breaks with no error or warning.
    
    ## Fix
    
    Emit a `logger.warning` at construction when a non-`None`
    `queue_manager` is passed, explaining that it is ignored in v2 and
    pointing at the documented workarounds (switch to
    `LegacyRequestHandler`, or route `/tasks/{id}:subscribe` to the replica
    holding the `ActiveTask`). This turns a silent misconfiguration into a
    visible one without changing v2's architecture. Also corrected the
    now-inaccurate inline comment on the parameter.
    
    ## Evidence
    
    - The parameter is dropped: `default_request_handler_v2.py` `__init__`
    binds `agent_executor`/`task_store`/etc. but has no `self._queue_manager
    = queue_manager` (contrast `default_request_handler.py:123`, which does
    store it and uses it at lines 209/313/503/611).
    - v2 uses the registry instead: `self._active_task_registry =
    ActiveTaskRegistry(...)`.
    
    ## Testing
    
    Added two tests to
    `tests/server/request_handlers/test_default_request_handler_v2.py`:
    - `test_init_warns_when_queue_manager_passed` — asserts a WARNING
    mentioning `queue_manager` is logged (fails on `main`, passes with the
    fix).
    - `test_init_no_warning_without_queue_manager` — asserts no such warning
    in the default case.
    
    ```
    uv run pytest tests/server/request_handlers/            # 195 passed
    ```
    `ruff check`, `ruff format --check`, and `ty check` all clean on the
    changed files.
    
    This change was developed with AI assistance; I verified the root cause
    and behavior against the code paths cited above, reviewed every line,
    and ran the tests.
    
    ---------
    
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    NishchayMahor and mykytanetipa authored Aug 22, 2026
    Configuration menu
    Copy the full SHA
    08fd223 View commit details
    Browse the repository at this point in the history

Commits on Aug 24, 2026

  1. fix: prevent first-owner write loss in in-memory stores (a2aproject#1194

    )
    
    ## Summary
    
    `_InMemoryTaskStoreImpl.save` and
    `InMemoryPushNotificationConfigStore.set_info` created owner buckets
    before acquiring their existing `RLock`. Two threads could both observe
    a missing owner, create separate buckets, and overwrite the bucket
    containing the first completed write.
    
    This moves owner-bucket creation inside the lock with `setdefault` in
    both stores. Threaded regression tests cover concurrent first writes and
    confirm that both records remain available through the public retrieval
    methods.
    
    ## Testing
    
    `uv run pytest tests/server/tasks/test_inmemory_task_store.py
    tests/server/tasks/test_inmemory_push_notifications.py -q`
    
    Result: 40 passed.
    
    Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    anxkhn and mykytanetipa authored Aug 24, 2026
    Configuration menu
    Copy the full SHA
    bcc489c View commit details
    Browse the repository at this point in the history

Commits on Aug 25, 2026

  1. fix(signing): canonicalize agent cards per RFC 8785 (a2aproject#1193)

    Fixes a2aproject#1174.
    
    The `_canonicalize_agent_card` function builds the bytes that a
    signature covers, so whatever it produces is what a verifier in another
    SDK has to reproduce exactly. Today it ends in one call:
    
    ```
    json.dumps(cleaned_dict, separators=(',', ':'), sort_keys=True)
    ```
    
    That does not produce RFC 8785 on any of the three axes the scheme
    exists to fix, and I ran each one against the code as it stands before
    changing it rather than reasoning about it. A card named Café Agent
    serializes its name as `{"name":"Caf\u00e9 Agent"}`, where JCS section
    3.2.2.2 asks for the literal UTF-8 bytes `{"name":"Café Agent"}` and
    escapes only the C0 range, the quote and the backslash. sort_keys=True
    orders keys by code point, where section 3.2.3 asks for UTF-16 code unit
    order, so a card whose extension params carry the keys U+1F600 and
    U+FF01 comes out in the opposite order from every conforming
    implementation. And repr formats numbers, where section 3.2.2.3 asks for
    the ECMAScript Number::toString algorithm, so an extension param of
    0.000001 is written 1e-06 and an integral param of 1 is written 1.0.
    Every one of those four canonical forms disagrees with an independent
    implementation of the RFC; after the change all four agree byte for
    byte.
    
    There is a fourth divergence underneath the three in the issue, and it
    is the one that would have made a fix on the first three ineffective.
    The signer parsed the canonical string back into a dict and handed the
    dict to jwt.encode, which serializes its payload with its own json.dumps
    and therefore re-applies ensure_ascii=True to bytes that had just been
    canonicalized. The canonical form would have been correct and the bytes
    actually signed would still have been escaped. The signer now hands the
    canonical bytes to the JWS layer directly. The protected header is
    byte-identical either way, which the tests assert against a jwt.encode
    reference so that this cannot regress into a silent compatibility break
    in the header a verifier reads kid and alg out of.
    
    On the question you left open, of vendoring a package versus writing the
    algorithm here: I wrote it here, and I put the package in as a test-only
    oracle instead, because that is strictly stronger than either option on
    its own. The `rfc8785` package on PyPI is Trail of Bits' implementation,
    Apache-2.0, a pure-Python py3-none-any wheel with no runtime
    dependencies of its own and requires-python >=3.8, so it installs
    cleanly across the whole 3.10 to 3.14 matrix; its most recent release is
    0.1.4 from 2024-09-27, which for a finished implementation of a frozen
    RFC is a reasonable place to sit rather than a warning sign. The reason
    not to make it a runtime dependency of `a2a-sdk[signing]` is that the
    signing extra is the code that decides whether a card is trustworthy,
    and a dependency there is one more package an attacker has to compromise
    to change what a signature means. The reason to use it anyway is that if
    it were the implementation, nothing would be checking it. As the oracle
    it checks this implementation on every accepted vector, alongside a
    corpus whose expected values two other implementations already agree on,
    so the change lands with three-way agreement instead of one-way trust.
    The expensive half is number formatting and I did not write that from
    scratch either: it is adapted from the reference implementation at
    `cyberphone/json-canonicalization`, Apache-2.0, credited in the module
    docstring, and then checked against the oracle over twenty thousand
    seeded random doubles and every mantissa-and-exponent pair near the
    notation thresholds. If you prefer that the SDK depend on rfc8785 at
    runtime, say so and I will make that swap; it is a one-line change to
    _canonicalize_agent_card and the entire test suite here stays valid,
    because the tests are written against the RFC rather than against this
    implementation.
    
    The tests are lifted rather than invented. The 57 vectors in
    `tests/utils/jcs_vectors.json` come verbatim from the language-neutral
    `a2a-jcs-v01` corpus proposed in a2aproject/a2a-tck#228, which is 47
    accept, 10 reject, and five groups covering signature exclusion, key
    ordering, string serialization, number serialization, and arrays and
    nesting. Each vector carries its expected canonical form as a UTF-8 hex
    string, so a transcription mistake cannot hide inside an editor's
    encoding, and each expected value was produced by two independent
    implementations written by neither SDK author which agree byte for byte.
    I verified the lift programmatically rather than by eye: all 57 ids
    present, no extras, and zero mismatches on disposition, input and
    expected hex against the vector files in that PR. On top of the corpus
    the suite adds the cases this ecosystem has already found
    implementations disagreeing on: unpaired surrogates in keys and in
    values, noncharacters, characters above the BMP, U+2028 and U+2029,
    negative zero, denormals, the 1e21 and 1e-7 notation boundaries,
    integers past the exact-integer range, and nesting on both sides of the
    depth bound. That is 165 tests, and the full suite is 1950 passed with
    ruff check, ruff format --check and ty check all clean.
    
    Then I tried to break it, which is where the depth bound comes from.
    Nesting is attacker-controlled: `AgentExtension.params` is a
    `google.protobuf.Struct` and nests arbitrarily, and an unbounded
    recursive serializer turns a hostile card into a stack exhaustion in
    whoever verifies it rather than a rejected signature. Serialization
    stops at 128 levels, and the `_clean_empty` helper needed the same bound
    because it runs first and would otherwise be the crash site while the
    serializer's bound sat unreachable behind it. A card past the bound now
    fails as `InvalidSignaturesError` on the verify path rather than as a
    new exception type at callers. I also checked the mutations: reversing
    the key order to code point, restoring ensure_ascii, restoring repr for
    numbers, removing the depth cap, emitting -0, and removing the surrogate
    rejection each turn the suite red, at 7, 35, 19, 4, 4 and 6 failures
    respectively. These tests fail for the reasons they claim to exist.
    
    Two things worth knowing that are not this change. The compatibility
    break is real but narrow, and I measured its edges rather than
    describing them: a pure-ASCII card with no extension params
    canonicalizes identically before and after, and so does one whose
    extension params are ASCII strings, so those signatures keep verifying.
    A card with any non-ASCII text anywhere, or any numeric extension param,
    produces different bytes and its existing signatures will not verify,
    and that is the point: those are exactly the cards that do not verify in
    other implementations today. Separately, while attacking the signing
    path I found that _clean_empty lets a card carry content the signature
    does not cover. A card whose extension params include `"policy": ""`
    canonicalizes to the same bytes as a card with no policy key at all, so
    a signature made over the second verifies the first. That behaviour
    predates this change and fixing it would be a second and larger break,
    so I have deliberately left it alone here; I have a reproducer and I am
    happy to open it as its own issue if you want it tracked.
    
    ---------
    
    Signed-off-by: Sankalp Gilda <sankalp.gilda@gmail.com>
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    astrogilda and mykytanetipa authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    f821b0b View commit details
    Browse the repository at this point in the history
  2. chore(itk): regenerate itk uv lock (a2aproject#1202)

    # Description
    
    regenerate itk uv lock to add dependency added in
    a2aproject#1193
    JakubWorek authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    593ba03 View commit details
    Browse the repository at this point in the history
  3. feat(itk): register itk-python-v10-agent as a uv workspace member and…

    … update dependency version markers (a2aproject#1203)
    
    # Description
    
    one lock file to avoid a2aproject#1202 problem in the future
    JakubWorek authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    6eee895 View commit details
    Browse the repository at this point in the history

Commits on Aug 26, 2026

  1. fix: make event queue sink removal idempotent (a2aproject#1134)

    # Description
    
    Thank you for opening a Pull Request!
    
    This pull request changed `EventQueueSource.remove_sink()` to use
    `set.discard()`, so removing an already-absent sink completes without
    raising
    `KeyError`. `EventQueueSink.close()` now delegates directly to that
    idempotent
    operation instead of suppressing the exception after telemetry has
    recorded it.
    A regression test closes the same sink twice and verifies that the
    mocked
    OpenTelemetry span records neither an exception nor an error
    description.
    
    Before submitting your PR, there are a few things you can do to make
    sure it goes smoothly:
    
    - [x] Follow the [`CONTRIBUTING`
    Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md).
    - [x] Make your Pull Request title in the
    <https://www.conventionalcommits.org/> specification.
    - Important Prefixes for
    [release-please](https://github.com/googleapis/release-please):
    - `fix:` which represents bug fixes, and correlates to a
    [SemVer](https://semver.org/) patch.
    - `feat:` represents a new feature, and correlates to a SemVer minor.
    - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking
    change (indicated by the `!`) and will result in a SemVer major.
    - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh`
    from the repository root to format)
    - [x] Appropriate docs were updated (if necessary)
    
    Fixes a2aproject#1133 🦕
    
    ---------
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    Co-authored-by: mykytanetipa <mykytanetipa@google.com>
    Copilot-Session: 42ca73c6-08fb-4954-96f4-b7e32e46613a
    3 people authored Aug 26, 2026
    Configuration menu
    Copy the full SHA
    58c72c6 View commit details
    Browse the repository at this point in the history
Loading

Back | FazBrowse Home | New Git URL