agent-first-data 0.4.2__tar.gz → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.4.2
3
+ Version: 0.6.0
4
4
  Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/cmnspore/agent-first-data
@@ -74,24 +74,24 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
74
 
75
75
  ## API Reference
76
76
 
77
- Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
77
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
81
- Build AFDATA protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
82
82
 
83
83
  ```python
84
84
  # Success (result)
85
85
  build_json_ok(result: Any, trace: Any = None) -> dict
86
86
 
87
- # Error (simple message)
88
- build_json_error(message: str, trace: Any = None) -> dict
87
+ # Error (simple message, optional hint)
88
+ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
89
89
 
90
90
  # Generic (any code + fields)
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
94
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
95
95
 
96
96
  **Example:**
97
97
  ```python
@@ -118,6 +118,9 @@ response = build_json_ok(
118
118
  # Error
119
119
  err = build_json_error("user not found", trace={"duration_ms": 5})
120
120
 
121
+ # Error with hint
122
+ err_hint = build_json_error("wallet not found", hint="list wallets with: afpay wallet list", trace={"duration_ms": 5})
123
+
121
124
  # Specific error code
122
125
  not_found = build_json(
123
126
  "not_found",
@@ -128,14 +131,21 @@ not_found = build_json(
128
131
 
129
132
  ### CLI/Log Output (returns str)
130
133
 
131
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
134
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
132
135
 
133
136
  ```python
134
137
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
138
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
135
139
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
136
140
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
137
141
  ```
138
142
 
143
+ ```python
144
+ class RedactionPolicy(enum.Enum):
145
+ RedactionTraceOnly = "RedactionTraceOnly"
146
+ RedactionNone = "RedactionNone"
147
+ ```
148
+
139
149
  **Example:**
140
150
  ```python
141
151
  from agent_first_data import *
@@ -197,7 +207,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
197
207
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
208
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
209
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
- build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
210
+ build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
201
211
  ```
202
212
 
203
213
  **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
@@ -65,24 +65,24 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
65
65
 
66
66
  ## API Reference
67
67
 
68
- Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
68
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
69
69
 
70
70
  ### Protocol Builders (returns dict)
71
71
 
72
- Build AFDATA protocol structures. Return dict objects for API responses.
72
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
73
73
 
74
74
  ```python
75
75
  # Success (result)
76
76
  build_json_ok(result: Any, trace: Any = None) -> dict
77
77
 
78
- # Error (simple message)
79
- build_json_error(message: str, trace: Any = None) -> dict
78
+ # Error (simple message, optional hint)
79
+ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
80
80
 
81
81
  # Generic (any code + fields)
82
82
  build_json(code: str, fields: Any, trace: Any = None) -> dict
83
83
  ```
84
84
 
85
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
85
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
86
86
 
87
87
  **Example:**
88
88
  ```python
@@ -109,6 +109,9 @@ response = build_json_ok(
109
109
  # Error
110
110
  err = build_json_error("user not found", trace={"duration_ms": 5})
111
111
 
112
+ # Error with hint
113
+ err_hint = build_json_error("wallet not found", hint="list wallets with: afpay wallet list", trace={"duration_ms": 5})
114
+
112
115
  # Specific error code
113
116
  not_found = build_json(
114
117
  "not_found",
@@ -119,14 +122,21 @@ not_found = build_json(
119
122
 
120
123
  ### CLI/Log Output (returns str)
121
124
 
122
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
125
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
123
126
 
124
127
  ```python
125
128
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
129
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
126
130
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
127
131
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
128
132
  ```
129
133
 
134
+ ```python
135
+ class RedactionPolicy(enum.Enum):
136
+ RedactionTraceOnly = "RedactionTraceOnly"
137
+ RedactionNone = "RedactionNone"
138
+ ```
139
+
130
140
  **Example:**
131
141
  ```python
132
142
  from agent_first_data import *
@@ -188,7 +198,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
188
198
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
189
199
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
190
200
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
191
- build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
201
+ build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
192
202
  ```
193
203
 
194
204
  **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
@@ -4,7 +4,9 @@ from agent_first_data.format import (
4
4
  build_json_ok,
5
5
  build_json_error,
6
6
  build_json,
7
+ RedactionPolicy,
7
8
  output_json,
9
+ output_json_with,
8
10
  output_yaml,
9
11
  output_plain,
10
12
  internal_redact_secrets,
@@ -33,7 +35,9 @@ __all__ = [
33
35
  "build_json_ok",
34
36
  "build_json_error",
35
37
  "build_json",
38
+ "RedactionPolicy",
36
39
  "output_json",
40
+ "output_json_with",
37
41
  "output_yaml",
38
42
  "output_plain",
39
43
  "internal_redact_secrets",
@@ -69,7 +69,7 @@ def cli_output(value: Any, format: OutputFormat) -> str:
69
69
  return output_json(value)
70
70
 
71
71
 
72
- def build_cli_error(message: str) -> dict:
72
+ def build_cli_error(message: str, hint: str | None = None) -> dict:
73
73
  """Build a standard CLI parse error value.
74
74
 
75
75
  Use when argument parsing fails or a flag value is invalid.
@@ -83,10 +83,13 @@ def build_cli_error(message: str) -> dict:
83
83
  >>> v["retryable"]
84
84
  False
85
85
  """
86
- return {
86
+ m: dict = {
87
87
  "code": "error",
88
88
  "error_code": "invalid_request",
89
89
  "error": message,
90
90
  "retryable": False,
91
91
  "trace": {"duration_ms": 0},
92
92
  }
93
+ if hint is not None:
94
+ m["hint"] = hint
95
+ return m
@@ -1,13 +1,14 @@
1
1
  """AFDATA output formatting and protocol templates.
2
2
 
3
- 8 public APIs: 3 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
+ 9 public APIs and 1 type: 3 protocol builders + 4 output formatters + 1 redaction + 1 utility + RedactionPolicy.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import copy
9
8
  import json
9
+ import math
10
10
  from datetime import datetime, timezone
11
+ from enum import Enum
11
12
  from typing import Any
12
13
 
13
14
 
@@ -24,9 +25,11 @@ def build_json_ok(result: Any, trace: Any = None) -> dict:
24
25
  return m
25
26
 
26
27
 
27
- def build_json_error(message: str, trace: Any = None) -> dict:
28
- """Build {code: "error", error: message, trace?}."""
28
+ def build_json_error(message: str, hint: str | None = None, trace: Any = None) -> dict:
29
+ """Build {code: "error", error: message, hint?, trace?}."""
29
30
  m: dict = {"code": "error", "error": message}
31
+ if hint is not None:
32
+ m["hint"] = hint
30
33
  if trace is not None:
31
34
  m["trace"] = trace
32
35
  return m
@@ -45,16 +48,28 @@ def build_json(code: str, fields: Any, trace: Any = None) -> dict:
45
48
  # Public API: Output Formatters
46
49
  # ═══════════════════════════════════════════
47
50
 
51
+ class RedactionPolicy(str, Enum):
52
+ RedactionTraceOnly = "RedactionTraceOnly"
53
+ RedactionNone = "RedactionNone"
54
+
48
55
 
49
56
  def output_json(value: Any) -> str:
50
57
  """Format as single-line JSON. Secrets redacted, original keys, raw values."""
51
- v = copy.deepcopy(value)
58
+ v = _sanitize_for_json(value)
52
59
  _redact_secrets(v)
53
60
  return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
54
61
 
55
62
 
63
+ def output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str:
64
+ """Format as single-line JSON with explicit redaction policy."""
65
+ v = _sanitize_for_json(value)
66
+ _apply_redaction_policy(v, redaction_policy)
67
+ return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
68
+
69
+
56
70
  def output_yaml(value: Any) -> str:
57
71
  """Format as multi-line YAML. Keys stripped, values formatted, secrets redacted."""
72
+ value = _sanitize_for_json(value)
58
73
  lines = ["---"]
59
74
  _render_yaml_processed(value, 0, lines)
60
75
  return "\n".join(lines)
@@ -62,6 +77,7 @@ def output_yaml(value: Any) -> str:
62
77
 
63
78
  def output_plain(value: Any) -> str:
64
79
  """Format as single-line logfmt. Keys stripped, values formatted, secrets redacted."""
80
+ value = _sanitize_for_json(value)
65
81
  pairs: list[tuple[str, str]] = []
66
82
  _collect_plain_pairs(value, "", pairs)
67
83
  pairs.sort(key=lambda p: p[0].encode("utf-16-be"))
@@ -84,6 +100,17 @@ def internal_redact_secrets(value: Any) -> None:
84
100
  _redact_secrets(value)
85
101
 
86
102
 
103
+ def _apply_redaction_policy(value: Any, redaction_policy: RedactionPolicy) -> None:
104
+ if redaction_policy == RedactionPolicy.RedactionTraceOnly:
105
+ if isinstance(value, dict) and "trace" in value:
106
+ _redact_secrets(value["trace"])
107
+ return
108
+ if redaction_policy == RedactionPolicy.RedactionNone:
109
+ return
110
+ # Safety fallback for unknown values.
111
+ _redact_secrets(value)
112
+
113
+
87
114
  def parse_size(s: str) -> int | None:
88
115
  """Parse a human-readable size string into bytes.
89
116
 
@@ -126,6 +153,43 @@ def parse_size(s: str) -> int | None:
126
153
  # ═══════════════════════════════════════════
127
154
 
128
155
 
156
+ def _sanitize_for_json(value: Any, stack: set[int] | None = None) -> Any:
157
+ if stack is None:
158
+ stack = set()
159
+
160
+ if value is None or isinstance(value, (str, bool, int)):
161
+ return value
162
+ if isinstance(value, float):
163
+ if math.isfinite(value):
164
+ return value
165
+ return "<unsupported:float>"
166
+ if isinstance(value, BaseException):
167
+ return str(value)
168
+
169
+ if isinstance(value, dict):
170
+ obj_id = id(value)
171
+ if obj_id in stack:
172
+ return "<unsupported:circular>"
173
+ stack.add(obj_id)
174
+ out: dict[str, Any] = {}
175
+ for k, v in value.items():
176
+ key = k if isinstance(k, str) else str(k)
177
+ out[key] = _sanitize_for_json(v, stack)
178
+ stack.remove(obj_id)
179
+ return out
180
+
181
+ if isinstance(value, (list, tuple)):
182
+ obj_id = id(value)
183
+ if obj_id in stack:
184
+ return "<unsupported:circular>"
185
+ stack.add(obj_id)
186
+ out = [_sanitize_for_json(item, stack) for item in value]
187
+ stack.remove(obj_id)
188
+ return out
189
+
190
+ return f"<unsupported:{type(value).__name__}>"
191
+
192
+
129
193
  def _redact_secrets(value: Any) -> None:
130
194
  if isinstance(value, dict):
131
195
  for k in list(value.keys()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.4.2
3
+ Version: 0.6.0
4
4
  Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/cmnspore/agent-first-data
@@ -74,24 +74,24 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
74
 
75
75
  ## API Reference
76
76
 
77
- Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
77
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
81
- Build AFDATA protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
82
82
 
83
83
  ```python
84
84
  # Success (result)
85
85
  build_json_ok(result: Any, trace: Any = None) -> dict
86
86
 
87
- # Error (simple message)
88
- build_json_error(message: str, trace: Any = None) -> dict
87
+ # Error (simple message, optional hint)
88
+ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
89
89
 
90
90
  # Generic (any code + fields)
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
94
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
95
95
 
96
96
  **Example:**
97
97
  ```python
@@ -118,6 +118,9 @@ response = build_json_ok(
118
118
  # Error
119
119
  err = build_json_error("user not found", trace={"duration_ms": 5})
120
120
 
121
+ # Error with hint
122
+ err_hint = build_json_error("wallet not found", hint="list wallets with: afpay wallet list", trace={"duration_ms": 5})
123
+
121
124
  # Specific error code
122
125
  not_found = build_json(
123
126
  "not_found",
@@ -128,14 +131,21 @@ not_found = build_json(
128
131
 
129
132
  ### CLI/Log Output (returns str)
130
133
 
131
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
134
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
132
135
 
133
136
  ```python
134
137
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
138
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
135
139
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
136
140
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
137
141
  ```
138
142
 
143
+ ```python
144
+ class RedactionPolicy(enum.Enum):
145
+ RedactionTraceOnly = "RedactionTraceOnly"
146
+ RedactionNone = "RedactionNone"
147
+ ```
148
+
139
149
  **Example:**
140
150
  ```python
141
151
  from agent_first_data import *
@@ -197,7 +207,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
197
207
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
208
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
209
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
- build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
210
+ build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
201
211
  ```
202
212
 
203
213
  **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.4.2"
3
+ version = "0.6.0"
4
4
  description = "Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents"
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -112,6 +112,12 @@ class TestCodeOverride:
112
112
  assert m["code"] == "log"
113
113
  assert m["event"] == "startup"
114
114
 
115
+ def test_exception_field_is_readable(self):
116
+ logger = make_logger("test_exc")
117
+ adapter = get_logger("test_exc")
118
+ m = capture_log(lambda: adapter.error("request failed", extra={"error": Exception("timeout")}))
119
+ assert m["error"] == "timeout"
120
+
115
121
 
116
122
  class TestGetLogger:
117
123
  def test_default_fields(self):
@@ -75,6 +75,16 @@ def test_build_cli_error_is_valid_json():
75
75
  assert parsed["code"] == "error"
76
76
 
77
77
 
78
+ def test_build_cli_error_with_hint():
79
+ v = build_cli_error("bad flag", hint="try --help")
80
+ assert v["hint"] == "try --help"
81
+
82
+
83
+ def test_build_cli_error_without_hint_has_no_hint_key():
84
+ v = build_cli_error("oops")
85
+ assert "hint" not in v
86
+
87
+
78
88
  # ── cli_output ────────────────────────────────────────────────────────────────
79
89
 
80
90
  def test_cli_output_dispatches_json():
@@ -7,7 +7,10 @@ from agent_first_data import (
7
7
  build_json_ok,
8
8
  build_json_error,
9
9
  build_json,
10
+ RedactionPolicy,
10
11
  internal_redact_secrets,
12
+ output_json,
13
+ output_json_with,
11
14
  )
12
15
  from agent_first_data.format import (
13
16
  _format_bytes_human,
@@ -50,7 +53,11 @@ def test_protocol_fixtures():
50
53
  elif typ == "error":
51
54
  result = build_json_error(args["message"])
52
55
  elif typ == "error_trace":
53
- result = build_json_error(args["message"], args["trace"])
56
+ result = build_json_error(args["message"], trace=args["trace"])
57
+ elif typ == "error_hint":
58
+ result = build_json_error(args["message"], hint=args.get("hint"))
59
+ elif typ == "error_hint_trace":
60
+ result = build_json_error(args["message"], hint=args.get("hint"), trace=args["trace"])
54
61
  elif typ == "status":
55
62
  result = build_json(args["code"], args.get("fields"))
56
63
  else:
@@ -83,3 +90,52 @@ def test_helper_fixtures():
83
90
  elif name == "parse_size":
84
91
  got = parse_size(inp)
85
92
  assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
93
+
94
+
95
+ def test_output_json_exception_field_is_readable():
96
+ out = output_json({"error": Exception("timeout")})
97
+ parsed = json.loads(out)
98
+ assert parsed["error"] == "timeout"
99
+
100
+
101
+ def test_output_json_unsupported_value_does_not_leak_secret():
102
+ class SecretRepr:
103
+ def __repr__(self) -> str:
104
+ return "Secret(sk-live-123)"
105
+
106
+ out = output_json({"meta": SecretRepr(), "api_key_secret": "sk-live-123"})
107
+ assert "sk-live-123" not in out
108
+ parsed = json.loads(out)
109
+ assert parsed["api_key_secret"] == "***"
110
+ assert parsed["meta"].startswith("<unsupported:")
111
+
112
+
113
+ def test_output_json_circular_reference():
114
+ v = {}
115
+ v["self"] = v
116
+ out = output_json(v)
117
+ parsed = json.loads(out)
118
+ assert parsed["self"] == "<unsupported:circular>"
119
+
120
+
121
+ def test_output_json_with_trace_only_redacts_only_trace():
122
+ out = output_json_with(
123
+ {
124
+ "code": "ok",
125
+ "result": {"api_key_secret": "sk-live-123"},
126
+ "trace": {"request_secret": "top-secret"},
127
+ },
128
+ RedactionPolicy.RedactionTraceOnly,
129
+ )
130
+ parsed = json.loads(out)
131
+ assert parsed["trace"]["request_secret"] == "***"
132
+ assert parsed["result"]["api_key_secret"] == "sk-live-123"
133
+
134
+
135
+ def test_output_json_with_none_keeps_secrets():
136
+ out = output_json_with(
137
+ {"api_key_secret": "sk-live-123"},
138
+ RedactionPolicy.RedactionNone,
139
+ )
140
+ parsed = json.loads(out)
141
+ assert parsed["api_key_secret"] == "sk-live-123"