Skip to content

Commit 6676ee3

Browse files
bokelleyclaude
andcommitted
examples: salesagent side-car Phase 2 reference implementation
Reference implementation for Phase 2 of the salesagent side-car experiment (PR #506). Lives in adcp-client-python as a doc-and- review-friendly skeleton; deploys into a salesagent fork worktree under src/sdk_runtime/ where it imports salesagent's _impl functions and models. Files: - account_store.py — SalesagentBuyerAgentRegistry + SalesagentAccountStore. Bearer-token (Principal.access_token) lookup; AgentAccountAccess scoping; sandbox→mode projection. fetch_gam_manual_approval_required helper for the HITL gate. - gam_platform.py — GAMPlatform wraps salesagent's _impl functions: _get_products_impl, _create_media_buy_impl, _update_media_buy_impl, _get_media_buy_delivery_impl, _sync_creatives_impl. Builds ResolvedIdentity from SDK ctx. - hitl_gate.py — compose_method before-hook. Wire→GAM-internal operation name mapping (create_media_buy, update_media_buy, sync_creatives→add_creative_assets). Writes WorkflowStep + MediaBuy(raw_request=...) rows via salesagent's existing WorkflowManager; short-circuits with status='pending_approval'. - serve_sidecar.py — adcp.serve(...) entrypoint. Wires platform + HITL gates + auth shim + WebhookSender (SDK→SDK signing per Step 0.6, since salesagent's scheme is incompatible). - README.md — deployment recipe (docker-compose addition, nginx routing config, scheduler patches, tenant configuration, storyboard run commands), exit criteria, open work in scaffold. - __init__.py The salesagent imports are wrapped in try/except so the files lint and import in adcp-client-python's tree without salesagent installed; SALESAGENT_AVAILABLE flag gates runtime behavior. Status: structurally complete reference. Actual storyboard run requires deploying into a salesagent fork worktree with sandbox GAM credentials, configuring the experiment tenant, and running nginx + docker-compose (recipe in README). Local-fork only — no upstream PR to salesagent per the experiment scope constraint. Open gaps documented in README: - _build_resolved_identity may need more fields than the minimal projection here - WorkflowManager constructor signature varies across salesagent versions; pin to a commit - Webhook signing parity is SDK→SDK only (Step 0.6 finding) Recipe shape already validated end-to-end in Phase 1B (examples/recipe_falsification/, 8 tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e4f25d6 commit 6676ee3

6 files changed

Lines changed: 1013 additions & 0 deletions

File tree

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Salesagent side-car experiment — Phase 2 reference implementation
2+
3+
The side-car runtime that runs alongside salesagent's existing
4+
`adcp-server` container, serving one experiment tenant via SDK
5+
primitives while leaving every other tenant on the legacy runtime.
6+
7+
This directory is the **reference implementation** in
8+
adcp-client-python. The actual deployment is into a salesagent
9+
fork worktree, where these files mount under `src/sdk_runtime/`
10+
and import salesagent's `_impl` functions, models, and
11+
workflow_manager.
12+
13+
See the [experiment plan](../../docs/proposals/salesagent-sidecar-experiment.md)
14+
for the full design (Phases 1A, 1B, 2; Step 0 prereqs; learning
15+
questions Q1–Q5; pre-registered falsification signals).
16+
17+
## What's here
18+
19+
| File | What it does |
20+
|---|---|
21+
| `account_store.py` | `SalesagentBuyerAgentRegistry` (Principal.access_token bearer lookup) + `SalesagentAccountStore` (Account row → SDK Account, AgentAccountAccess scoping) + `fetch_gam_manual_approval_required` (HITL flag) |
22+
| `gam_platform.py` | `GAMPlatform` wraps salesagent's `_impl` functions: `_get_products_impl`, `_create_media_buy_impl`, `_update_media_buy_impl`, `_get_media_buy_delivery_impl`, `_sync_creatives_impl`. Builds `ResolvedIdentity` from SDK ctx |
23+
| `hitl_gate.py` | `compose_method` `before` hook — checks `gam_manual_approval_required`, writes `WorkflowStep` + `MediaBuy(raw_request=...)` rows via salesagent's existing `WorkflowManager`, short-circuits with `status='pending_approval'`. Wire→GAM-internal operation name mapping for the approval-config check |
24+
| `serve_sidecar.py` | Entrypoint — `adcp.serve(...)` with the platform + auth shim + `WebhookSender` configured |
25+
26+
## How it ties to salesagent
27+
28+
```
29+
┌──────────────────────────────────────────────────────────────┐
30+
│ nginx proxy (port 8000) │
31+
│ route by X-Tenant-Id header: │
32+
│ experiment_tenant → adcp-sidecar (port 8081) │
33+
│ everyone else → adcp-server (port 8080) │
34+
└──────────────────┬─────────────────────┬─────────────────────┘
35+
│ │
36+
┌──────────▼─────────┐ ┌────────▼───────────┐
37+
│ adcp-server (8080) │ │ adcp-sidecar (8081)│
38+
│ legacy runtime: │ │ this code: │
39+
│ - FastMCP server │ │ - adcp.serve(...) │
40+
│ - A2A server │ │ - GAMPlatform │
41+
│ - existing tools │ │ - HITL gates │
42+
└──────────┬─────────┘ └────────┬───────────┘
43+
│ │
44+
↓ ↓
45+
┌─────────────────────────────────────────────┐
46+
│ Postgres (shared) — Tenant, Principal, │
47+
│ Account, Product, MediaBuy, WorkflowStep │
48+
└─────────────────────────────────────────────┘
49+
```
50+
51+
## Deployment recipe (salesagent fork only — local, no upstream PR)
52+
53+
In a salesagent worktree (created via
54+
`git worktree add /Users/brianokelley/Developer/salesagent/.conductor/sidecar-experiment -b bokelley/sidecar-experiment main`):
55+
56+
### 1. Copy this directory in
57+
58+
```bash
59+
cp -r /path/to/adcp-client-python/examples/salesagent_sidecar \
60+
/Users/brianokelley/Developer/salesagent/.conductor/sidecar-experiment/src/sdk_runtime
61+
```
62+
63+
The imports inside the files use `from src.core.tools...` and
64+
`from src.core.database.models...` — they'll resolve in the salesagent
65+
container.
66+
67+
### 2. Patch the two cross-tenant schedulers (Step 0.4)
68+
69+
In the worktree, edit:
70+
71+
* `src/services/media_buy_status_scheduler.py` — add tenant filter
72+
* `src/services/delivery_webhook_scheduler.py` — add tenant filter
73+
74+
```python
75+
# At top of each scheduler module
76+
import os
77+
SKIP_TENANT_IDS = {
78+
t.strip()
79+
for t in os.environ.get("EXPERIMENT_TENANT_IDS", "").split(",")
80+
if t.strip()
81+
}
82+
83+
# In the cross-tenant query, add:
84+
stmt = stmt.where(MediaBuy.tenant_id.notin_(SKIP_TENANT_IDS))
85+
```
86+
87+
(Two cross-tenant schedulers; per-tenant disable is local-fork only,
88+
not pushed upstream.)
89+
90+
### 3. Add the side-car service to docker-compose
91+
92+
Append to `docker-compose.yml`:
93+
94+
```yaml
95+
adcp-sidecar:
96+
build:
97+
context: .
98+
dockerfile: Dockerfile
99+
entrypoint: []
100+
env_file:
101+
- path: .env
102+
required: false
103+
environment:
104+
DATABASE_URL: postgresql://adcp_user:secure_password_change_me@postgres:5432/adcp?sslmode=disable
105+
EXPERIMENT_TENANT_IDS: "tenant_acme_test"
106+
SIDECAR_PORT: "8081"
107+
SIDECAR_WEBHOOK_SECRET: "test-secret-bytes-for-adcp-legacy"
108+
PYTHONPATH: "/app/.venv/lib/python3.12/site-packages:/app"
109+
depends_on:
110+
postgres:
111+
condition: service_healthy
112+
db-init:
113+
condition: service_completed_successfully
114+
volumes:
115+
- .:/app
116+
- /app/.venv
117+
# Mount adcp-client-python source for live changes
118+
- ../../adcp-client-python/src/adcp:/app/.venv/lib/python3.12/site-packages/adcp:ro
119+
command: ["python", "-m", "src.sdk_runtime.serve_sidecar"]
120+
healthcheck:
121+
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
122+
interval: 30s
123+
timeout: 10s
124+
retries: 3
125+
```
126+
127+
### 4. Update nginx config to route by tenant
128+
129+
In `config/nginx/nginx-development.conf`, add a `map` block at the
130+
top to detect the experiment tenant from headers and route:
131+
132+
```nginx
133+
map $http_x_tenant_id $upstream_runtime {
134+
default adcp-server:8080;
135+
tenant_acme_test adcp-sidecar:8081;
136+
}
137+
138+
# Then in the server block, replace the upstream proxy_pass with:
139+
proxy_pass http://$upstream_runtime;
140+
```
141+
142+
### 5. Bring it up
143+
144+
```bash
145+
cd /Users/brianokelley/Developer/salesagent/.conductor/sidecar-experiment
146+
docker compose up --build
147+
```
148+
149+
Both adcp-server (legacy) and adcp-sidecar (SDK) come up. Nginx routes
150+
by `X-Tenant-Id` header.
151+
152+
### 6. Configure the experiment tenant
153+
154+
Through salesagent's admin UI at http://localhost:8000/:
155+
156+
* Create tenant `tenant_acme_test`
157+
* Create at least one Principal under it (note the `access_token`)
158+
* Create at least one Account with `sandbox=True`
159+
* Configure GAM credentials (real sandbox Network ID + service account JSON)
160+
* Set `gam_manual_approval_required=False` for first run; flip to `True`
161+
for HITL exercise
162+
163+
### 7. Run the storyboards
164+
165+
```bash
166+
# Happy path (instant approval)
167+
npx -y -p @adcp/client adcp storyboard run \
168+
-H "X-Tenant-Id: tenant_acme_test" \
169+
-H "Authorization: Bearer <principal_access_token>" \
170+
http://localhost:8000/mcp media_buy_seller --json
171+
172+
# HITL exercise (approval flow) — set gam_manual_approval_required=True first
173+
npx -y -p @adcp/client adcp storyboard run \
174+
-H "X-Tenant-Id: tenant_acme_test" \
175+
-H "Authorization: Bearer <principal_access_token>" \
176+
http://localhost:8000/mcp media_buy_guaranteed_approval --json
177+
```
178+
179+
## Exit criteria (from PR #506)
180+
181+
1. Both storyboards pass against sandbox GAM
182+
2. Recipe carries `implementation_config` without escape hatches
183+
(✅ already confirmed in Phase 1B —
184+
[`examples/recipe_falsification/`](../recipe_falsification/))
185+
3. Glue LOC under ratio thresholds (proposal-side ≤60%,
186+
decisioning-side ≤30%)
187+
4. Zero structural-guard allowlist additions (per Step 0.3, no
188+
guards fire on `src/sdk_runtime/`)
189+
5. At least one finding contradicts a #502 prior (✅ already
190+
satisfied — Q1.5 in Phase 1A)
191+
6. Webhook signature verified by a subscribed test buyer (per
192+
Step 0.6, parity is SDK→SDK only since salesagent's scheme
193+
is incompatible)
194+
195+
## Open work in this scaffold
196+
197+
The reference implementation here is structurally complete but has
198+
gaps that surface during deployment. Document each as a finding when
199+
you hit it:
200+
201+
* **`_build_resolved_identity` may need more fields** — `_impl`s
202+
read `identity.tenant.gemini_api_key`, `identity.tenant.advertising_policy`,
203+
etc. The minimal projection here may need expanding.
204+
* **`_create_media_buy_impl` returns `CreateMediaBuyResult`** (a
205+
wrapper with `.response` and `.status`) — ensure the return-type
206+
projection matches the wire shape.
207+
* **`WorkflowManager` constructor signature** varies across
208+
salesagent versions — pin to a specific commit and adjust if
209+
needed.
210+
* **`_already_approved` setattr survival** — verified against
211+
`compose_method` in Step 0.5, but make sure no salesagent-side
212+
request projection re-validates between admin UI approval and
213+
the sidecar's gate check.
214+
* **F12 webhook test against subscribed buyer** uses
215+
`adcp.WebhookReceiver` with matching secret (per Step 0.6) —
216+
not real salesagent buyers.
217+
218+
## Why local-fork, not upstream PR
219+
220+
The user explicitly scoped this experiment as local-fork only
221+
(no PRs to salesagent). The scheduler patches and `src/sdk_runtime/`
222+
directory live as local commits in the salesagent worktree;
223+
`git checkout main` reverts everything cleanly.
224+
225+
## See also
226+
227+
* [Experiment plan](../../docs/proposals/salesagent-sidecar-experiment.md)
228+
* [Recipe falsification (Phase 1B)](../recipe_falsification/)
229+
* [Product architecture (revised post-experiment)](../../docs/proposals/product-architecture.md)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Salesagent side-car experiment — Phase 2 reference implementation.
2+
3+
See README.md for what's here and how to deploy into a salesagent fork.
4+
"""

0 commit comments

Comments
 (0)