Commit f4e7e86
authored
feat(tracker): FGAC authz on agent_task_tracker routes (AGX1-307) (#265)
## Summary
Adds FGAC authorization to all three `agent_task_tracker` routes by
delegating to the parent task. Trackers have no SpiceDB type of their
own so no schema changes, `register_resource` calls, or dual-write are
needed.
- **GET /tracker/{tracker_id}** checks `task.read` (view) on the parent
task, resolved via `tracker.task_id`. A denied or missing tracker
collapses to 404 rather than 403, so callers cannot probe cross-tenant
existence by comparing status codes. Side effect: a missing tracker now
returns 404 (previously 400) because the parent-task fetch runs before
the handler body.
- **GET /tracker** uses `DAuthorizedResourceIds(task)` to get the
caller's viewable task set and drops trackers under unauthorized tasks
silently. An explicit `?task_id=` the caller cannot view returns an
empty list, never a 404. `agent_id` remains a plain filter.
- **PUT /tracker/{tracker_id}** now checks `task.execute` on the parent
task (matching the message and checkpoint write routes), with the same
404 collapse on denial. This closes a cross-tenant write gap where the
read routes were locked down but the mutating route was left unenforced.
Also cleans up copy-paste artifacts from the original file (variable
naming, log strings, stale docstring args) and removes an unreachable
`task_id` scalar path in `use_case.list` whose only caller already
passes `task_ids`.
## Test plan
- Existing integration tests continue to pass (20 integration, 1 unit)
- New integration tests in `test_agent_task_tracker_authz_api.py`:
- List with authz disabled returns all trackers (bypass path)
- List with zero authorized tasks and no filter returns empty list
- PUT authorized returns 200 and records exactly one `task.execute`
check on the parent task
- PUT unauthorized returns 404
## Rollout note
PUT enforcement is behind the same flag as the read routes. Before
enabling tracker FGAC, confirm the agent runtime that commits
cursor/status updates either runs with an authz bypass (agent API key)
or holds `task.execute` on the parent task.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds fine-grained access control (FGAC) to the three
`agent_task_tracker` routes by delegating authorization to the parent
task. No SpiceDB schema changes are needed since trackers inherit task
permissions.
- **GET /tracker/{tracker_id}** and **PUT /tracker/{tracker_id}** use
`DAuthorizedId` with `TaskChildResourceType.agent_task_tracker` to
resolve `tracker → task_id` before the handler runs, checking
`task.read` and `task.execute` respectively; a missing or unauthorized
tracker collapses to 404.
- **GET /tracker** uses `DAuthorizedResourceIds(task)` to scope the
result set to the caller's authorized tasks, with explicit `?task_id=`
filters silently clamped to the authorized set. The use case signature
changes from a scalar `task_id` to a list `task_ids` to support the `IN
(...)` SQL pattern (with `[]` producing zero rows via SQLAlchemy's `IN
()` handling).
- Side-effect documented in the PR: a missing tracker on GET/PUT now
returns 404 (previously 400) because the parent-task resolution runs
unconditionally in the dependency, even on the bypass path.
<details><summary><h3>Confidence Score: 4/5</h3></summary>
The change is safe to merge. Authorization logic is correct for all
three routes and consistent with the existing state/message patterns.
The documented behavioral change (missing tracker → 404 instead of 400)
is intentional and the existing test suite has been updated to reflect
it.
The authz logic across GET, PUT, and list routes is well-tested and
correct. The only observations are a linear list-membership scan on
authorized_task_ids in the filter route and a redundant DB fetch of the
tracker in the GET handler — both minor and non-blocking.
agentex/src/api/routes/agent_task_tracker.py — the list-route
authorized_task_ids membership check and the GET handler's double
tracker fetch are both worth a second look before scaling.
</details>
<h3>Important Files Changed</h3>
| Filename | Overview |
|----------|----------|
| agentex/src/api/routes/agent_task_tracker.py | Adds DAuthorizedId
(read/execute) to GET/PUT handlers and DAuthorizedResourceIds to the
list handler; effective_task_ids logic is correct but uses a linear list
membership check for the explicit ?task_id= case. |
| agentex/src/utils/authorization_shortcuts.py | Adds
DAgentTaskTrackerRepository to _get_parent_task_id registry and threads
it through DAuthorizedId/DAuthorizedQuery; pattern is consistent with
existing state/message entries. |
| agentex/src/domain/use_cases/agent_task_tracker_use_case.py |
Signature change from scalar task_id to list task_ids; empty-list case
correctly produces a falsy IN () filter via SQLAlchemy; None correctly
bypasses the filter. |
|
agentex/tests/integration/api/agent_task_tracker/test_agent_task_tracker_authz_api.py
| New authz integration tests covering bypass, zero-authorized-tasks,
authorized/unauthorized GET and PUT, and explicit task_id filter cases;
mock call inspection relies on positional-arg indexing. |
|
agentex/tests/integration/api/agent_task_tracker/test_agent_task_tracker_api.py
| Updates non-existent-tracker test from 400 to 404 to reflect the
documented behaviour change caused by DAuthorizedId resolving before the
handler body. |
| agentex/tests/unit/api/test_agent_api_keys_authz.py | Adds
tracker_repository MagicMock to existing DAuthorizedId unit test
call-sites to match the updated dependency signature. |
| agentex/src/api/schemas/authorization_types.py | Adds
agent_task_tracker to TaskChildResourceType enum; minimal and correct
change. |
</details>
<details><summary><h3>Sequence Diagram</h3></summary>
```mermaid
sequenceDiagram
participant C as Caller
participant R as Router
participant DI as DAuthorizedId / DAuthorizedResourceIds
participant TR as TrackerRepository
participant AS as AuthorizationService
participant UC as TrackerUseCase
Note over R,DI: GET /tracker/{tracker_id} & PUT /tracker/{tracker_id}
C->>R: "GET/PUT /tracker/{tracker_id}"
R->>DI: resolve DAuthorizedId(agent_task_tracker, read/execute)
DI->>TR: "get(id=tracker_id) → task_id"
alt tracker missing
TR-->>DI: ItemDoesNotExist
DI-->>C: 404
end
DI->>AS: check(task.read / task.execute, task_id)
alt unauthorized
AS-->>DI: AuthorizationError → ItemDoesNotExist
DI-->>C: 404
end
DI-->>R: tracker_id (authorized)
R->>UC: get/update tracker
UC->>TR: get/update(tracker_id)
TR-->>UC: AgentTaskTrackerEntity
UC-->>R: entity
R-->>C: 200
Note over R,DI: GET /tracker (list)
C->>R: "GET /tracker[?task_id=X]"
R->>DI: resolve DAuthorizedResourceIds(task, read)
DI->>AS: list_resources(task, read)
AS-->>DI: authorized_task_ids (list) or None (bypass)
DI-->>R: authorized_task_ids
R->>R: compute effective_task_ids
R->>UC: "list(task_ids=effective_task_ids)"
UC->>TR: "list(filters={task_id: IN(effective_task_ids)})"
TR-->>UC: filtered trackers
UC-->>R: entities
R-->>C: 200 list
```
</details>
<a
href="https://app.greptile.com/api/ide/cursor?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A73-76%0AWhen%20authz%20is%20enabled%20and%20an%20explicit%20%60%3Ftask_id%3D%60%20is%20provided%2C%20%60task_id%20in%20authorized_task_ids%60%20performs%20a%20linear%20O%28n%29%20scan%20over%20a%20%60list%5Bstr%5D%60.%20If%20a%20caller%20is%20authorized%20for%20thousands%20of%20tasks%2C%20this%20membership%20test%20dominates%20on%20every%20filtered%20list%20request.%20Converting%20to%20a%20%60set%60%20at%20the%20point%20of%20use%20eliminates%20the%20overhead.%0A%0A%60%60%60suggestion%0A%20%20%20%20elif%20task_id%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%23%20Explicit%20task_id%20is%20only%20honored%20if%20the%20caller%20is%20authorized%20for%20it%3B%0A%20%20%20%20%20%20%20%20%23%20otherwise%20the%20result%20set%20is%20empty%20%28IN%20%28%29%29.%0A%20%20%20%20%20%20%20%20authorized_task_id_set%20%3D%20set%28authorized_task_ids%29%0A%20%20%20%20%20%20%20%20effective_task_ids%20%3D%20%5Btask_id%5D%20if%20task_id%20in%20authorized_task_id_set%20else%20%5B%5D%0A%60%60%60%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A41-50%0A**Double%20DB%20fetch%20of%20tracker%20on%20GET**%0A%0A%60DAuthorizedId%60%20already%20calls%20%60tracker_repository.get%28id%3Dtracker_id%29%60%20inside%20%60_get_parent_task_id%60%20to%20resolve%20%60task_id%60.%20The%20handler%20then%20issues%20a%20second%20identical%20%60get%60%20via%20%60get_agent_task_tracker%60.%20For%20the%20GET%20path%20this%20means%20two%20sequential%20round-trips%20to%20retrieve%20the%20same%20row.%20The%20existing%20%60state%60%20and%20%60message%60%20routes%20share%20this%20pattern%2C%20so%20this%20is%20consistent%20with%20the%20codebase%2C%20but%20it%20may%20be%20worth%20caching%20the%20resolved%20entity%20in%20the%20dependency%20for%20tracker%20routes%20given%20how%20frequently%20the%20cursor-commit%20path%20is%20called.%0A%0A&pr=265&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursorDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3"><img
alt="Fix All in Cursor"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3"
height="20"></picture></a> <a
href="https://app.greptile.com/ide/claude-code?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A73-76%0AWhen%20authz%20is%20enabled%20and%20an%20explicit%20%60%3Ftask_id%3D%60%20is%20provided%2C%20%60task_id%20in%20authorized_task_ids%60%20performs%20a%20linear%20O%28n%29%20scan%20over%20a%20%60list%5Bstr%5D%60.%20If%20a%20caller%20is%20authorized%20for%20thousands%20of%20tasks%2C%20this%20membership%20test%20dominates%20on%20every%20filtered%20list%20request.%20Converting%20to%20a%20%60set%60%20at%20the%20point%20of%20use%20eliminates%20the%20overhead.%0A%0A%60%60%60suggestion%0A%20%20%20%20elif%20task_id%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%23%20Explicit%20task_id%20is%20only%20honored%20if%20the%20caller%20is%20authorized%20for%20it%3B%0A%20%20%20%20%20%20%20%20%23%20otherwise%20the%20result%20set%20is%20empty%20%28IN%20%28%29%29.%0A%20%20%20%20%20%20%20%20authorized_task_id_set%20%3D%20set%28authorized_task_ids%29%0A%20%20%20%20%20%20%20%20effective_task_ids%20%3D%20%5Btask_id%5D%20if%20task_id%20in%20authorized_task_id_set%20else%20%5B%5D%0A%60%60%60%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A41-50%0A**Double%20DB%20fetch%20of%20tracker%20on%20GET**%0A%0A%60DAuthorizedId%60%20already%20calls%20%60tracker_repository.get%28id%3Dtracker_id%29%60%20inside%20%60_get_parent_task_id%60%20to%20resolve%20%60task_id%60.%20The%20handler%20then%20issues%20a%20second%20identical%20%60get%60%20via%20%60get_agent_task_tracker%60.%20For%20the%20GET%20path%20this%20means%20two%20sequential%20round-trips%20to%20retrieve%20the%20same%20row.%20The%20existing%20%60state%60%20and%20%60message%60%20routes%20share%20this%20pattern%2C%20so%20this%20is%20consistent%20with%20the%20codebase%2C%20but%20it%20may%20be%20worth%20caching%20the%20resolved%20entity%20in%20the%20dependency%20for%20tracker%20routes%20given%20how%20frequently%20the%20cursor-commit%20path%20is%20called.%0A%0A&repo=scaleapi%2Fscale-agentex&pr=265&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaudeDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3"><img
alt="Fix All in Claude Code"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3"
height="20"></picture></a> <a
href="https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22asher.fink%2Fagx1-307-tracker-route-fgac%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22asher.fink%2Fagx1-307-tracker-route-fgac%22.%0A%0AFix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A73-76%0AWhen%20authz%20is%20enabled%20and%20an%20explicit%20%60%3Ftask_id%3D%60%20is%20provided%2C%20%60task_id%20in%20authorized_task_ids%60%20performs%20a%20linear%20O%28n%29%20scan%20over%20a%20%60list%5Bstr%5D%60.%20If%20a%20caller%20is%20authorized%20for%20thousands%20of%20tasks%2C%20this%20membership%20test%20dominates%20on%20every%20filtered%20list%20request.%20Converting%20to%20a%20%60set%60%20at%20the%20point%20of%20use%20eliminates%20the%20overhead.%0A%0A%60%60%60suggestion%0A%20%20%20%20elif%20task_id%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%23%20Explicit%20task_id%20is%20only%20honored%20if%20the%20caller%20is%20authorized%20for%20it%3B%0A%20%20%20%20%20%20%20%20%23%20otherwise%20the%20result%20set%20is%20empty%20%28IN%20%28%29%29.%0A%20%20%20%20%20%20%20%20authorized_task_id_set%20%3D%20set%28authorized_task_ids%29%0A%20%20%20%20%20%20%20%20effective_task_ids%20%3D%20%5Btask_id%5D%20if%20task_id%20in%20authorized_task_id_set%20else%20%5B%5D%0A%60%60%60%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fapi%2Froutes%2Fagent_task_tracker.py%3A41-50%0A**Double%20DB%20fetch%20of%20tracker%20on%20GET**%0A%0A%60DAuthorizedId%60%20already%20calls%20%60tracker_repository.get%28id%3Dtracker_id%29%60%20inside%20%60_get_parent_task_id%60%20to%20resolve%20%60task_id%60.%20The%20handler%20then%20issues%20a%20second%20identical%20%60get%60%20via%20%60get_agent_task_tracker%60.%20For%20the%20GET%20path%20this%20means%20two%20sequential%20round-trips%20to%20retrieve%20the%20same%20row.%20The%20existing%20%60state%60%20and%20%60message%60%20routes%20share%20this%20pattern%2C%20so%20this%20is%20consistent%20with%20the%20codebase%2C%20but%20it%20may%20be%20worth%20caching%20the%20resolved%20entity%20in%20the%20dependency%20for%20tracker%20routes%20given%20how%20frequently%20the%20cursor-commit%20path%20is%20called.%0A%0A"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3"><img
alt="Fix All in Codex"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3"
height="20"></picture></a>
<details><summary>Prompt To Fix All With AI</summary>
`````markdown
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
agentex/src/api/routes/agent_task_tracker.py:73-76
When authz is enabled and an explicit `?task_id=` is provided, `task_id in authorized_task_ids` performs a linear O(n) scan over a `list[str]`. If a caller is authorized for thousands of tasks, this membership test dominates on every filtered list request. Converting to a `set` at the point of use eliminates the overhead.
```suggestion
elif task_id is not None:
# Explicit task_id is only honored if the caller is authorized for it;
# otherwise the result set is empty (IN ()).
authorized_task_id_set = set(authorized_task_ids)
effective_task_ids = [task_id] if task_id in authorized_task_id_set else
[]
```
### Issue 2 of 2
agentex/src/api/routes/agent_task_tracker.py:41-50
**Double DB fetch of tracker on GET**
`DAuthorizedId` already calls `tracker_repository.get(id=tracker_id)` inside `_get_parent_task_id` to resolve `task_id`. The handler then issues a second identical `get` via `get_agent_task_tracker`. For the GET path this means two sequential round-trips to retrieve the same row. The existing `state` and `message` routes share this pattern, so this is consistent with the codebase, but it may be worth caching the resolved entity in the dependency for tracker routes given how frequently the cursor-commit path is called.
`````
</details>
<sub>Reviews (1): Last reviewed commit: ["feat(tracker): FGAC authz on
agent\_task\_..."](5ffc036)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=35398752)</sub>
<!-- /greptile_comment -->1 parent 1d53e80 commit f4e7e86
7 files changed
Lines changed: 484 additions & 48 deletions
File tree
- agentex
- src
- api
- routes
- schemas
- domain/use_cases
- utils
- tests
- integration/api/agent_task_tracker
- unit/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
7 | 12 | | |
8 | 13 | | |
9 | 14 | | |
10 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
11 | 20 | | |
12 | 21 | | |
13 | 22 | | |
| |||
22 | 31 | | |
23 | 32 | | |
24 | 33 | | |
25 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
26 | 39 | | |
27 | 40 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | 41 | | |
32 | | - | |
| 42 | + | |
33 | 43 | | |
34 | 44 | | |
35 | | - | |
| 45 | + | |
36 | 46 | | |
37 | 47 | | |
38 | 48 | | |
39 | | - | |
| 49 | + | |
40 | 50 | | |
41 | 51 | | |
42 | 52 | | |
| |||
48 | 58 | | |
49 | 59 | | |
50 | 60 | | |
| 61 | + | |
51 | 62 | | |
52 | 63 | | |
53 | 64 | | |
54 | 65 | | |
55 | 66 | | |
56 | 67 | | |
57 | 68 | | |
58 | | - | |
59 | | - | |
60 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
61 | 80 | | |
62 | 81 | | |
63 | | - | |
| 82 | + | |
64 | 83 | | |
65 | 84 | | |
66 | 85 | | |
| |||
79 | 98 | | |
80 | 99 | | |
81 | 100 | | |
82 | | - | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
83 | 106 | | |
84 | 107 | | |
85 | 108 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | 109 | | |
90 | | - | |
| 110 | + | |
91 | 111 | | |
92 | 112 | | |
93 | 113 | | |
94 | 114 | | |
95 | 115 | | |
96 | | - | |
| 116 | + | |
97 | 117 | | |
98 | 118 | | |
99 | 119 | | |
100 | | - | |
| 120 | + | |
101 | 121 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| |||
Lines changed: 4 additions & 24 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
35 | | - | |
36 | | - | |
| 34 | + | |
37 | 35 | | |
38 | 36 | | |
39 | 37 | | |
40 | | - | |
41 | | - | |
| 38 | + | |
| 39 | + | |
42 | 40 | | |
43 | 41 | | |
44 | 42 | | |
| |||
55 | 53 | | |
56 | 54 | | |
57 | 55 | | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | 56 | | |
76 | | - | |
77 | 57 | | |
78 | 58 | | |
79 | 59 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
22 | 25 | | |
23 | 26 | | |
24 | 27 | | |
| 28 | + | |
25 | 29 | | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| 34 | + | |
30 | 35 | | |
31 | 36 | | |
32 | 37 | | |
| |||
46 | 51 | | |
47 | 52 | | |
48 | 53 | | |
| 54 | + | |
49 | 55 | | |
50 | 56 | | |
51 | 57 | | |
| |||
57 | 63 | | |
58 | 64 | | |
59 | 65 | | |
| 66 | + | |
60 | 67 | | |
61 | 68 | | |
62 | 69 | | |
| |||
92 | 99 | | |
93 | 100 | | |
94 | 101 | | |
| 102 | + | |
95 | 103 | | |
96 | 104 | | |
97 | 105 | | |
| |||
103 | 111 | | |
104 | 112 | | |
105 | 113 | | |
| 114 | + | |
106 | 115 | | |
107 | 116 | | |
108 | 117 | | |
| |||
Lines changed: 10 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
315 | 315 | | |
316 | 316 | | |
317 | 317 | | |
318 | | - | |
319 | | - | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
320 | 326 | | |
321 | 327 | | |
322 | 328 | | |
323 | | - | |
324 | | - | |
| 329 | + | |
| 330 | + | |
325 | 331 | | |
326 | 332 | | |
327 | 333 | | |
| |||
0 commit comments