yadirect-agent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. yadirect_agent-0.1.0/.env.example +49 -0
  2. yadirect_agent-0.1.0/.github/ISSUE_TEMPLATE/bug.yml +70 -0
  3. yadirect_agent-0.1.0/.github/ISSUE_TEMPLATE/feature.yml +32 -0
  4. yadirect_agent-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +49 -0
  5. yadirect_agent-0.1.0/.github/dependabot.yml +59 -0
  6. yadirect_agent-0.1.0/.github/workflows/ci.yml +56 -0
  7. yadirect_agent-0.1.0/.github/workflows/codeql.yml +57 -0
  8. yadirect_agent-0.1.0/.github/workflows/release.yml +151 -0
  9. yadirect_agent-0.1.0/.gitignore +18 -0
  10. yadirect_agent-0.1.0/.pre-commit-config.yaml +52 -0
  11. yadirect_agent-0.1.0/CLAUDE.md +208 -0
  12. yadirect_agent-0.1.0/LICENSE +21 -0
  13. yadirect_agent-0.1.0/Makefile +60 -0
  14. yadirect_agent-0.1.0/PKG-INFO +187 -0
  15. yadirect_agent-0.1.0/README.md +131 -0
  16. yadirect_agent-0.1.0/SECURITY.md +76 -0
  17. yadirect_agent-0.1.0/agent_policy.example.yml +56 -0
  18. yadirect_agent-0.1.0/docs/ARCHITECTURE.md +151 -0
  19. yadirect_agent-0.1.0/docs/BACKLOG.md +1173 -0
  20. yadirect_agent-0.1.0/docs/CODING_RULES.md +171 -0
  21. yadirect_agent-0.1.0/docs/OPERATING.md +413 -0
  22. yadirect_agent-0.1.0/docs/PRIOR_ART.md +240 -0
  23. yadirect_agent-0.1.0/docs/REVIEW.md +160 -0
  24. yadirect_agent-0.1.0/docs/TECHNICAL_SPEC.md +1343 -0
  25. yadirect_agent-0.1.0/docs/TESTING.md +276 -0
  26. yadirect_agent-0.1.0/pyproject.toml +161 -0
  27. yadirect_agent-0.1.0/src/yadirect_agent/__init__.py +3 -0
  28. yadirect_agent-0.1.0/src/yadirect_agent/agent/__init__.py +15 -0
  29. yadirect_agent-0.1.0/src/yadirect_agent/agent/executor.py +555 -0
  30. yadirect_agent-0.1.0/src/yadirect_agent/agent/loop.py +469 -0
  31. yadirect_agent-0.1.0/src/yadirect_agent/agent/pipeline.py +767 -0
  32. yadirect_agent-0.1.0/src/yadirect_agent/agent/plans.py +189 -0
  33. yadirect_agent-0.1.0/src/yadirect_agent/agent/prompts.py +56 -0
  34. yadirect_agent-0.1.0/src/yadirect_agent/agent/rationale_store.py +122 -0
  35. yadirect_agent-0.1.0/src/yadirect_agent/agent/safety.py +1568 -0
  36. yadirect_agent-0.1.0/src/yadirect_agent/agent/tools.py +807 -0
  37. yadirect_agent-0.1.0/src/yadirect_agent/audit.py +456 -0
  38. yadirect_agent-0.1.0/src/yadirect_agent/cli/__init__.py +8 -0
  39. yadirect_agent-0.1.0/src/yadirect_agent/cli/doctor.py +150 -0
  40. yadirect_agent-0.1.0/src/yadirect_agent/cli/health.py +123 -0
  41. yadirect_agent-0.1.0/src/yadirect_agent/cli/main.py +1003 -0
  42. yadirect_agent-0.1.0/src/yadirect_agent/cli/rationale.py +102 -0
  43. yadirect_agent-0.1.0/src/yadirect_agent/clients/__init__.py +1 -0
  44. yadirect_agent-0.1.0/src/yadirect_agent/clients/base.py +247 -0
  45. yadirect_agent-0.1.0/src/yadirect_agent/clients/direct.py +248 -0
  46. yadirect_agent-0.1.0/src/yadirect_agent/clients/metrika.py +366 -0
  47. yadirect_agent-0.1.0/src/yadirect_agent/clients/wordstat.py +76 -0
  48. yadirect_agent-0.1.0/src/yadirect_agent/config.py +104 -0
  49. yadirect_agent-0.1.0/src/yadirect_agent/exceptions.py +81 -0
  50. yadirect_agent-0.1.0/src/yadirect_agent/logging.py +60 -0
  51. yadirect_agent-0.1.0/src/yadirect_agent/mcp/__init__.py +18 -0
  52. yadirect_agent-0.1.0/src/yadirect_agent/mcp/server.py +167 -0
  53. yadirect_agent-0.1.0/src/yadirect_agent/models/__init__.py +13 -0
  54. yadirect_agent-0.1.0/src/yadirect_agent/models/campaigns.py +97 -0
  55. yadirect_agent-0.1.0/src/yadirect_agent/models/health.py +114 -0
  56. yadirect_agent-0.1.0/src/yadirect_agent/models/keywords.py +108 -0
  57. yadirect_agent-0.1.0/src/yadirect_agent/models/metrika.py +198 -0
  58. yadirect_agent-0.1.0/src/yadirect_agent/models/rationale.py +209 -0
  59. yadirect_agent-0.1.0/src/yadirect_agent/py.typed +0 -0
  60. yadirect_agent-0.1.0/src/yadirect_agent/rollout.py +186 -0
  61. yadirect_agent-0.1.0/src/yadirect_agent/services/__init__.py +6 -0
  62. yadirect_agent-0.1.0/src/yadirect_agent/services/bidding.py +275 -0
  63. yadirect_agent-0.1.0/src/yadirect_agent/services/campaigns.py +452 -0
  64. yadirect_agent-0.1.0/src/yadirect_agent/services/health_check.py +241 -0
  65. yadirect_agent-0.1.0/src/yadirect_agent/services/reporting.py +275 -0
  66. yadirect_agent-0.1.0/src/yadirect_agent/services/semantics.py +80 -0
  67. yadirect_agent-0.1.0/tests/__init__.py +0 -0
  68. yadirect_agent-0.1.0/tests/evals/README.md +67 -0
  69. yadirect_agent-0.1.0/tests/evals/__init__.py +0 -0
  70. yadirect_agent-0.1.0/tests/evals/conftest.py +52 -0
  71. yadirect_agent-0.1.0/tests/evals/harness.py +256 -0
  72. yadirect_agent-0.1.0/tests/evals/test_confirm_path_bid_change.py +127 -0
  73. yadirect_agent-0.1.0/tests/evals/test_pause_low_ctr.py +136 -0
  74. yadirect_agent-0.1.0/tests/evals/test_safety_reject_budget_cap.py +129 -0
  75. yadirect_agent-0.1.0/tests/unit/__init__.py +0 -0
  76. yadirect_agent-0.1.0/tests/unit/agent/__init__.py +0 -0
  77. yadirect_agent-0.1.0/tests/unit/agent/conftest.py +88 -0
  78. yadirect_agent-0.1.0/tests/unit/agent/test_executor.py +922 -0
  79. yadirect_agent-0.1.0/tests/unit/agent/test_executor_rationale.py +354 -0
  80. yadirect_agent-0.1.0/tests/unit/agent/test_loop.py +413 -0
  81. yadirect_agent-0.1.0/tests/unit/agent/test_pipeline.py +654 -0
  82. yadirect_agent-0.1.0/tests/unit/agent/test_plans.py +278 -0
  83. yadirect_agent-0.1.0/tests/unit/agent/test_prompts.py +31 -0
  84. yadirect_agent-0.1.0/tests/unit/agent/test_rationale_store.py +196 -0
  85. yadirect_agent-0.1.0/tests/unit/agent/test_safety.py +2469 -0
  86. yadirect_agent-0.1.0/tests/unit/agent/test_tools.py +875 -0
  87. yadirect_agent-0.1.0/tests/unit/cli/__init__.py +0 -0
  88. yadirect_agent-0.1.0/tests/unit/cli/test_cli.py +1002 -0
  89. yadirect_agent-0.1.0/tests/unit/cli/test_doctor.py +291 -0
  90. yadirect_agent-0.1.0/tests/unit/cli/test_health.py +216 -0
  91. yadirect_agent-0.1.0/tests/unit/cli/test_rationale_cli.py +266 -0
  92. yadirect_agent-0.1.0/tests/unit/clients/__init__.py +0 -0
  93. yadirect_agent-0.1.0/tests/unit/clients/test_base.py +370 -0
  94. yadirect_agent-0.1.0/tests/unit/clients/test_direct.py +227 -0
  95. yadirect_agent-0.1.0/tests/unit/clients/test_metrika.py +519 -0
  96. yadirect_agent-0.1.0/tests/unit/conftest.py +60 -0
  97. yadirect_agent-0.1.0/tests/unit/mcp/__init__.py +0 -0
  98. yadirect_agent-0.1.0/tests/unit/mcp/test_server.py +182 -0
  99. yadirect_agent-0.1.0/tests/unit/models/__init__.py +0 -0
  100. yadirect_agent-0.1.0/tests/unit/models/test_campaigns.py +138 -0
  101. yadirect_agent-0.1.0/tests/unit/models/test_health.py +164 -0
  102. yadirect_agent-0.1.0/tests/unit/models/test_keywords.py +239 -0
  103. yadirect_agent-0.1.0/tests/unit/models/test_metrika.py +228 -0
  104. yadirect_agent-0.1.0/tests/unit/models/test_rationale.py +286 -0
  105. yadirect_agent-0.1.0/tests/unit/services/__init__.py +0 -0
  106. yadirect_agent-0.1.0/tests/unit/services/test_bidding.py +648 -0
  107. yadirect_agent-0.1.0/tests/unit/services/test_campaigns.py +1075 -0
  108. yadirect_agent-0.1.0/tests/unit/services/test_health_check.py +540 -0
  109. yadirect_agent-0.1.0/tests/unit/services/test_reporting.py +495 -0
  110. yadirect_agent-0.1.0/tests/unit/services/test_semantics.py +229 -0
  111. yadirect_agent-0.1.0/tests/unit/test_audit.py +635 -0
  112. yadirect_agent-0.1.0/tests/unit/test_config.py +75 -0
  113. yadirect_agent-0.1.0/tests/unit/test_rollout.py +262 -0
@@ -0,0 +1,49 @@
1
+ # =========================================================
2
+ # Yandex Direct Agent — configuration
3
+ # Copy to .env and fill in. NEVER commit .env.
4
+ # =========================================================
5
+
6
+ # --- Yandex auth (OAuth) ---
7
+ # Register an app at https://oauth.yandex.ru/ with scopes:
8
+ # direct:api, metrika:read, metrika:write
9
+ # Use scripts/get_oauth_token.py to obtain a token.
10
+ YANDEX_DIRECT_TOKEN=
11
+ YANDEX_METRIKA_TOKEN=
12
+
13
+ # Numeric counter ID for the website attached to this Direct account.
14
+ # Required for the M6 reporting service (campaign_performance,
15
+ # account_overview, conversion_by_source). Find it in the Metrika UI
16
+ # at the top of the counter's dashboard, or via list_counters.
17
+ YANDEX_METRIKA_COUNTER_ID=
18
+
19
+ # M15.5 health check — account-wide target CPA in RUB. The high-CPA
20
+ # rule uses this to flag campaigns spending above the acceptable cost
21
+ # per conversion. Leave empty to disable that rule for now.
22
+ ACCOUNT_TARGET_CPA_RUB=
23
+
24
+ # For agency accounts: login of the client sub-account to act on.
25
+ # Leave empty for direct accounts.
26
+ YANDEX_CLIENT_LOGIN=
27
+
28
+ # Safety: start in sandbox. Flip to false only after you've verified things.
29
+ YANDEX_USE_SANDBOX=true
30
+
31
+ # --- Anthropic ---
32
+ ANTHROPIC_API_KEY=
33
+ # Always use the latest Opus for the agent loop; costs a bit more but
34
+ # multi-step planning quality makes it pay off vs Sonnet for ads.
35
+ ANTHROPIC_MODEL=claude-opus-4-7
36
+
37
+ # --- Agent behaviour ---
38
+ # Which operations auto-approve vs require human confirmation.
39
+ # See agent/safety.py for the policy schema.
40
+ AGENT_POLICY_PATH=./agent_policy.yml
41
+
42
+ # Hard daily spend guard. If total budget across managed campaigns
43
+ # exceeds this (RUB), the agent refuses further changes.
44
+ AGENT_MAX_DAILY_BUDGET_RUB=10000
45
+
46
+ # --- Observability ---
47
+ LOG_LEVEL=INFO
48
+ LOG_FORMAT=json # json | console
49
+ AUDIT_LOG_PATH=./logs/audit.jsonl
@@ -0,0 +1,70 @@
1
+ name: Bug report
2
+ description: Something is broken in yadirect-agent.
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for reporting. Please fill in the fields below so we can reproduce.
9
+
10
+ - type: input
11
+ id: version
12
+ attributes:
13
+ label: Version
14
+ description: Output of `python -c "import yadirect_agent; print(yadirect_agent.__version__)"`
15
+ placeholder: "0.1.0"
16
+ validations:
17
+ required: true
18
+
19
+ - type: dropdown
20
+ id: mode
21
+ attributes:
22
+ label: Mode
23
+ options:
24
+ - CLI agent
25
+ - MCP server
26
+ - Library / programmatic
27
+ - Tests / dev tooling
28
+ validations:
29
+ required: true
30
+
31
+ - type: dropdown
32
+ id: environment
33
+ attributes:
34
+ label: Yandex environment
35
+ options:
36
+ - sandbox
37
+ - production
38
+ validations:
39
+ required: true
40
+
41
+ - type: textarea
42
+ id: what-happened
43
+ attributes:
44
+ label: What happened?
45
+ description: What did you do, what did you expect, what happened instead?
46
+ placeholder: |
47
+ Steps:
48
+ 1.
49
+ 2.
50
+ Expected:
51
+ Actual:
52
+ validations:
53
+ required: true
54
+
55
+ - type: textarea
56
+ id: logs
57
+ attributes:
58
+ label: Relevant logs
59
+ description: |
60
+ Include structured log lines (one JSON per line). **Redact any tokens
61
+ or personally identifiable data before pasting.**
62
+ render: shell
63
+
64
+ - type: checkboxes
65
+ id: safety
66
+ attributes:
67
+ label: Confirm
68
+ options:
69
+ - label: I have removed tokens, OAuth codes, and account identifiers from pasted output.
70
+ required: true
@@ -0,0 +1,32 @@
1
+ name: Feature request
2
+ description: Propose new functionality or an improvement.
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem
9
+ description: What real task or pain is this solving? Link to the milestone in `docs/TECHNICAL_SPEC.md` if one fits.
10
+ validations:
11
+ required: true
12
+
13
+ - type: textarea
14
+ id: proposal
15
+ attributes:
16
+ label: Proposal
17
+ description: Sketch the API / CLI / tool surface. Rough is fine — we'll iterate.
18
+
19
+ - type: textarea
20
+ id: alternatives
21
+ attributes:
22
+ label: Alternatives considered
23
+ description: What have you already ruled out, and why?
24
+
25
+ - type: textarea
26
+ id: safety
27
+ attributes:
28
+ label: Safety implications
29
+ description: |
30
+ Does this expand the set of mutating operations, touch bidding,
31
+ budgets, or audience settings? If yes — how do we keep it reversible
32
+ and policy-gated?
@@ -0,0 +1,49 @@
1
+ <!--
2
+ Thanks for opening a PR. Every box in "Reviewer checklist" corresponds to an
3
+ item in docs/REVIEW.md — please self-check before requesting review.
4
+ -->
5
+
6
+ ## What & why
7
+
8
+ <!-- One-paragraph summary. Link the milestone / issue. -->
9
+
10
+ Closes: #
11
+
12
+ ## How
13
+
14
+ <!--
15
+ Call out:
16
+ - new modules / responsibilities
17
+ - any changes to the safety surface (policy schema, kill-switches, audit)
18
+ - any blocking I/O introduced (should be none)
19
+ -->
20
+
21
+ ## Tests
22
+
23
+ - [ ] **TDD trail is visible**: at least one `test:` commit precedes the
24
+ matching `feat:`/`fix:` commit for every new behaviour. Exempt PR
25
+ types: `refactor`, `docs`, `chore`, `ci`, `build`, `style`. If this
26
+ PR bundles a feature into a single commit, the commit body states
27
+ "tests written first". See `docs/TESTING.md#tdd_workflow`.
28
+ - [ ] Unit tests added / updated (`pytest -q` green locally)
29
+ - [ ] `respx` fixtures cover the happy and failure paths for new HTTP calls
30
+ - [ ] Coverage for changed files ≥ 80%
31
+
32
+ ## Checklist (see `docs/REVIEW.md`)
33
+
34
+ - [ ] `make check` passes (`lint + type + test`)
35
+ - [ ] No business logic in `clients/` — only HTTP + type mapping
36
+ - [ ] No blocking calls in the async main path
37
+ - [ ] No secrets logged or committed
38
+ - [ ] Error paths use typed exceptions from `exceptions.py`
39
+ - [ ] Destructive / mutating changes go through the plan → confirm → execute flow
40
+ - [ ] Public API / flags documented in `README.md` and the relevant doc in `docs/`
41
+
42
+ ## Safety notes
43
+
44
+ <!--
45
+ If this PR adds mutating capability:
46
+ - Which kill-switches apply?
47
+ - Is the default `agent_policy.yml` sane out of the box?
48
+ - Is the operation reversible? If not, say so explicitly.
49
+ -->
@@ -0,0 +1,59 @@
1
+ # Dependabot configuration.
2
+ #
3
+ # Rationale: the agent links user money to a third-party advertising API
4
+ # through a small-but-growing dependency graph. A malicious or
5
+ # accidentally broken update — in httpx, anthropic, tenacity — would
6
+ # land on main silently if we relied on manual bumps.
7
+ #
8
+ # Strategy:
9
+ # - Weekly cadence (lower review cost than daily for a solo project).
10
+ # - Grouped minor / patch updates to reduce noise — one PR per week per
11
+ # ecosystem when there's anything to bump.
12
+ # - Labels so the PRs are filterable and the review tier is obvious.
13
+ # - Security updates are NOT grouped and run on Dependabot's own
14
+ # schedule regardless of the weekly cadence.
15
+ version: 2
16
+ updates:
17
+ - package-ecosystem: pip
18
+ directory: /
19
+ schedule:
20
+ interval: weekly
21
+ day: monday
22
+ time: "06:00"
23
+ timezone: Europe/Moscow
24
+ groups:
25
+ minor-and-patch:
26
+ applies-to: version-updates
27
+ update-types:
28
+ - minor
29
+ - patch
30
+ labels:
31
+ - dependencies
32
+ - python
33
+ commit-message:
34
+ prefix: chore
35
+ prefix-development: chore
36
+ include: scope
37
+ open-pull-requests-limit: 5
38
+
39
+ - package-ecosystem: github-actions
40
+ directory: /
41
+ schedule:
42
+ interval: weekly
43
+ day: monday
44
+ time: "06:00"
45
+ timezone: Europe/Moscow
46
+ groups:
47
+ all-actions:
48
+ applies-to: version-updates
49
+ update-types:
50
+ - minor
51
+ - patch
52
+ - major
53
+ labels:
54
+ - dependencies
55
+ - github-actions
56
+ commit-message:
57
+ prefix: ci
58
+ include: scope
59
+ open-pull-requests-limit: 3
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ check:
11
+ name: lint + type + test (py${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.11", "3.12"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v6
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ cache: pip
26
+ cache-dependency-path: pyproject.toml
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ pip install -e ".[dev]"
32
+
33
+ - name: Ruff (lint + format check)
34
+ run: |
35
+ ruff check .
36
+ ruff format --check .
37
+
38
+ - name: mypy (strict)
39
+ run: mypy src/
40
+
41
+ - name: pytest (with coverage gate)
42
+ run: |
43
+ pytest -q \
44
+ --cov=src/yadirect_agent \
45
+ --cov-report=term-missing \
46
+ --cov-report=xml \
47
+ --cov-fail-under=80
48
+
49
+ - name: Upload coverage XML
50
+ if: always()
51
+ uses: actions/upload-artifact@v7
52
+ with:
53
+ name: coverage-${{ matrix.python-version }}
54
+ path: coverage.xml
55
+ if-no-files-found: ignore
56
+ retention-days: 7
@@ -0,0 +1,57 @@
1
+ # CodeQL — GitHub-native static analysis for Python.
2
+ #
3
+ # Rationale: catches common security-relevant patterns (unsanitised
4
+ # shell, unsafe deserialisation, missing auth checks, SQL injection,
5
+ # etc.) on every push. It's a low-signal security net; most of our
6
+ # real safety lives in the policy layer, but CodeQL catches the kind
7
+ # of mistakes that slip through review.
8
+ #
9
+ # Cadence: on push to main, on PR to main, and weekly on Monday so a
10
+ # new CodeQL rule set applied by GitHub isn't missed.
11
+
12
+ name: CodeQL
13
+
14
+ on:
15
+ push:
16
+ branches: [main]
17
+ pull_request:
18
+ branches: [main]
19
+ schedule:
20
+ # Monday 07:00 Europe/Moscow = 04:00 UTC.
21
+ - cron: "0 4 * * 1"
22
+
23
+ permissions:
24
+ contents: read
25
+ security-events: write
26
+ actions: read
27
+
28
+ jobs:
29
+ analyze:
30
+ name: Analyze (${{ matrix.language }})
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 20
33
+ strategy:
34
+ fail-fast: false
35
+ matrix:
36
+ language: [python]
37
+
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v6
41
+
42
+ - name: Initialize CodeQL
43
+ uses: github/codeql-action/init@v4
44
+ with:
45
+ languages: ${{ matrix.language }}
46
+ # 'security-and-quality' is the broader pack; it catches more
47
+ # maintainability issues in addition to pure security bugs.
48
+ # If noise becomes a problem, drop to 'security-extended'.
49
+ queries: security-and-quality
50
+
51
+ - name: Autobuild
52
+ uses: github/codeql-action/autobuild@v4
53
+
54
+ - name: Perform CodeQL analysis
55
+ uses: github/codeql-action/analyze@v4
56
+ with:
57
+ category: "/language:${{ matrix.language }}"
@@ -0,0 +1,151 @@
1
+ name: Release
2
+
3
+ # Triggered when a tag matching v*.*.* is pushed (e.g. v0.1.0).
4
+ # Builds sdist + wheel and publishes to PyPI via Trusted Publishing
5
+ # (OIDC) — no PyPI token stored anywhere in this repo.
6
+ #
7
+ # One-time prerequisite: register this workflow as a Trusted
8
+ # Publisher at pypi.org → "Manage" → "Publishing":
9
+ # PyPI Project Name: yadirect-agent
10
+ # Owner: Kozharina
11
+ # Repository name: yadirect-agent
12
+ # Workflow name: release.yml
13
+ # Environment name: pypi
14
+ # Until that registration exists, the publish step will fail with
15
+ # a clear "Trusted Publisher not configured" error and the build
16
+ # artefacts remain available on the workflow run for inspection.
17
+ #
18
+ # To cut a release:
19
+ # 1. Bump version in pyproject.toml on main
20
+ # 2. git tag v<version> && git push origin v<version>
21
+ # 3. This workflow runs, builds, publishes, attaches artefacts
22
+ # to a GitHub release.
23
+
24
+ on:
25
+ push:
26
+ tags:
27
+ - "v*.*.*"
28
+
29
+ # Concurrency lock per tag. ``cancel-in-progress: false`` is
30
+ # deliberate (auditor M15.1 LOW-3): on a duplicate tag push, run
31
+ # #1 publishes to PyPI; run #2 then attempts the same upload and
32
+ # PyPI rejects it with "File already exists" because filenames
33
+ # are deterministic. Workflow run #2 ends red with a clear error,
34
+ # no double-publish, no orphaned half-state. Cancelling run #1
35
+ # mid-flight (the alternative) could leave a PyPI release with
36
+ # no matching GitHub release if cancellation lands between the
37
+ # two upload steps.
38
+ concurrency:
39
+ group: release-${{ github.ref }}
40
+ cancel-in-progress: false
41
+
42
+ permissions:
43
+ contents: read
44
+
45
+ jobs:
46
+ # Stage 1: build the package on a clean runner. Output the
47
+ # artefacts so the publish job can pick them up. The build is
48
+ # deliberately minimal — no tests here; tests run on every PR
49
+ # via ci.yml. A tag must come from main, which means tests
50
+ # already passed.
51
+ #
52
+ # Repository guard: a fork that pushes a v*.*.* tag would also
53
+ # trigger this workflow. We refuse to do anything on a fork —
54
+ # PyPI's Trusted Publisher would reject the OIDC claim anyway,
55
+ # but consuming the fork's Actions minutes / artefact storage
56
+ # / running ``python -m build`` against potentially malicious
57
+ # ``pyproject.toml`` build hooks is also pointless.
58
+ # (auditor M15.1 MEDIUM-3.)
59
+ build:
60
+ if: github.repository == 'Kozharina/yadirect-agent'
61
+ name: Build sdist and wheel
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ # Actions pinned to commit SHA (auditor M15.1 MEDIUM-1) so a
65
+ # malicious tag-move on a third-party action cannot substitute
66
+ # arbitrary code into the release pipeline. Dependabot's
67
+ # github-actions group will open PRs to advance these.
68
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
69
+
70
+ - name: Set up Python 3.11
71
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
72
+ with:
73
+ python-version: "3.11"
74
+ cache: pip
75
+ cache-dependency-path: pyproject.toml
76
+
77
+ - name: Verify tag matches pyproject version
78
+ run: |
79
+ # Tag is refs/tags/vX.Y.Z; strip the leading 'v'.
80
+ # Using $GITHUB_REF (env var, expanded by bash) rather than
81
+ # ${{ github.ref }} (template, substituted by GitHub Actions
82
+ # before the shell sees the script) keeps this safe against
83
+ # tag names containing shell metacharacters.
84
+ tag_version="${GITHUB_REF#refs/tags/v}"
85
+ if ! [[ "$tag_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
86
+ echo "::error::tag format invalid: $tag_version"
87
+ exit 1
88
+ fi
89
+ py_version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
90
+ echo "Tag version: $tag_version"
91
+ echo "pyproject.toml version: $py_version"
92
+ if [ "$tag_version" != "$py_version" ]; then
93
+ echo "::error::tag $tag_version does not match pyproject.toml version $py_version"
94
+ exit 1
95
+ fi
96
+
97
+ - name: Install build tooling
98
+ run: |
99
+ python -m pip install --upgrade pip
100
+ pip install build
101
+
102
+ - name: Build sdist and wheel
103
+ run: python -m build
104
+
105
+ - name: List built artefacts
106
+ run: ls -lah dist/
107
+
108
+ - name: Upload build artefacts
109
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
110
+ with:
111
+ name: dist
112
+ path: dist/
113
+ if-no-files-found: error
114
+ retention-days: 7
115
+
116
+ # Stage 2: publish to PyPI via Trusted Publishing. This job has
117
+ # ``id-token: write`` to mint the OIDC token, ``contents: write``
118
+ # to attach the artefacts to a GitHub release.
119
+ publish:
120
+ if: github.repository == 'Kozharina/yadirect-agent'
121
+ name: Publish to PyPI
122
+ needs: build
123
+ runs-on: ubuntu-latest
124
+ environment:
125
+ name: pypi
126
+ url: https://pypi.org/p/yadirect-agent
127
+ permissions:
128
+ id-token: write
129
+ contents: write
130
+
131
+ steps:
132
+ - name: Download build artefacts
133
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
134
+ with:
135
+ name: dist
136
+ path: dist/
137
+
138
+ # The action handles OIDC token minting and twine upload
139
+ # against the configured Trusted Publisher. No PYPI_TOKEN
140
+ # secret is read.
141
+ - name: Publish to PyPI
142
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
143
+ with:
144
+ packages-dir: dist/
145
+
146
+ - name: Create GitHub release with artefacts
147
+ uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
148
+ with:
149
+ files: dist/*
150
+ generate_release_notes: true
151
+ fail_on_unmatched_files: true
@@ -0,0 +1,18 @@
1
+ .env
2
+ .env.local
3
+ *.pyc
4
+ __pycache__/
5
+ .venv/
6
+ venv/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ dist/
14
+ build/
15
+ *.egg-info/
16
+ logs/
17
+ .vcr_cassettes/
18
+ agent_policy.local.yml
@@ -0,0 +1,52 @@
1
+ # pre-commit hooks for yadirect-agent.
2
+ #
3
+ # Install locally with: make install-hooks
4
+ # CI also runs the same ruff rules via `make lint`, so hooks are belt-and-braces.
5
+
6
+ repos:
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v5.0.0
9
+ hooks:
10
+ - id: trailing-whitespace
11
+ - id: end-of-file-fixer
12
+ - id: check-yaml
13
+ - id: check-toml
14
+ - id: check-added-large-files
15
+ args: ["--maxkb=500"]
16
+ - id: detect-private-key
17
+ - id: check-merge-conflict
18
+ - id: mixed-line-ending
19
+ args: ["--fix=lf"]
20
+
21
+ - repo: https://github.com/astral-sh/ruff-pre-commit
22
+ # Pinned exact to match the ruff pin in pyproject.toml dev extras.
23
+ # Bumping ruff is a 3-file operation: here, pyproject.toml dev deps,
24
+ # and (if the new ruff ships new default lints) the code that trips
25
+ # them. See docs/TESTING.md on the version-skew lesson.
26
+ rev: v0.15.11
27
+ hooks:
28
+ - id: ruff
29
+ args: ["--fix", "--exit-non-zero-on-fix"]
30
+ - id: ruff-format
31
+
32
+ - repo: https://github.com/pre-commit/mirrors-mypy
33
+ rev: v1.13.0
34
+ hooks:
35
+ - id: mypy
36
+ files: ^src/
37
+ # mypy runs in its own isolated venv here — every runtime dep whose
38
+ # types we rely on must be listed, otherwise we see false
39
+ # "module not found" errors that the in-venv mypy does not.
40
+ additional_dependencies:
41
+ - pydantic>=2.8
42
+ - pydantic-settings>=2.4
43
+ - httpx>=0.27
44
+ - structlog>=24.4
45
+ - tenacity>=9.0
46
+ - anthropic>=0.39
47
+ - typer>=0.12
48
+ - pyyaml>=6.0
49
+ - mcp>=1.0
50
+ - types-python-slugify
51
+ - types-pyyaml
52
+ args: ["--config-file=pyproject.toml"]