backloop-sdk 0.1.1__tar.gz → 0.1.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.
Files changed (28) hide show
  1. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/PKG-INFO +1 -1
  2. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/pyproject.toml +1 -1
  3. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/__init__.py +1 -1
  4. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/server.py +8 -0
  5. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_server.py +13 -0
  6. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/.gitignore +0 -0
  7. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/LICENSE +0 -0
  8. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/README.md +0 -0
  9. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/_generated.py +0 -0
  10. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/_http.py +0 -0
  11. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/client.py +0 -0
  12. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/forward.py +0 -0
  13. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/ids.py +0 -0
  14. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/integrations.py +0 -0
  15. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/py.typed +0 -0
  16. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/redact.py +0 -0
  17. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/tool.py +0 -0
  18. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/types.py +0 -0
  19. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/src/backloop/validate.py +0 -0
  20. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/__init__.py +0 -0
  21. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/helpers.py +0 -0
  22. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_client.py +0 -0
  23. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_forward.py +0 -0
  24. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_ids.py +0 -0
  25. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_integrations.py +0 -0
  26. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_redact.py +0 -0
  27. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_tool.py +0 -0
  28. {backloop_sdk-0.1.1 → backloop_sdk-0.1.2}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: backloop-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python SDK for the Agent Feedback Protocol: validate, send and receive structured feedback from AI agents.
5
5
  Project-URL: Homepage, https://trybackloop.com
6
6
  Project-URL: Specification, https://trybackloop.com/spec/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "backloop-sdk"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Python SDK for the Agent Feedback Protocol: validate, send and receive structured feedback from AI agents."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -44,7 +44,7 @@ from .validate import (
44
44
  validate_submission,
45
45
  )
46
46
 
47
- __version__ = "0.1.1"
47
+ __version__ = "0.1.2"
48
48
 
49
49
  __all__ = [
50
50
  "ACK_SCHEMA",
@@ -12,6 +12,7 @@ from datetime import datetime, timezone
12
12
  from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
13
13
 
14
14
  from .ids import new_id
15
+ from .redact import redact as _redact
15
16
  from .types import (
16
17
  DEFAULT_MAX_BYTES,
17
18
  FEEDBACK_TYPES,
@@ -93,6 +94,9 @@ class FeedbackHandler:
93
94
  account behind a framework request; used by the integrations.
94
95
  - ``rate_limit`` is ``(max, window_seconds)`` per account (or per IP), or
95
96
  None to disable it.
97
+ - ``redact`` removes secrets, card numbers and email addresses from what
98
+ agents send, before ``on_record`` sees it. True (default), False, or a
99
+ dict of ``redact()`` options. Not every agent uses a client that does.
96
100
  """
97
101
 
98
102
  def __init__(
@@ -106,6 +110,7 @@ class FeedbackHandler:
106
110
  public_endpoint: Optional[str] = None,
107
111
  rate_limit: Optional[Tuple[int, float]] = (60, 60.0),
108
112
  source: FeedbackSource = "http",
113
+ redact: Union[bool, Mapping[str, Any]] = True,
109
114
  generate_id: Optional[Callable[[], str]] = None,
110
115
  now: Optional[Callable[[], datetime]] = None,
111
116
  ) -> None:
@@ -118,6 +123,7 @@ class FeedbackHandler:
118
123
  self.max_bytes = max_bytes
119
124
  self.public_endpoint = public_endpoint
120
125
  self.source = source
126
+ self.redact = redact
121
127
  self._limiter = _RateLimiter(*rate_limit) if rate_limit else None
122
128
  self._generate_id = generate_id or (lambda: new_id("fb"))
123
129
  self._now = now or (lambda: datetime.now(timezone.utc))
@@ -163,6 +169,8 @@ class FeedbackHandler:
163
169
  if account:
164
170
  record["account"] = account
165
171
  record["source"] = self.source or "http"
172
+ if self.redact is not False:
173
+ parsed = _redact(parsed, **(self.redact if isinstance(self.redact, Mapping) else {}))
166
174
  record["feedback"] = parsed
167
175
 
168
176
  try:
@@ -51,6 +51,19 @@ class HappyPathTest(unittest.TestCase):
51
51
  [{"id": "fb_test", "received_at": "2026-09-28T10:04:11.123Z", "source": "http", "feedback": SUBMISSION}],
52
52
  )
53
53
 
54
+ def test_removes_secrets_and_emails_whatever_client_sent_them(self):
55
+ leaky = dict(SUBMISSION, message="401 with Bearer 9f8e7d6c5b4a3f2e for jane@acme.test", metadata={"password": "hunter2", "plan": "team"})
56
+ handler, sink = make()
57
+ status, _, _ = handler.handle_submit(json.dumps(leaky).encode(), JSON)
58
+ self.assertEqual(status, 202)
59
+ feedback = sink.records[0]["feedback"]
60
+ self.assertEqual(feedback["message"], "401 with [REDACTED] for [REDACTED]")
61
+ self.assertEqual(feedback["metadata"], {"password": "[REDACTED]", "plan": "team"})
62
+ # Asked not to: as sent.
63
+ handler, sink = make(redact=False)
64
+ handler.handle_submit(json.dumps(leaky).encode(), JSON)
65
+ self.assertEqual(sink.records[0]["feedback"]["message"], leaky["message"])
66
+
54
67
  def test_record_carries_service_account_and_source(self):
55
68
  handler, sink = make(service="acme-api", source="mcp")
56
69
  handler.handle_submit(BODY, JSON, "acct_abc")
File without changes
File without changes
File without changes