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.
- yadirect_agent-0.1.0/.env.example +49 -0
- yadirect_agent-0.1.0/.github/ISSUE_TEMPLATE/bug.yml +70 -0
- yadirect_agent-0.1.0/.github/ISSUE_TEMPLATE/feature.yml +32 -0
- yadirect_agent-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +49 -0
- yadirect_agent-0.1.0/.github/dependabot.yml +59 -0
- yadirect_agent-0.1.0/.github/workflows/ci.yml +56 -0
- yadirect_agent-0.1.0/.github/workflows/codeql.yml +57 -0
- yadirect_agent-0.1.0/.github/workflows/release.yml +151 -0
- yadirect_agent-0.1.0/.gitignore +18 -0
- yadirect_agent-0.1.0/.pre-commit-config.yaml +52 -0
- yadirect_agent-0.1.0/CLAUDE.md +208 -0
- yadirect_agent-0.1.0/LICENSE +21 -0
- yadirect_agent-0.1.0/Makefile +60 -0
- yadirect_agent-0.1.0/PKG-INFO +187 -0
- yadirect_agent-0.1.0/README.md +131 -0
- yadirect_agent-0.1.0/SECURITY.md +76 -0
- yadirect_agent-0.1.0/agent_policy.example.yml +56 -0
- yadirect_agent-0.1.0/docs/ARCHITECTURE.md +151 -0
- yadirect_agent-0.1.0/docs/BACKLOG.md +1173 -0
- yadirect_agent-0.1.0/docs/CODING_RULES.md +171 -0
- yadirect_agent-0.1.0/docs/OPERATING.md +413 -0
- yadirect_agent-0.1.0/docs/PRIOR_ART.md +240 -0
- yadirect_agent-0.1.0/docs/REVIEW.md +160 -0
- yadirect_agent-0.1.0/docs/TECHNICAL_SPEC.md +1343 -0
- yadirect_agent-0.1.0/docs/TESTING.md +276 -0
- yadirect_agent-0.1.0/pyproject.toml +161 -0
- yadirect_agent-0.1.0/src/yadirect_agent/__init__.py +3 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/__init__.py +15 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/executor.py +555 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/loop.py +469 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/pipeline.py +767 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/plans.py +189 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/prompts.py +56 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/rationale_store.py +122 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/safety.py +1568 -0
- yadirect_agent-0.1.0/src/yadirect_agent/agent/tools.py +807 -0
- yadirect_agent-0.1.0/src/yadirect_agent/audit.py +456 -0
- yadirect_agent-0.1.0/src/yadirect_agent/cli/__init__.py +8 -0
- yadirect_agent-0.1.0/src/yadirect_agent/cli/doctor.py +150 -0
- yadirect_agent-0.1.0/src/yadirect_agent/cli/health.py +123 -0
- yadirect_agent-0.1.0/src/yadirect_agent/cli/main.py +1003 -0
- yadirect_agent-0.1.0/src/yadirect_agent/cli/rationale.py +102 -0
- yadirect_agent-0.1.0/src/yadirect_agent/clients/__init__.py +1 -0
- yadirect_agent-0.1.0/src/yadirect_agent/clients/base.py +247 -0
- yadirect_agent-0.1.0/src/yadirect_agent/clients/direct.py +248 -0
- yadirect_agent-0.1.0/src/yadirect_agent/clients/metrika.py +366 -0
- yadirect_agent-0.1.0/src/yadirect_agent/clients/wordstat.py +76 -0
- yadirect_agent-0.1.0/src/yadirect_agent/config.py +104 -0
- yadirect_agent-0.1.0/src/yadirect_agent/exceptions.py +81 -0
- yadirect_agent-0.1.0/src/yadirect_agent/logging.py +60 -0
- yadirect_agent-0.1.0/src/yadirect_agent/mcp/__init__.py +18 -0
- yadirect_agent-0.1.0/src/yadirect_agent/mcp/server.py +167 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/__init__.py +13 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/campaigns.py +97 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/health.py +114 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/keywords.py +108 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/metrika.py +198 -0
- yadirect_agent-0.1.0/src/yadirect_agent/models/rationale.py +209 -0
- yadirect_agent-0.1.0/src/yadirect_agent/py.typed +0 -0
- yadirect_agent-0.1.0/src/yadirect_agent/rollout.py +186 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/__init__.py +6 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/bidding.py +275 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/campaigns.py +452 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/health_check.py +241 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/reporting.py +275 -0
- yadirect_agent-0.1.0/src/yadirect_agent/services/semantics.py +80 -0
- yadirect_agent-0.1.0/tests/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/evals/README.md +67 -0
- yadirect_agent-0.1.0/tests/evals/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/evals/conftest.py +52 -0
- yadirect_agent-0.1.0/tests/evals/harness.py +256 -0
- yadirect_agent-0.1.0/tests/evals/test_confirm_path_bid_change.py +127 -0
- yadirect_agent-0.1.0/tests/evals/test_pause_low_ctr.py +136 -0
- yadirect_agent-0.1.0/tests/evals/test_safety_reject_budget_cap.py +129 -0
- yadirect_agent-0.1.0/tests/unit/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/agent/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/agent/conftest.py +88 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_executor.py +922 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_executor_rationale.py +354 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_loop.py +413 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_pipeline.py +654 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_plans.py +278 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_prompts.py +31 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_rationale_store.py +196 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_safety.py +2469 -0
- yadirect_agent-0.1.0/tests/unit/agent/test_tools.py +875 -0
- yadirect_agent-0.1.0/tests/unit/cli/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/cli/test_cli.py +1002 -0
- yadirect_agent-0.1.0/tests/unit/cli/test_doctor.py +291 -0
- yadirect_agent-0.1.0/tests/unit/cli/test_health.py +216 -0
- yadirect_agent-0.1.0/tests/unit/cli/test_rationale_cli.py +266 -0
- yadirect_agent-0.1.0/tests/unit/clients/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/clients/test_base.py +370 -0
- yadirect_agent-0.1.0/tests/unit/clients/test_direct.py +227 -0
- yadirect_agent-0.1.0/tests/unit/clients/test_metrika.py +519 -0
- yadirect_agent-0.1.0/tests/unit/conftest.py +60 -0
- yadirect_agent-0.1.0/tests/unit/mcp/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/mcp/test_server.py +182 -0
- yadirect_agent-0.1.0/tests/unit/models/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/models/test_campaigns.py +138 -0
- yadirect_agent-0.1.0/tests/unit/models/test_health.py +164 -0
- yadirect_agent-0.1.0/tests/unit/models/test_keywords.py +239 -0
- yadirect_agent-0.1.0/tests/unit/models/test_metrika.py +228 -0
- yadirect_agent-0.1.0/tests/unit/models/test_rationale.py +286 -0
- yadirect_agent-0.1.0/tests/unit/services/__init__.py +0 -0
- yadirect_agent-0.1.0/tests/unit/services/test_bidding.py +648 -0
- yadirect_agent-0.1.0/tests/unit/services/test_campaigns.py +1075 -0
- yadirect_agent-0.1.0/tests/unit/services/test_health_check.py +540 -0
- yadirect_agent-0.1.0/tests/unit/services/test_reporting.py +495 -0
- yadirect_agent-0.1.0/tests/unit/services/test_semantics.py +229 -0
- yadirect_agent-0.1.0/tests/unit/test_audit.py +635 -0
- yadirect_agent-0.1.0/tests/unit/test_config.py +75 -0
- 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,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"]
|