agent-first-data 0.7.4__tar.gz → 0.8.1__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,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.7.4
4
- Summary: Agent-First Data (AFDATA) suffix-driven output formatting and protocol templates for AI agents
3
+ Version: 0.8.1
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
- Project-URL: Repository, https://github.com/cmnspore/agent-first-data
6
+ Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
 
@@ -74,7 +74,7 @@ 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: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
77
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
@@ -91,6 +91,16 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
+ ### Redaction Helpers (returns Any)
95
+
96
+ Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
97
+
98
+ ```python
99
+ redacted_value(value: Any) -> Any
100
+ redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
101
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
102
+ ```
103
+
94
104
  **Use case:** structured protocol payloads (frameworks automatically serialize)
95
105
 
96
106
  **Example:**
@@ -129,23 +139,31 @@ not_found = build_json(
129
139
  )
130
140
  ```
131
141
 
132
- ### CLI/Log Output (returns str)
142
+ ### Output Formatters (returns str)
133
143
 
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.
144
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use the `*_with_options` functions to pass legacy secret names such as `api_key`. YAML and Plain always redact secrets and apply human-readable formatting.
135
145
 
136
146
  ```python
137
147
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
138
148
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
149
+ output_json_with_options(value: Any, redaction_options: RedactionOptions) -> str
139
150
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
151
+ output_yaml_with_options(value: Any, redaction_options: RedactionOptions) -> str
140
152
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
153
+ output_plain_with_options(value: Any, redaction_options: RedactionOptions) -> str
141
154
  ```
142
155
 
143
156
  ```python
144
157
  class RedactionPolicy(enum.Enum):
145
158
  RedactionTraceOnly = "RedactionTraceOnly"
146
159
  RedactionNone = "RedactionNone"
160
+ RedactionStrict = "RedactionStrict"
161
+
162
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
147
163
  ```
148
164
 
165
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. They do not change YAML/Plain suffix stripping.
166
+
149
167
  **Example:**
150
168
  ```python
151
169
  from agent_first_data import *
@@ -178,6 +196,7 @@ print(output_plain(data))
178
196
 
179
197
  ```python
180
198
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
199
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
181
200
  ```
182
201
 
183
202
  Most users don't need this. Output functions automatically protect secrets.
@@ -513,7 +532,7 @@ All formats automatically redact `_secret` fields.
513
532
 
514
533
  ## Repository
515
534
 
516
- This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
535
+ This package is part of the [agent-first-data](https://github.com/agentfirstkit/agent-first-data) repository, which also contains:
517
536
 
518
537
  - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
519
538
  - **`skills/`** — AI coding agent skill for working with AFDATA conventions
@@ -521,7 +540,7 @@ This package is part of the [agent-first-data](https://github.com/cmnspore/agent
521
540
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
522
541
 
523
542
  ```bash
524
- git clone https://github.com/cmnspore/agent-first-data
543
+ git clone https://github.com/agentfirstkit/agent-first-data
525
544
  cd agent-first-data/python
526
545
  python -m pytest
527
546
  ```
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: agent-first-data
3
- Version: 0.7.4
4
- Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
- License-Expression: MIT
6
- Project-URL: Repository, https://github.com/cmnspore/agent-first-data
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
1
  # agent-first-data
11
2
 
12
3
  **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
@@ -74,7 +65,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
65
 
75
66
  ## API Reference
76
67
 
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`)
68
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
78
69
 
79
70
  ### Protocol Builders (returns dict)
80
71
 
@@ -91,6 +82,16 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
91
82
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
83
  ```
93
84
 
85
+ ### Redaction Helpers (returns Any)
86
+
87
+ Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
88
+
89
+ ```python
90
+ redacted_value(value: Any) -> Any
91
+ redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
92
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
93
+ ```
94
+
94
95
  **Use case:** structured protocol payloads (frameworks automatically serialize)
95
96
 
96
97
  **Example:**
@@ -129,23 +130,31 @@ not_found = build_json(
129
130
  )
130
131
  ```
131
132
 
132
- ### CLI/Log Output (returns str)
133
+ ### Output Formatters (returns str)
133
134
 
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.
135
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use the `*_with_options` functions to pass legacy secret names such as `api_key`. YAML and Plain always redact secrets and apply human-readable formatting.
135
136
 
136
137
  ```python
137
138
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
138
139
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
140
+ output_json_with_options(value: Any, redaction_options: RedactionOptions) -> str
139
141
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
142
+ output_yaml_with_options(value: Any, redaction_options: RedactionOptions) -> str
140
143
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
144
+ output_plain_with_options(value: Any, redaction_options: RedactionOptions) -> str
141
145
  ```
142
146
 
143
147
  ```python
144
148
  class RedactionPolicy(enum.Enum):
145
149
  RedactionTraceOnly = "RedactionTraceOnly"
146
150
  RedactionNone = "RedactionNone"
151
+ RedactionStrict = "RedactionStrict"
152
+
153
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
147
154
  ```
148
155
 
156
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. They do not change YAML/Plain suffix stripping.
157
+
149
158
  **Example:**
150
159
  ```python
151
160
  from agent_first_data import *
@@ -178,6 +187,7 @@ print(output_plain(data))
178
187
 
179
188
  ```python
180
189
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
190
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
181
191
  ```
182
192
 
183
193
  Most users don't need this. Output functions automatically protect secrets.
@@ -513,7 +523,7 @@ All formats automatically redact `_secret` fields.
513
523
 
514
524
  ## Repository
515
525
 
516
- This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
526
+ This package is part of the [agent-first-data](https://github.com/agentfirstkit/agent-first-data) repository, which also contains:
517
527
 
518
528
  - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
519
529
  - **`skills/`** — AI coding agent skill for working with AFDATA conventions
@@ -521,7 +531,7 @@ This package is part of the [agent-first-data](https://github.com/cmnspore/agent
521
531
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
522
532
 
523
533
  ```bash
524
- git clone https://github.com/cmnspore/agent-first-data
534
+ git clone https://github.com/agentfirstkit/agent-first-data
525
535
  cd agent-first-data/python
526
536
  python -m pytest
527
537
  ```
@@ -5,11 +5,19 @@ from agent_first_data.format import (
5
5
  build_json_error,
6
6
  build_json,
7
7
  RedactionPolicy,
8
+ RedactionOptions,
8
9
  output_json,
9
10
  output_json_with,
11
+ output_json_with_options,
10
12
  output_yaml,
13
+ output_yaml_with_options,
11
14
  output_plain,
15
+ output_plain_with_options,
12
16
  internal_redact_secrets,
17
+ internal_redact_secrets_with_options,
18
+ redacted_value,
19
+ redacted_value_with,
20
+ redacted_value_with_options,
13
21
  parse_size,
14
22
  )
15
23
 
@@ -36,11 +44,19 @@ __all__ = [
36
44
  "build_json_error",
37
45
  "build_json",
38
46
  "RedactionPolicy",
47
+ "RedactionOptions",
39
48
  "output_json",
40
49
  "output_json_with",
50
+ "output_json_with_options",
41
51
  "output_yaml",
52
+ "output_yaml_with_options",
42
53
  "output_plain",
54
+ "output_plain_with_options",
43
55
  "internal_redact_secrets",
56
+ "internal_redact_secrets_with_options",
57
+ "redacted_value",
58
+ "redacted_value_with",
59
+ "redacted_value_with_options",
44
60
  "parse_size",
45
61
  "AfdataHandler",
46
62
  "AfdataJsonHandler",
@@ -1,15 +1,17 @@
1
1
  """AFDATA output formatting and protocol templates.
2
2
 
3
- 9 public APIs and 1 type: 3 protocol builders + 4 output formatters + 1 redaction + 1 utility + RedactionPolicy.
3
+ 16 public APIs and 2 types: protocol builders, redacted value helpers,
4
+ output formatters, redaction, parse_size, RedactionPolicy, and RedactionOptions.
4
5
  """
5
6
 
6
7
  from __future__ import annotations
7
8
 
8
9
  import json
9
10
  import math
11
+ from dataclasses import dataclass
10
12
  from datetime import datetime, timezone
11
13
  from enum import Enum
12
- from typing import Any
14
+ from typing import Any, Sequence
13
15
 
14
16
 
15
17
  # ═══════════════════════════════════════════
@@ -51,25 +53,48 @@ def build_json(code: str, fields: Any, trace: Any = None) -> dict:
51
53
  class RedactionPolicy(str, Enum):
52
54
  RedactionTraceOnly = "RedactionTraceOnly"
53
55
  RedactionNone = "RedactionNone"
56
+ RedactionStrict = "RedactionStrict"
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class RedactionOptions:
61
+ """Redaction options for legacy secret field names."""
62
+
63
+ policy: RedactionPolicy | None = None
64
+ # Exact field-name matches at any nesting level.
65
+ secret_names: Sequence[str] = ()
54
66
 
55
67
 
56
68
  def output_json(value: Any) -> str:
57
69
  """Format as single-line JSON. Secrets redacted, original keys, raw values."""
58
- v = _sanitize_for_json(value)
59
- _redact_secrets(v)
60
- return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
70
+ return json.dumps(redacted_value(value), ensure_ascii=False, separators=(",", ":"))
61
71
 
62
72
 
63
73
  def output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str:
64
74
  """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=(",", ":"))
75
+ return json.dumps(redacted_value_with(value, redaction_policy), ensure_ascii=False, separators=(",", ":"))
76
+
77
+
78
+ def output_json_with_options(value: Any, redaction_options: RedactionOptions) -> str:
79
+ """Format as single-line JSON with explicit redaction options."""
80
+ return json.dumps(
81
+ redacted_value_with_options(value, redaction_options),
82
+ ensure_ascii=False,
83
+ separators=(",", ":"),
84
+ )
68
85
 
69
86
 
70
87
  def output_yaml(value: Any) -> str:
71
88
  """Format as multi-line YAML. Keys stripped, values formatted, secrets redacted."""
72
- value = _sanitize_for_json(value)
89
+ value = redacted_value(value)
90
+ lines = ["---"]
91
+ _render_yaml_processed(value, 0, lines)
92
+ return "\n".join(lines)
93
+
94
+
95
+ def output_yaml_with_options(value: Any, redaction_options: RedactionOptions) -> str:
96
+ """Format as multi-line YAML with explicit redaction options."""
97
+ value = redacted_value_with_options(value, redaction_options)
73
98
  lines = ["---"]
74
99
  _render_yaml_processed(value, 0, lines)
75
100
  return "\n".join(lines)
@@ -77,16 +102,25 @@ def output_yaml(value: Any) -> str:
77
102
 
78
103
  def output_plain(value: Any) -> str:
79
104
  """Format as single-line logfmt. Keys stripped, values formatted, secrets redacted."""
80
- value = _sanitize_for_json(value)
105
+ value = redacted_value(value)
81
106
  pairs: list[tuple[str, str]] = []
82
107
  _collect_plain_pairs(value, "", pairs)
83
108
  pairs.sort(key=lambda p: p[0].encode("utf-16-be"))
84
109
  parts = []
85
110
  for k, v in pairs:
86
- if " " in v:
87
- parts.append(f'{k}="{v}"')
88
- else:
89
- parts.append(f"{k}={v}")
111
+ parts.append(f"{k}={_quote_logfmt_value(v)}")
112
+ return " ".join(parts)
113
+
114
+
115
+ def output_plain_with_options(value: Any, redaction_options: RedactionOptions) -> str:
116
+ """Format as single-line logfmt with explicit redaction options."""
117
+ value = redacted_value_with_options(value, redaction_options)
118
+ pairs: list[tuple[str, str]] = []
119
+ _collect_plain_pairs(value, "", pairs)
120
+ pairs.sort(key=lambda p: p[0].encode("utf-16-be"))
121
+ parts = []
122
+ for k, v in pairs:
123
+ parts.append(f"{k}={_quote_logfmt_value(v)}")
90
124
  return " ".join(parts)
91
125
 
92
126
 
@@ -100,15 +134,57 @@ def internal_redact_secrets(value: Any) -> None:
100
134
  _redact_secrets(value)
101
135
 
102
136
 
137
+ def internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None:
138
+ """Redact secret fields in-place using explicit redaction options."""
139
+ _apply_redaction_options(value, redaction_options)
140
+
141
+
142
+ def redacted_value(value: Any) -> Any:
143
+ """Return a JSON-safe copy with default _secret redaction applied."""
144
+ v = _sanitize_for_json(value)
145
+ _redact_secrets(v)
146
+ return v
147
+
148
+
149
+ def redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any:
150
+ """Return a JSON-safe copy with an explicit redaction policy applied."""
151
+ v = _sanitize_for_json(value)
152
+ _apply_redaction_policy(v, redaction_policy)
153
+ return v
154
+
155
+
156
+ def redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any:
157
+ """Return a JSON-safe copy with explicit redaction options applied."""
158
+ v = _sanitize_for_json(value)
159
+ _apply_redaction_options(v, redaction_options)
160
+ return v
161
+
162
+
103
163
  def _apply_redaction_policy(value: Any, redaction_policy: RedactionPolicy) -> None:
164
+ _apply_redaction_policy_with_names(value, redaction_policy, frozenset())
165
+
166
+
167
+ def _apply_redaction_options(value: Any, redaction_options: RedactionOptions) -> None:
168
+ secret_names = _secret_name_set(redaction_options.secret_names)
169
+ _apply_redaction_policy_with_names(value, redaction_options.policy, secret_names)
170
+
171
+
172
+ def _apply_redaction_policy_with_names(
173
+ value: Any,
174
+ redaction_policy: RedactionPolicy | None,
175
+ secret_names: frozenset[str],
176
+ ) -> None:
104
177
  if redaction_policy == RedactionPolicy.RedactionTraceOnly:
105
178
  if isinstance(value, dict) and "trace" in value:
106
- _redact_secrets(value["trace"])
179
+ _redact_secrets(value["trace"], secret_names)
107
180
  return
108
181
  if redaction_policy == RedactionPolicy.RedactionNone:
109
182
  return
110
- # Safety fallback for unknown values.
111
- _redact_secrets(value)
183
+ if redaction_policy == RedactionPolicy.RedactionStrict:
184
+ _redact_secrets_strict(value, secret_names)
185
+ return
186
+ # Empty/unknown policy falls back to default full redaction.
187
+ _redact_secrets(value, secret_names)
112
188
 
113
189
 
114
190
  def parse_size(s: str) -> int | None:
@@ -196,19 +272,43 @@ def _sanitize_for_json(value: Any, stack: set[int] | None = None) -> Any:
196
272
  return f"<unsupported:{type(value).__name__}>"
197
273
 
198
274
 
199
- def _redact_secrets(value: Any) -> None:
275
+ def _secret_name_set(secret_names: Sequence[str]) -> frozenset[str]:
276
+ return frozenset(secret_names)
277
+
278
+
279
+ def _key_has_secret_suffix(key: str) -> bool:
280
+ return key.endswith("_secret") or key.endswith("_SECRET")
281
+
282
+
283
+ def _is_secret_key(key: str, secret_names: frozenset[str]) -> bool:
284
+ return _key_has_secret_suffix(key) or key in secret_names
285
+
286
+
287
+ def _redact_secrets(value: Any, secret_names: frozenset[str] = frozenset()) -> None:
200
288
  if isinstance(value, dict):
201
289
  for k in list(value.keys()):
202
- if k.endswith("_secret") or k.endswith("_SECRET"):
290
+ if _is_secret_key(k, secret_names):
203
291
  if isinstance(value[k], (dict, list)):
204
- _redact_secrets(value[k])
292
+ _redact_secrets(value[k], secret_names)
205
293
  else:
206
294
  value[k] = "***"
207
295
  else:
208
- _redact_secrets(value[k])
296
+ _redact_secrets(value[k], secret_names)
297
+ elif isinstance(value, list):
298
+ for item in value:
299
+ _redact_secrets(item, secret_names)
300
+
301
+
302
+ def _redact_secrets_strict(value: Any, secret_names: frozenset[str] = frozenset()) -> None:
303
+ if isinstance(value, dict):
304
+ for k in list(value.keys()):
305
+ if _is_secret_key(k, secret_names):
306
+ value[k] = "***"
307
+ else:
308
+ _redact_secrets_strict(value[k], secret_names)
209
309
  elif isinstance(value, list):
210
310
  for item in value:
211
- _redact_secrets(item)
311
+ _redact_secrets_strict(item, secret_names)
212
312
 
213
313
 
214
314
  # ═══════════════════════════════════════════
@@ -572,3 +672,19 @@ def _plain_scalar(value: Any) -> str:
572
672
  if isinstance(value, (int, float)):
573
673
  return str(value)
574
674
  return str(value)
675
+
676
+
677
+ def _quote_logfmt_value(value: str) -> str:
678
+ if value == "":
679
+ return ""
680
+ needs_quote = any(c.isspace() or c in '="\\"' for c in value)
681
+ if not needs_quote:
682
+ return value
683
+ escaped = (
684
+ value.replace("\\", "\\\\")
685
+ .replace('"', '\\"')
686
+ .replace("\n", "\\n")
687
+ .replace("\r", "\\r")
688
+ .replace("\t", "\\t")
689
+ )
690
+ return f'"{escaped}"'
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-first-data
3
+ Version: 0.8.1
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # agent-first-data
2
11
 
3
12
  **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
@@ -65,7 +74,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
65
74
 
66
75
  ## API Reference
67
76
 
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`)
77
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
69
78
 
70
79
  ### Protocol Builders (returns dict)
71
80
 
@@ -82,6 +91,16 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
82
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
83
92
  ```
84
93
 
94
+ ### Redaction Helpers (returns Any)
95
+
96
+ Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
97
+
98
+ ```python
99
+ redacted_value(value: Any) -> Any
100
+ redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
101
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
102
+ ```
103
+
85
104
  **Use case:** structured protocol payloads (frameworks automatically serialize)
86
105
 
87
106
  **Example:**
@@ -120,23 +139,31 @@ not_found = build_json(
120
139
  )
121
140
  ```
122
141
 
123
- ### CLI/Log Output (returns str)
142
+ ### Output Formatters (returns str)
124
143
 
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.
144
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use the `*_with_options` functions to pass legacy secret names such as `api_key`. YAML and Plain always redact secrets and apply human-readable formatting.
126
145
 
127
146
  ```python
128
147
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
129
148
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
149
+ output_json_with_options(value: Any, redaction_options: RedactionOptions) -> str
130
150
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
151
+ output_yaml_with_options(value: Any, redaction_options: RedactionOptions) -> str
131
152
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
153
+ output_plain_with_options(value: Any, redaction_options: RedactionOptions) -> str
132
154
  ```
133
155
 
134
156
  ```python
135
157
  class RedactionPolicy(enum.Enum):
136
158
  RedactionTraceOnly = "RedactionTraceOnly"
137
159
  RedactionNone = "RedactionNone"
160
+ RedactionStrict = "RedactionStrict"
161
+
162
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
138
163
  ```
139
164
 
165
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. They do not change YAML/Plain suffix stripping.
166
+
140
167
  **Example:**
141
168
  ```python
142
169
  from agent_first_data import *
@@ -169,6 +196,7 @@ print(output_plain(data))
169
196
 
170
197
  ```python
171
198
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
199
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
172
200
  ```
173
201
 
174
202
  Most users don't need this. Output functions automatically protect secrets.
@@ -504,7 +532,7 @@ All formats automatically redact `_secret` fields.
504
532
 
505
533
  ## Repository
506
534
 
507
- This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
535
+ This package is part of the [agent-first-data](https://github.com/agentfirstkit/agent-first-data) repository, which also contains:
508
536
 
509
537
  - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
510
538
  - **`skills/`** — AI coding agent skill for working with AFDATA conventions
@@ -512,7 +540,7 @@ This package is part of the [agent-first-data](https://github.com/cmnspore/agent
512
540
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
513
541
 
514
542
  ```bash
515
- git clone https://github.com/cmnspore/agent-first-data
543
+ git clone https://github.com/agentfirstkit/agent-first-data
516
544
  cd agent-first-data/python
517
545
  python -m pytest
518
546
  ```
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.7.4"
4
- description = "Agent-First Data (AFDATA) suffix-driven output formatting and protocol templates for AI agents"
3
+ version = "0.8.1"
4
+ description = "A naming convention that lets AI agents understand your data without being told what it means."
5
5
  license = "MIT"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.9"
8
8
  dependencies = []
9
9
 
10
10
  [project.urls]
11
- Repository = "https://github.com/cmnspore/agent-first-data"
11
+ Repository = "https://github.com/agentfirstkit/agent-first-data"
12
12
 
13
13
  [build-system]
14
14
  requires = ["setuptools>=68"]
@@ -8,11 +8,19 @@ from agent_first_data import (
8
8
  build_json_error,
9
9
  build_json,
10
10
  RedactionPolicy,
11
+ RedactionOptions,
11
12
  internal_redact_secrets,
13
+ internal_redact_secrets_with_options,
14
+ redacted_value,
15
+ redacted_value_with,
16
+ redacted_value_with_options,
12
17
  output_json,
13
18
  output_json_with,
19
+ output_json_with_options,
14
20
  output_yaml,
21
+ output_yaml_with_options,
15
22
  output_plain,
23
+ output_plain_with_options,
16
24
  )
17
25
  from agent_first_data.format import (
18
26
  _format_bytes_human,
@@ -29,6 +37,12 @@ def _load(name):
29
37
  return json.load(f)
30
38
 
31
39
 
40
+ def _redaction_options(case):
41
+ opts = case.get("options", {})
42
+ policy = RedactionPolicy(opts["policy"]) if "policy" in opts else None
43
+ return RedactionOptions(policy=policy, secret_names=opts.get("secret_names", ()))
44
+
45
+
32
46
  # --- Redact fixtures ---
33
47
 
34
48
 
@@ -40,6 +54,30 @@ def test_redact_fixtures():
40
54
  assert inp == case["expected"], f"[redact/{name}] got {inp}"
41
55
 
42
56
 
57
+ def test_redaction_options_fixtures():
58
+ for case in _load("redaction_options.json"):
59
+ name = case["name"]
60
+ options = _redaction_options(case)
61
+ expected = case["expected"]
62
+
63
+ got = redacted_value_with_options(case["input"], options)
64
+ assert got == expected, f"[redaction_options/{name}] value mismatch: {got}"
65
+
66
+ inp = json.loads(json.dumps(case["input"]))
67
+ internal_redact_secrets_with_options(inp, options)
68
+ assert inp == expected, f"[redaction_options/{name}] in-place mismatch: {inp}"
69
+
70
+ got_json = json.loads(output_json_with_options(case["input"], options))
71
+ assert got_json == expected, f"[redaction_options/{name}] json mismatch: {got_json}"
72
+
73
+ if "expected_yaml" in case:
74
+ got_yaml = output_yaml_with_options(case["input"], options)
75
+ assert got_yaml == case["expected_yaml"], f"[redaction_options/{name}] yaml mismatch: {got_yaml!r}"
76
+ if "expected_plain" in case:
77
+ got_plain = output_plain_with_options(case["input"], options)
78
+ assert got_plain == case["expected_plain"], f"[redaction_options/{name}] plain mismatch: {got_plain!r}"
79
+
80
+
43
81
  # --- Protocol fixtures ---
44
82
 
45
83
 
@@ -156,3 +194,20 @@ def test_output_json_with_none_keeps_secrets():
156
194
  )
157
195
  parsed = json.loads(out)
158
196
  assert parsed["api_key_secret"] == "sk-live-123"
197
+
198
+
199
+ def test_redacted_value_returns_safe_copy():
200
+ inp = {"api_key_secret": "sk-live-123", "nested": {"token_secret": "tok"}}
201
+ got = redacted_value(inp)
202
+ assert got["api_key_secret"] == "***"
203
+ assert got["nested"]["token_secret"] == "***"
204
+ assert inp["api_key_secret"] == "sk-live-123"
205
+
206
+
207
+ def test_redacted_value_with_strict_redacts_secret_subtree():
208
+ inp = {"db_secret": {"password_secret": "real", "host": "localhost"}}
209
+ default = redacted_value(inp)
210
+ strict = redacted_value_with(inp, RedactionPolicy.RedactionStrict)
211
+ assert default["db_secret"]["password_secret"] == "***"
212
+ assert default["db_secret"]["host"] == "localhost"
213
+ assert strict["db_secret"] == "***"