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.
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/PKG-INFO +18 -8
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/README.md +17 -7
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data/__init__.py +4 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data/cli.py +5 -2
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data/format.py +69 -5
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data.egg-info/PKG-INFO +18 -8
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/pyproject.toml +1 -1
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/tests/test_afdata_logging.py +6 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/tests/test_cli.py +10 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/tests/test_format.py +57 -1
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data/afdata_logging.py +0 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/setup.cfg +0 -0
- {agent_first_data-0.4.2 → agent_first_data-0.6.0}/tests/test_no_stderr_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
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: **
|
|
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
|
|
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:**
|
|
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.
|
|
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
|
|
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: **
|
|
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
|
|
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:**
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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: **
|
|
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
|
|
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:**
|
|
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.
|
|
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
|
|
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:
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
{agent_first_data-0.4.2 → agent_first_data-0.6.0}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|