adloop 0.29.0__tar.gz → 0.29.2__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.
- {adloop-0.29.0 → adloop-0.29.2}/PKG-INFO +1 -1
- {adloop-0.29.0 → adloop-0.29.2}/pyproject.toml +1 -1
- {adloop-0.29.0 → adloop-0.29.2}/pyproject.toml.orig +1 -1
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/approval.py +35 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/server.py +31 -4
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/upload_card.html +7 -1
- {adloop-0.29.0 → adloop-0.29.2}/README.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/__main__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/audiences.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/brands.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/currency.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/enums.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/read.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/shopping.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ads/write.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/auth.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/cli.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/config.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/crossref.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/diagnostics.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ga4/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/ga4/write.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gsc/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gtm/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gtm/read.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/gtm/write.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/merchant/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/merchant/read.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/pagespeed.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/client.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/read.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/reddit/write.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/adloop.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/rules_install.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/runtime.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/approval_card.html +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/audit.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/guards.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/safety/preview.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/tracking.py +0 -0
- {adloop-0.29.0 → adloop-0.29.2}/src/adloop/uploads.py +0 -0
|
@@ -91,6 +91,41 @@ def code_matches(plan: ChangePlan, code: str) -> bool:
|
|
|
91
91
|
return hmac.compare_digest(approval_code(plan), str(code or ""))
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
UPLOAD_META_KEY = "adloop/upload"
|
|
95
|
+
# The upload card's code is good for this long after upload_images opened it.
|
|
96
|
+
_UPLOAD_CODE_SECONDS = 2 * 3600
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def upload_code(now: float | None = None) -> str:
|
|
100
|
+
"""The code an upload card presents with each file: "<expiry>.<signature>".
|
|
101
|
+
|
|
102
|
+
Some apps send a card's own calls without declaring MCP Apps (the Claude
|
|
103
|
+
iOS app), so the upload tool cannot rely on that declaration. The code
|
|
104
|
+
goes to the card in the result's _meta, never into the model's context,
|
|
105
|
+
and is bound to the workspace and an expiry.
|
|
106
|
+
"""
|
|
107
|
+
import time
|
|
108
|
+
|
|
109
|
+
from adloop.runtime import current_tenant
|
|
110
|
+
|
|
111
|
+
expiry = int((now if now is not None else time.time()) + _UPLOAD_CODE_SECONDS)
|
|
112
|
+
message = "\x00".join(("upload", current_tenant(), str(expiry)))
|
|
113
|
+
return f"{expiry}.{hmac.new(_secret, message.encode(), hashlib.sha256).hexdigest()[:32]}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def upload_code_valid(code: str, now: float | None = None) -> bool:
|
|
117
|
+
import time
|
|
118
|
+
|
|
119
|
+
from adloop.runtime import current_tenant
|
|
120
|
+
|
|
121
|
+
expiry, _, signature = str(code or "").partition(".")
|
|
122
|
+
if not expiry.isdigit() or int(expiry) < (now if now is not None else time.time()):
|
|
123
|
+
return False
|
|
124
|
+
message = "\x00".join(("upload", current_tenant(), expiry))
|
|
125
|
+
expected = hmac.new(_secret, message.encode(), hashlib.sha256).hexdigest()[:32]
|
|
126
|
+
return hmac.compare_digest(expected, signature)
|
|
127
|
+
|
|
128
|
+
|
|
94
129
|
def card_click_active() -> bool:
|
|
95
130
|
"""True while ``approve_change`` runs a click from the card."""
|
|
96
131
|
return _card_click.get()
|
|
@@ -252,11 +252,18 @@ _APP_ONLY_TOOLS = {
|
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
class _AppOnlyToolsForCardClients(Middleware):
|
|
255
|
-
"""List
|
|
255
|
+
"""List app-only tools only to apps that render cards.
|
|
256
256
|
|
|
257
257
|
The MCP Apps spec makes a card-capable app hide app-only tools from its
|
|
258
258
|
model. An app without the extension knows nothing about that rule and
|
|
259
259
|
would hand them to its model, so they are not offered to it at all.
|
|
260
|
+
|
|
261
|
+
Calls are a different matter. Some apps send a card's own click without
|
|
262
|
+
the extension declaration (the Claude iOS app does), so refusing every
|
|
263
|
+
call from an "app without cards" breaks the card itself. approve_change
|
|
264
|
+
needs no such check: it only acts on the plan's one-time code, which
|
|
265
|
+
goes to the card and never into the model's context. The upload card's
|
|
266
|
+
tool passes with the declaration or its own code from upload_images.
|
|
260
267
|
"""
|
|
261
268
|
|
|
262
269
|
async def on_list_tools(self, context, call_next):
|
|
@@ -267,7 +274,12 @@ class _AppOnlyToolsForCardClients(Middleware):
|
|
|
267
274
|
|
|
268
275
|
async def on_call_tool(self, context, call_next):
|
|
269
276
|
card = _APP_ONLY_TOOLS.get(context.message.name)
|
|
270
|
-
if card and
|
|
277
|
+
if card and context.message.name == STORE_UPLOAD_TOOL:
|
|
278
|
+
from adloop.safety.approval import upload_code_valid
|
|
279
|
+
|
|
280
|
+
if upload_code_valid((context.message.arguments or {}).get("code", "")):
|
|
281
|
+
card = None
|
|
282
|
+
if card and context.message.name != APPROVE_TOOL and not client_shows_cards():
|
|
271
283
|
from fastmcp.exceptions import ToolError
|
|
272
284
|
|
|
273
285
|
raise ToolError(f"{context.message.name} can only be called from the {card}.")
|
|
@@ -4437,7 +4449,21 @@ def upload_images() -> dict:
|
|
|
4437
4449
|
"""
|
|
4438
4450
|
from adloop.uploads import upload_instructions
|
|
4439
4451
|
|
|
4440
|
-
|
|
4452
|
+
shows_cards = client_shows_cards()
|
|
4453
|
+
result = upload_instructions(shows_cards)
|
|
4454
|
+
if not shows_cards:
|
|
4455
|
+
return result
|
|
4456
|
+
from fastmcp.tools import ToolResult
|
|
4457
|
+
from mcp.types import TextContent
|
|
4458
|
+
|
|
4459
|
+
from adloop.safety.approval import UPLOAD_META_KEY, upload_code
|
|
4460
|
+
|
|
4461
|
+
# The card sends this code with every file (see store_image_upload).
|
|
4462
|
+
return ToolResult(
|
|
4463
|
+
content=[TextContent(type="text", text=json.dumps(result, default=str))],
|
|
4464
|
+
structured_content=result,
|
|
4465
|
+
meta={UPLOAD_META_KEY: {"code": upload_code()}},
|
|
4466
|
+
)
|
|
4441
4467
|
|
|
4442
4468
|
|
|
4443
4469
|
@_tool(
|
|
@@ -4447,7 +4473,7 @@ def upload_images() -> dict:
|
|
|
4447
4473
|
meta={"ui": {"resourceUri": UPLOAD_CARD_URI, "visibility": ["app"]}},
|
|
4448
4474
|
)
|
|
4449
4475
|
@_safe
|
|
4450
|
-
def store_image_upload(name: str, data_base64: str) -> dict:
|
|
4476
|
+
def store_image_upload(name: str, data_base64: str, code: str = "") -> dict:
|
|
4451
4477
|
"""Store an image dropped on the AdLoop upload card.
|
|
4452
4478
|
|
|
4453
4479
|
Called only by the upload card; AI apps keep this tool out of the
|
|
@@ -4456,6 +4482,7 @@ def store_image_upload(name: str, data_base64: str) -> dict:
|
|
|
4456
4482
|
Args:
|
|
4457
4483
|
name: The file name, used for the asset name.
|
|
4458
4484
|
data_base64: The file content, base64-encoded.
|
|
4485
|
+
code: The upload card's code from upload_images.
|
|
4459
4486
|
"""
|
|
4460
4487
|
import base64
|
|
4461
4488
|
import binascii
|
|
@@ -77,6 +77,10 @@
|
|
|
77
77
|
(() => {
|
|
78
78
|
"use strict";
|
|
79
79
|
const TOOL = "store_image_upload";
|
|
80
|
+
const META_KEY = "adloop/upload";
|
|
81
|
+
// From upload_images: some apps send the card's calls without declaring
|
|
82
|
+
// MCP Apps, so each file carries the card's code instead.
|
|
83
|
+
let code = "";
|
|
80
84
|
const MAX_BYTES = 5120 * 1024;
|
|
81
85
|
const TYPES = ["image/png", "image/jpeg", "image/gif"];
|
|
82
86
|
const STRINGS = {
|
|
@@ -168,7 +172,7 @@
|
|
|
168
172
|
if (file.size > MAX_BYTES) { row.state.textContent = t("tooBig"); row.state.classList.add("bad"); continue; }
|
|
169
173
|
row.state.textContent = t("uploading");
|
|
170
174
|
try {
|
|
171
|
-
const result = await request("tools/call", { name: TOOL, arguments: { name: file.name, data_base64: await readBase64(file) } });
|
|
175
|
+
const result = await request("tools/call", { name: TOOL, arguments: { name: file.name, data_base64: await readBase64(file), code } });
|
|
172
176
|
const r = payloadOf(result);
|
|
173
177
|
if ((result && result.isError) || r.error || !r.upload_id) throw new Error(r.error || "upload failed");
|
|
174
178
|
row.meta.textContent = describe(r);
|
|
@@ -200,6 +204,8 @@
|
|
|
200
204
|
|
|
201
205
|
let shownExisting = false;
|
|
202
206
|
function onToolResult(result) {
|
|
207
|
+
const meta = result && result._meta && result._meta[META_KEY];
|
|
208
|
+
if (meta && meta.code) code = meta.code;
|
|
203
209
|
if (shownExisting || !result) return;
|
|
204
210
|
shownExisting = true;
|
|
205
211
|
const r = payloadOf(result);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|