agent-first-data 0.15.0 → 0.16.0
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.
- package/README.md +18 -22
- package/assets/protocol-v1.schema.json +84 -0
- package/assets/registry.json +260 -0
- package/assets/skills/agent-first-data/SKILL.md +64 -0
- package/assets/skills/agent-first-data/references/protocol-v1.schema.json +66 -0
- package/assets/skills/agent-first-data/references/registry.json +260 -0
- package/assets/skills/agent-first-data/references/rules.md +347 -0
- package/dist/cli.d.ts +48 -14
- package/dist/cli.js +134 -36
- package/dist/format.d.ts +152 -55
- package/dist/format.js +652 -144
- package/dist/index.d.ts +2 -5
- package/dist/index.js +2 -5
- package/dist/stream_redirect.js +17 -3
- package/package.json +9 -8
- package/dist/afdata_logging.d.ts +0 -44
- package/dist/afdata_logging.js +0 -90
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agentfirstkit.org/schemas/agent-first-data/registry-v1.schema.json",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"name": "agent-first-data",
|
|
5
|
+
"description": "Machine-readable registry for Agent-First Data suffix semantics.",
|
|
6
|
+
"safe_integer": {
|
|
7
|
+
"minimum": -9007199254740991,
|
|
8
|
+
"maximum": 9007199254740991
|
|
9
|
+
},
|
|
10
|
+
"suffixes": [
|
|
11
|
+
{
|
|
12
|
+
"suffix": "_ns",
|
|
13
|
+
"category": "duration",
|
|
14
|
+
"json_types": ["number"],
|
|
15
|
+
"unit": "nanosecond",
|
|
16
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit"},
|
|
17
|
+
"redaction": {"mode": "none"},
|
|
18
|
+
"constraints": {"number": true}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"suffix": "_us",
|
|
22
|
+
"category": "duration",
|
|
23
|
+
"json_types": ["number"],
|
|
24
|
+
"unit": "microsecond",
|
|
25
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit"},
|
|
26
|
+
"redaction": {"mode": "none"},
|
|
27
|
+
"constraints": {"number": true}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"suffix": "_ms",
|
|
31
|
+
"category": "duration",
|
|
32
|
+
"json_types": ["number"],
|
|
33
|
+
"unit": "millisecond",
|
|
34
|
+
"formatting": {"strip_key": true, "plain_yaml": "duration_ms"},
|
|
35
|
+
"redaction": {"mode": "none"},
|
|
36
|
+
"constraints": {"number": true}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"suffix": "_s",
|
|
40
|
+
"category": "duration",
|
|
41
|
+
"json_types": ["number"],
|
|
42
|
+
"unit": "second",
|
|
43
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit"},
|
|
44
|
+
"redaction": {"mode": "none"},
|
|
45
|
+
"constraints": {"number": true}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"suffix": "_minutes",
|
|
49
|
+
"category": "duration",
|
|
50
|
+
"json_types": ["number"],
|
|
51
|
+
"unit": "minute",
|
|
52
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
|
|
53
|
+
"redaction": {"mode": "none"},
|
|
54
|
+
"constraints": {"number": true}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"suffix": "_hours",
|
|
58
|
+
"category": "duration",
|
|
59
|
+
"json_types": ["number"],
|
|
60
|
+
"unit": "hour",
|
|
61
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
|
|
62
|
+
"redaction": {"mode": "none"},
|
|
63
|
+
"constraints": {"number": true}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"suffix": "_days",
|
|
67
|
+
"category": "duration",
|
|
68
|
+
"json_types": ["number"],
|
|
69
|
+
"unit": "day",
|
|
70
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
|
|
71
|
+
"redaction": {"mode": "none"},
|
|
72
|
+
"constraints": {"number": true}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"suffix": "_epoch_s",
|
|
76
|
+
"category": "timestamp",
|
|
77
|
+
"json_types": ["integer"],
|
|
78
|
+
"unit": "unix_epoch_second",
|
|
79
|
+
"formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
|
|
80
|
+
"redaction": {"mode": "none"},
|
|
81
|
+
"constraints": {"integer": true}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"suffix": "_epoch_ms",
|
|
85
|
+
"category": "timestamp",
|
|
86
|
+
"json_types": ["integer"],
|
|
87
|
+
"unit": "unix_epoch_millisecond",
|
|
88
|
+
"formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
|
|
89
|
+
"redaction": {"mode": "none"},
|
|
90
|
+
"constraints": {"integer": true}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"suffix": "_epoch_ns",
|
|
94
|
+
"category": "timestamp",
|
|
95
|
+
"json_types": ["string"],
|
|
96
|
+
"unit": "unix_epoch_nanosecond",
|
|
97
|
+
"formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
|
|
98
|
+
"redaction": {"mode": "none"},
|
|
99
|
+
"constraints": {"decimal_integer_string": true}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"suffix": "_rfc3339",
|
|
103
|
+
"category": "timestamp",
|
|
104
|
+
"json_types": ["string"],
|
|
105
|
+
"unit": "rfc3339_datetime",
|
|
106
|
+
"formatting": {"strip_key": true, "plain_yaml": "pass_through"},
|
|
107
|
+
"redaction": {"mode": "none"},
|
|
108
|
+
"constraints": {"rfc3339_datetime": true}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"suffix": "_rfc3339_date",
|
|
112
|
+
"category": "strict_string",
|
|
113
|
+
"json_types": ["string"],
|
|
114
|
+
"unit": "rfc3339_full_date",
|
|
115
|
+
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
116
|
+
"redaction": {"mode": "none"},
|
|
117
|
+
"constraints": {"rfc3339_date": true}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"suffix": "_rfc3339_time",
|
|
121
|
+
"category": "strict_string",
|
|
122
|
+
"json_types": ["string"],
|
|
123
|
+
"unit": "rfc3339_partial_time",
|
|
124
|
+
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
125
|
+
"redaction": {"mode": "none"},
|
|
126
|
+
"constraints": {"rfc3339_time": true}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"suffix": "_bcp47",
|
|
130
|
+
"category": "strict_string",
|
|
131
|
+
"json_types": ["string"],
|
|
132
|
+
"unit": "bcp47_language_tag",
|
|
133
|
+
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
134
|
+
"redaction": {"mode": "none"},
|
|
135
|
+
"constraints": {"string": true}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"suffix": "_utc_offset",
|
|
139
|
+
"category": "strict_string",
|
|
140
|
+
"json_types": ["string"],
|
|
141
|
+
"unit": "fixed_utc_offset",
|
|
142
|
+
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
143
|
+
"redaction": {"mode": "none"},
|
|
144
|
+
"constraints": {"utc_offset": true}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"suffix": "_bytes",
|
|
148
|
+
"category": "size",
|
|
149
|
+
"json_types": ["integer"],
|
|
150
|
+
"unit": "byte",
|
|
151
|
+
"formatting": {"strip_key": true, "plain_yaml": "binary_size"},
|
|
152
|
+
"redaction": {"mode": "none"},
|
|
153
|
+
"constraints": {"integer": true, "minimum": 0}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"suffix": "_size",
|
|
157
|
+
"category": "size",
|
|
158
|
+
"json_types": ["string"],
|
|
159
|
+
"unit": "byte_size_input",
|
|
160
|
+
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
161
|
+
"redaction": {"mode": "none"},
|
|
162
|
+
"constraints": {"parse_size_units": ["B", "kB", "MB", "GB", "TB", "KiB", "MiB", "GiB", "TiB"]}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"suffix": "_percent",
|
|
166
|
+
"category": "ratio",
|
|
167
|
+
"json_types": ["number"],
|
|
168
|
+
"unit": "percent",
|
|
169
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_percent"},
|
|
170
|
+
"redaction": {"mode": "none"},
|
|
171
|
+
"constraints": {"number": true}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"suffix": "_msats",
|
|
175
|
+
"category": "currency",
|
|
176
|
+
"json_types": ["integer", "string"],
|
|
177
|
+
"unit": "millisatoshi",
|
|
178
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit"},
|
|
179
|
+
"redaction": {"mode": "none"},
|
|
180
|
+
"constraints": {"integer_or_decimal_integer_string": true}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"suffix": "_sats",
|
|
184
|
+
"category": "currency",
|
|
185
|
+
"json_types": ["integer", "string"],
|
|
186
|
+
"unit": "satoshi",
|
|
187
|
+
"formatting": {"strip_key": true, "plain_yaml": "append_unit"},
|
|
188
|
+
"redaction": {"mode": "none"},
|
|
189
|
+
"constraints": {"integer_or_decimal_integer_string": true}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"suffix": "_usd_cents",
|
|
193
|
+
"category": "currency",
|
|
194
|
+
"json_types": ["integer"],
|
|
195
|
+
"unit": "usd_cent",
|
|
196
|
+
"formatting": {"strip_key": true, "plain_yaml": "currency_minor_unit"},
|
|
197
|
+
"redaction": {"mode": "none"},
|
|
198
|
+
"constraints": {"integer": true}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"suffix": "_eur_cents",
|
|
202
|
+
"category": "currency",
|
|
203
|
+
"json_types": ["integer"],
|
|
204
|
+
"unit": "eur_cent",
|
|
205
|
+
"formatting": {"strip_key": true, "plain_yaml": "currency_minor_unit"},
|
|
206
|
+
"redaction": {"mode": "none"},
|
|
207
|
+
"constraints": {"integer": true}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"suffix": "_jpy",
|
|
211
|
+
"category": "currency",
|
|
212
|
+
"json_types": ["integer"],
|
|
213
|
+
"unit": "jpy",
|
|
214
|
+
"formatting": {"strip_key": true, "plain_yaml": "currency_major_unit"},
|
|
215
|
+
"redaction": {"mode": "none"},
|
|
216
|
+
"constraints": {"integer": true}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"suffix": "_{code}_cents",
|
|
220
|
+
"category": "currency",
|
|
221
|
+
"json_types": ["integer"],
|
|
222
|
+
"unit": "currency_minor_unit",
|
|
223
|
+
"formatting": {"strip_key": true, "plain_yaml": "currency_code_minor_unit"},
|
|
224
|
+
"redaction": {"mode": "none"},
|
|
225
|
+
"constraints": {"integer": true, "code_pattern": "^[A-Za-z]{3,4}$"}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"suffix": "_{code}_micro",
|
|
229
|
+
"category": "currency",
|
|
230
|
+
"json_types": ["integer"],
|
|
231
|
+
"unit": "currency_micro_unit",
|
|
232
|
+
"formatting": {"strip_key": true, "plain_yaml": "currency_code_micro_unit"},
|
|
233
|
+
"redaction": {"mode": "none"},
|
|
234
|
+
"constraints": {"integer": true, "code_pattern": "^[A-Za-z]{3,4}$"}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"suffix": "_secret",
|
|
238
|
+
"category": "sensitive",
|
|
239
|
+
"json_types": ["any"],
|
|
240
|
+
"unit": null,
|
|
241
|
+
"formatting": {"strip_key": true, "plain_yaml": "redacted"},
|
|
242
|
+
"redaction": {"mode": "whole_value", "marker": "***"},
|
|
243
|
+
"constraints": {"free_text_detection": false}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"suffix": "_url",
|
|
247
|
+
"category": "sensitive",
|
|
248
|
+
"json_types": ["string"],
|
|
249
|
+
"unit": "url",
|
|
250
|
+
"formatting": {"strip_key": false, "plain_yaml": "scrub_url"},
|
|
251
|
+
"redaction": {
|
|
252
|
+
"mode": "url_components",
|
|
253
|
+
"userinfo_password": true,
|
|
254
|
+
"query_param_secret_suffix": "_secret",
|
|
255
|
+
"marker": "***"
|
|
256
|
+
},
|
|
257
|
+
"constraints": {"single_url_string": true, "free_text_detection": false}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<!-- Canonical source: skills/agent-first-data/references/rules.md.
|
|
2
|
+
Mirrored byte-for-byte into go/python/typescript assets/ by scripts/sync_offline_assets.py.
|
|
3
|
+
Edit only here; run that script (checked by scripts/test.sh) to propagate. -->
|
|
4
|
+
|
|
5
|
+
# Agent-First Data detailed rules
|
|
6
|
+
|
|
7
|
+
Three parts:
|
|
8
|
+
|
|
9
|
+
1. **Naming** — encode units and semantics in field names so agents parse structured data without external schemas
|
|
10
|
+
2. **Output** — suffix-driven formatting with key stripping, value formatting, and automatic secret redaction
|
|
11
|
+
3. **Protocol** — optional JSONL protocol with `code` (required) and `trace` (recommended)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Part 1: Naming Convention
|
|
16
|
+
|
|
17
|
+
The field name is the schema. Always encode units and semantics in the field name.
|
|
18
|
+
|
|
19
|
+
### Duration
|
|
20
|
+
|
|
21
|
+
| Suffix | Unit | Example |
|
|
22
|
+
|:-------|:-----|:--------|
|
|
23
|
+
| `_ns` | nanoseconds | `gc_pause_ns: 450000` |
|
|
24
|
+
| `_us` | microseconds | `query_us: 830` |
|
|
25
|
+
| `_ms` | milliseconds | `latency_ms: 142` |
|
|
26
|
+
| `_s` | seconds | `dns_ttl_s: 3600` |
|
|
27
|
+
| `_minutes` | minutes | `session_timeout_minutes: 30` |
|
|
28
|
+
| `_hours` | hours | `token_validity_hours: 24` |
|
|
29
|
+
| `_days` | days | `cert_validity_days: 365` |
|
|
30
|
+
|
|
31
|
+
### Timestamps
|
|
32
|
+
|
|
33
|
+
| Suffix | Format | Example |
|
|
34
|
+
|:-------|:-------|:--------|
|
|
35
|
+
| `_epoch_ms` | milliseconds since Unix epoch | `created_at_epoch_ms: 1707868800000` |
|
|
36
|
+
| `_epoch_s` | seconds since Unix epoch | `cached_epoch_s: 1707868800` |
|
|
37
|
+
| `_epoch_ns` | nanoseconds since Unix epoch as a decimal string | `created_epoch_ns: "1707868800000000000"` |
|
|
38
|
+
| `_rfc3339` | RFC 3339 date-time string | `expires_rfc3339: "2026-02-14T10:30:00Z"` |
|
|
39
|
+
|
|
40
|
+
### Strict string formats
|
|
41
|
+
|
|
42
|
+
| Suffix | Format | Example |
|
|
43
|
+
|:-------|:-------|:--------|
|
|
44
|
+
| `_bcp47` | BCP-47 language tag string | `language_bcp47: "zh-CN"` |
|
|
45
|
+
| `_utc_offset` | fixed UTC offset string | `timezone_utc_offset: "+08:00"` |
|
|
46
|
+
| `_rfc3339_date` | RFC 3339 full-date string | `invoice_due_rfc3339_date: "2026-06-13"` |
|
|
47
|
+
| `_rfc3339_time` | RFC 3339 partial-time string | `market_open_rfc3339_time: "09:30:00"` |
|
|
48
|
+
|
|
49
|
+
`*_bcp47` identifies a BCP-47 language tag string. AFDATA does not implement the full BCP-47 registry; tools may validate tags when needed.
|
|
50
|
+
|
|
51
|
+
`*_utc_offset` identifies a fixed UTC offset. Canonical persisted and structured output values are `"UTC"` or `±HH:MM`, with `HH` in `00..23` and `MM` in `00..59`; zero offsets normalize to `"UTC"`. This is not an IANA timezone name, DST rule, or timezone database field.
|
|
52
|
+
|
|
53
|
+
`*_rfc3339_date` identifies an RFC 3339 `full-date` string (`YYYY-MM-DD`). It is a calendar date, not an instant, and has no time, offset, or timezone.
|
|
54
|
+
|
|
55
|
+
`*_rfc3339_time` identifies an RFC 3339 `partial-time` string (`HH:MM:SS[.fraction]`). It is a time-of-day, not an instant, and MUST NOT include `Z`, `±HH:MM`, IANA timezone names, or other timezone annotations. Use `_rfc3339` or `_epoch_*` for instants.
|
|
56
|
+
|
|
57
|
+
Do not create companion timezone-name fields as an AFDATA core pattern. If a future tool needs IANA timezone semantics with a timestamp, prefer a self-contained standard value such as RFC 9557.
|
|
58
|
+
|
|
59
|
+
Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a tool needs auto/default behavior, define it in that tool's own config semantics, not as an AFDATA-wide rule.
|
|
60
|
+
|
|
61
|
+
### Size
|
|
62
|
+
|
|
63
|
+
| Suffix | Example |
|
|
64
|
+
|:-------|:--------|
|
|
65
|
+
| `_bytes` | `payload_bytes: 456789` (non-negative integer) |
|
|
66
|
+
| `_size` | `buffer_size: "10MiB"` or `"10MB"` (config files only, human-readable) |
|
|
67
|
+
|
|
68
|
+
`_size` parsing requires explicit units: `B`, decimal `kB/MB/GB/TB`, or binary `KiB/MiB/GiB/TiB`. Bare numbers and ambiguous `K/M/G/T` are invalid.
|
|
69
|
+
|
|
70
|
+
`parse_size("10MiB")` → `10485760`; `parse_size("10MB")` → `10000000`. Returns null for invalid or negative input.
|
|
71
|
+
|
|
72
|
+
### Percentage
|
|
73
|
+
|
|
74
|
+
| Suffix | Example |
|
|
75
|
+
|:-------|:--------|
|
|
76
|
+
| `_percent` | `cpu_percent: 85` |
|
|
77
|
+
|
|
78
|
+
### Currency
|
|
79
|
+
|
|
80
|
+
Bitcoin:
|
|
81
|
+
|
|
82
|
+
| Suffix | Example |
|
|
83
|
+
|:-------|:--------|
|
|
84
|
+
| `_msats` | `balance_msats: 97900` |
|
|
85
|
+
| `_sats` | `withdrawn_sats: 1234` |
|
|
86
|
+
|
|
87
|
+
Do not use a floating `_btc` suffix. Use integer `_sats` or `_msats` instead; values outside the JSON safe-integer range should be decimal strings.
|
|
88
|
+
|
|
89
|
+
Fiat — `_{iso4217}_cents` for currencies with 1/100 subdivision, `_{iso4217}` for currencies without. Generic `_{code}_cents` matches only 3-4 ASCII letters:
|
|
90
|
+
|
|
91
|
+
| Suffix | Example |
|
|
92
|
+
|:-------|:--------|
|
|
93
|
+
| `_usd_cents` | `price_usd_cents: 999` |
|
|
94
|
+
| `_eur_cents` | `price_eur_cents: 850` |
|
|
95
|
+
| `_jpy` | `price_jpy: 1500` |
|
|
96
|
+
| `_usdt_cents` | `deposit_usdt_cents: 1000` |
|
|
97
|
+
|
|
98
|
+
Sub-cent precision — `_{code}_micro`, integer millionths (10⁻⁶) of the major unit (`cost_usd_micro: 170000` = $0.17). The fiat analog of `_msats`: when cents are too coarse (per-token pricing, metered costs), use a smaller integer unit, never decimal cents. `_{code}_cents` for user-facing amounts, `_{code}_micro` for high-precision internal accounting.
|
|
99
|
+
|
|
100
|
+
### Sensitive
|
|
101
|
+
|
|
102
|
+
| Suffix | Handling | Example |
|
|
103
|
+
|:-------|:---------|:--------|
|
|
104
|
+
| `_secret` | redact the entire value/subtree to `***` | `api_key_secret: "sk-or-v1-abc..."` |
|
|
105
|
+
| `_url` | scrub secrets *inside* the URL value, keep the rest | `callback_url: "https://h/cb?code_secret=..."` |
|
|
106
|
+
|
|
107
|
+
All CLI output formats (JSON, YAML, Plain) automatically redact `_secret` fields. Matching recognizes `_secret` and `_SECRET` only — no mixed case. The entire `_secret` value/subtree becomes `***`, including objects and arrays. For legacy fields that cannot be renamed, configure `OutputOptions.redaction` with `secret_names`/`secretNames` such as `["api_key", "authorization"]`; names match exact field names at any nesting level; no trim, case folding, hyphen/underscore normalization, globs, regex, or substring matching. AFDATA does not define named redaction profiles; use the default, `secret_names`, `RedactionTraceOnly`, or `RedactionNone` deliberately at the serialization boundary. Callers that need schema-preserving YAML/plain rendering can pass `OutputOptions` with the `Raw` output style.
|
|
108
|
+
|
|
109
|
+
`***` means only one thing: AFDATA redacted a sensitive value. Do not use it for serialization failure, truncation, unsupported types, or “maybe secret” guesses.
|
|
110
|
+
|
|
111
|
+
Name URL-valued fields `_url` so the userinfo password and any `_secret`/`secret_names` query parameter inside them are scrubbed automatically (the rest of the URL is preserved; the suffix is not stripped). For a URL inside a free-form message, redact it with `redact_url_secrets` before interpolating — `_url` only fires on whole-URL field values, never on prose. AFDATA does not scan arbitrary free text for secrets.
|
|
112
|
+
|
|
113
|
+
**`_url` scrubs the userinfo password and suffix-named params only — not arbitrary credential params.** Common parameters like `?access_token=`, `?api_key=`, `?code=`, `?sig=` are NOT redacted unless their name ends in `_secret` or is passed in `secret_names`. Rename params you own to the suffix (`?access_token_secret=`); list the rest in `secret_names`. A `_url` value that is not a clean scheme-prefixed URL but carries internal whitespace or an `@` credential sigil (e.g. a schemeless `user:pass@host/db`) is redacted wholesale to `***` (fail-closed).
|
|
114
|
+
|
|
115
|
+
### Environment variables
|
|
116
|
+
|
|
117
|
+
Same suffixes, `UPPER_SNAKE_CASE`:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
DATABASE_URL_SECRET=postgres://user:pass@host/db
|
|
121
|
+
CACHE_TTL_S=3600
|
|
122
|
+
TOKEN_VALIDITY_HOURS=24
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### No suffix needed
|
|
126
|
+
|
|
127
|
+
Fields whose meaning is obvious: `redb_path`, `proof_count`, `search_enabled`, `method`, `domain`, `model`. (URL fields are the exception — use `_url` so embedded secrets are scrubbed.)
|
|
128
|
+
|
|
129
|
+
### Database columns
|
|
130
|
+
|
|
131
|
+
Use suffixes on generic types (`INTEGER`, `BIGINT`, `TEXT`). Native types that carry semantics (`TIMESTAMPTZ`, `INTERVAL`) don't need suffixes.
|
|
132
|
+
|
|
133
|
+
| Column | Type | Suffix? | Why |
|
|
134
|
+
|:-------|:-----|:--------|:----|
|
|
135
|
+
| `created_at` | `TIMESTAMPTZ` | no | type says timestamp |
|
|
136
|
+
| `duration_ms` | `INTEGER` | yes | integer is ambiguous |
|
|
137
|
+
| `api_key_secret` | `TEXT` | yes | enables auto-redaction |
|
|
138
|
+
| `retry_count` | `INTEGER` | no | meaning obvious |
|
|
139
|
+
|
|
140
|
+
ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
|
|
141
|
+
|
|
142
|
+
### Common mistakes
|
|
143
|
+
|
|
144
|
+
| Bad | Good | Why |
|
|
145
|
+
|:----|:-----|:----|
|
|
146
|
+
| `timeout: 30` | `timeout_s: 30` | 30 what? seconds? ms? |
|
|
147
|
+
| `timestamp: 1707868800` | `cached_epoch_s: 1707868800` | what unit? what event? |
|
|
148
|
+
| `size: 456789` | `payload_bytes: 456789` | bytes? KiB? |
|
|
149
|
+
| `price: 999` | `price_usd_cents: 999` | what currency? what unit? |
|
|
150
|
+
| `latency: 142` | `latency_ms: 142` | seconds? milliseconds? |
|
|
151
|
+
| `api_key: "sk-..."` | `api_key_secret: "sk-..."` | won't be auto-redacted |
|
|
152
|
+
| `cpu: 85` | `cpu_percent: 85` | 85 what? |
|
|
153
|
+
| `buffer: "10MiB"` | `buffer_size: "10MiB"` | only `_size` gets parsed |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Part 2: Output Processing
|
|
158
|
+
|
|
159
|
+
Three output formats via `output_json()`, `output_yaml()`, `output_plain()`. Default YAML and Plain apply key stripping + value formatting. Optional `options` (Python/TS keyword arg; Rust/Go `_with_options` variant).
|
|
160
|
+
|
|
161
|
+
### Formats
|
|
162
|
+
|
|
163
|
+
- **JSON** — single-line, original keys, raw values, no sorting (machine-readable), secrets redacted
|
|
164
|
+
- **YAML** — multi-line, formatting suffixes stripped, values formatted, secrets redacted by default
|
|
165
|
+
- **Plain** — single-line logfmt, formatting suffixes stripped, values formatted, secrets redacted by default
|
|
166
|
+
|
|
167
|
+
### Key stripping (YAML and Plain)
|
|
168
|
+
|
|
169
|
+
Remove recognized formatting suffix from key. Longest match first, exact lowercase or uppercase only:
|
|
170
|
+
|
|
171
|
+
1. `_epoch_ms`, `_epoch_s`, `_epoch_ns`
|
|
172
|
+
2. `_usd_cents`, `_eur_cents`, `_{code}_cents`, `_{code}_micro` (`code` is 3-4 ASCII letters)
|
|
173
|
+
3. `_rfc3339`, `_minutes`, `_hours`, `_days`
|
|
174
|
+
4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
|
|
175
|
+
5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
|
|
176
|
+
|
|
177
|
+
`_size`, `_bcp47`, `_utc_offset`, `_rfc3339_date`, and `_rfc3339_time` are NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting). Redaction runs before collision handling, so fallback never restores a secret.
|
|
178
|
+
|
|
179
|
+
### Value formatting (YAML and Plain)
|
|
180
|
+
|
|
181
|
+
- `_ms` with absolute value < 1000 → `{n}ms`; absolute value ≥ 1000 → seconds (`1280` → `1.28s`, `-1500` → `-1.5s`)
|
|
182
|
+
- `_s`, `_ns`, `_us` → append unit (`3600s`, `450000ns`, `830μs`)
|
|
183
|
+
- `_minutes`, `_hours`, `_days` → append unit (`30 minutes`)
|
|
184
|
+
- `_epoch_ms`/`_epoch_s`/decimal-string `_epoch_ns` → RFC 3339 (negative = pre-1970)
|
|
185
|
+
- `_rfc3339` → pass through
|
|
186
|
+
- `_bytes` → human-readable (`456789` → `446.1KiB`); negative and fractional byte values fall through raw
|
|
187
|
+
- `_size` → pass through
|
|
188
|
+
- `_percent` → append `%`
|
|
189
|
+
- `_msats` → `{n}msats`, `_sats` → `{n}sats`
|
|
190
|
+
- `_usd_cents` → `$X.XX`, `_eur_cents` → `€X.XX`, `_jpy` → `¥X,XXX`, `_{code}_cents` → `X.XX CODE`, `_{code}_micro` → `X.XXXXXX CODE` where `code` is 3-4 ASCII letters
|
|
191
|
+
- `_secret` → `***` (the redaction phase already replaced the subtree)
|
|
192
|
+
- `_bcp47`, `_utc_offset`, `_rfc3339_date`, `_rfc3339_time` → pass through unchanged
|
|
193
|
+
|
|
194
|
+
**Type constraints**: `_bytes`/`_epoch_*` require integer. `_usd_cents`/`_eur_cents`/`_jpy`/`_{code}_cents`/`_{code}_micro` require non-negative integer. Duration/Bitcoin/`_percent` accept any number. Wrong type → raw value + original key.
|
|
195
|
+
|
|
196
|
+
### Plain logfmt details
|
|
197
|
+
|
|
198
|
+
- Nested keys use dot notation: `trace.duration=1.28s`
|
|
199
|
+
- Keys and values with ASCII space, tab/newline, VT, FF, NBSP, `=`, `"`, or `\` are quoted/escaped so each record stays one physical line
|
|
200
|
+
- Arrays comma-joined: `fields=email,age`
|
|
201
|
+
- Null → empty value: `RUST_LOG=`
|
|
202
|
+
- Sort by full dot path (JCS / UTF-16 code unit order)
|
|
203
|
+
|
|
204
|
+
### Key ordering
|
|
205
|
+
|
|
206
|
+
YAML and Plain sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785). For ASCII keys this equals byte-order sorting.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Part 3: Protocol Template (Optional)
|
|
211
|
+
|
|
212
|
+
Every protocol event uses `kind` plus one same-named payload:
|
|
213
|
+
|
|
214
|
+
| `kind` | When |
|
|
215
|
+
|:-------|:-----|
|
|
216
|
+
| `"log"` | Diagnostic event; `log.event` may identify startup/request/retry/redirect |
|
|
217
|
+
| `"progress"` | Non-terminal status/progress |
|
|
218
|
+
| `"result"` | Terminal success |
|
|
219
|
+
| `"error"` | Terminal error with non-empty `error.code` and `error.message` |
|
|
220
|
+
|
|
221
|
+
Channel policy:
|
|
222
|
+
- `stdout` is the only protocol/log stream for machine-readable events
|
|
223
|
+
- runtime protocol events MUST NOT be emitted on `stderr`
|
|
224
|
+
- `stderr` is reserved for unrecoverable pre-protocol startup failures only
|
|
225
|
+
|
|
226
|
+
Optional stream redirection:
|
|
227
|
+
- use `--stdout-file <PATH>` to redirect stdout bytes to a file
|
|
228
|
+
- use `--stderr-file <PATH>` to redirect stderr bytes to a file
|
|
229
|
+
- keep `--output` reserved for stdout format selection (`json`, `yaml`, `plain`, and help-specific `markdown`)
|
|
230
|
+
- do not convert stderr diagnostics to JSON; Rust panics, Python tracebacks, and runtime errors stay native stderr bytes
|
|
231
|
+
- treat this as stream destination control, not a second protocol stream and not stream copying
|
|
232
|
+
- do not implement application-level rotation
|
|
233
|
+
|
|
234
|
+
Recommended enforcement:
|
|
235
|
+
- Rust: enable clippy `print_stderr = "deny"` and disallow `std::eprintln` / `std::io::stderr`
|
|
236
|
+
- Go/Python/TypeScript: add source-policy tests that fail if runtime sources reference stderr APIs
|
|
237
|
+
|
|
238
|
+
### Templates
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{"kind":"log","log":{"message":"startup","level":"info","event":"startup","config":{...}},"trace":{}}
|
|
242
|
+
{"kind":"progress","progress":{"message":"syncing","current":3,"total":10},"trace":{"duration_ms":5}}
|
|
243
|
+
{"kind":"result","result":{...},"trace":{"duration_ms":12,"source":"redb"}}
|
|
244
|
+
{"kind":"error","error":{"code":"not_found","message":"user not found","resource":"user","id":123,"retryable":false},"trace":{"duration_ms":8}}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Always include `trace` for execution context: duration, token counts, cost, data source.
|
|
248
|
+
Startup payload fields are tool-defined; `config` is recommended, while `version`/`argv`/`args`/`env` are optional.
|
|
249
|
+
|
|
250
|
+
### Same structure, any transport
|
|
251
|
+
|
|
252
|
+
| Transport | Format |
|
|
253
|
+
|:----------|:-------|
|
|
254
|
+
| CLI stdout | JSONL |
|
|
255
|
+
| REST API | JSON body |
|
|
256
|
+
| MCP tool | JSON |
|
|
257
|
+
| SSE stream | JSONL |
|
|
258
|
+
|
|
259
|
+
All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
|
|
260
|
+
|
|
261
|
+
Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event, complete log fields (`message`, `level`; `code` field MUST NOT appear in log payload), `retryable` on every error, and a non-empty progress `message`. Use strict validation before claiming compliance with all recommendations.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Library Usage
|
|
266
|
+
|
|
267
|
+
Use the local language README for installation and the full overview/spec for API reference. Keep this skill focused on naming, output, protocol, logging, and review rules rather than duplicating import snippets that drift across languages.
|
|
268
|
+
|
|
269
|
+
Required cross-language behavior to rely on:
|
|
270
|
+
|
|
271
|
+
- Output helpers redact before formatting: `output_json()`, `output_yaml()`, `output_plain()` (with optional `options` in Python/TS).
|
|
272
|
+
- Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `output_json()`.
|
|
273
|
+
- Use `redact_url_secrets()` for URLs embedded in log messages or prose before interpolating them into output.
|
|
274
|
+
- Use `cli_parse_output()`, `cli_parse_log_filters()`, `cli_output()`, `build_cli_error()`, and the version helper for CLI tools instead of custom parsing/error envelopes.
|
|
275
|
+
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
276
|
+
- Rust CLIs should call `cli_handle_version_or_continue()` before clap parsing so `--version --output json|yaml|plain` emits a structured `kind:"result"` event with `result.version` instead of clap's plain text. Use `VersionConfig::conventional_default()` so bare `--version` stays human text while explicit `--output` remains structured.
|
|
277
|
+
- Use the protocol reader `decode_protocol_event(text)` to parse and strict-validate a single JSON text line, receiving a typed decoded event.
|
|
278
|
+
|
|
279
|
+
## AFDATA Logging
|
|
280
|
+
|
|
281
|
+
Long-running services and processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) use a logging init function to capture the full process. One-time CLI output uses `json_log()` + `cli_output()` or `CliEmitter`.
|
|
282
|
+
|
|
283
|
+
### Init (Rust only; pick one format per process)
|
|
284
|
+
|
|
285
|
+
| Format | Rust |
|
|
286
|
+
|:-------|:-----|
|
|
287
|
+
| **Unified** | `afdata_tracing::try_init(filter, format, redactor)` |
|
|
288
|
+
|
|
289
|
+
Go, Python, and TypeScript have no built-in logging integration; emit log events via `json_log()` + output helpers or construct them explicitly.
|
|
290
|
+
|
|
291
|
+
Rust requires `cargo add agent-first-data --features tracing`.
|
|
292
|
+
|
|
293
|
+
### Spans (Rust only; add fields to all log events in scope)
|
|
294
|
+
|
|
295
|
+
```rust
|
|
296
|
+
// Rust — tracing spans
|
|
297
|
+
let span = info_span!("request", request_id = %uuid);
|
|
298
|
+
let _guard = span.enter();
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
For Go, Python, and TypeScript: emit span fields explicitly in each log event via `.field()` or equivalent event construction.
|
|
302
|
+
|
|
303
|
+
### Output fields
|
|
304
|
+
|
|
305
|
+
Every log line contains `message`, `level` (debug/info/warn/error), plus span fields and event fields. Log payloads MUST NOT contain a `code` field. Do not use the log level as `code`; `code:"error"` is reserved for terminal protocol errors. Projects that need timestamps add them as extension fields (`timestamp_epoch_ms`) via emitter defaults or per-call `.field()`.
|
|
306
|
+
|
|
307
|
+
Log redaction is **by field name** (the same `_secret`/`_url` rule as all output), applied when the line is emitted. Name the secret field — `info!(api_key_secret = %key)` — rather than logging a whole object by its `Debug`/string rendering, which hides the inner field names from redaction. For structured/nested secret-bearing data, build a value, apply redaction through output formatters or redacted_value(), then emit via the standard formatting helpers — do not pass the struct to a `?`/`%`-rendered log field.
|
|
308
|
+
|
|
309
|
+
## CLI Flags
|
|
310
|
+
|
|
311
|
+
CLI tools that use AFDATA should support output and logging flags:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
--output json|yaml|plain # default is tool-defined (interactive → yaml, scripting/logging → json)
|
|
315
|
+
--log startup,request,progress,retry,redirect
|
|
316
|
+
--verbose # shorthand for all log categories
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- Protocol output (`build_json_*` + `output_*`) follows `--output`
|
|
320
|
+
- Log format follows `--output` or a separate `--log-format` flag if independent control is needed
|
|
321
|
+
- Help scope and format are orthogonal: `--recursive` decides one-level vs recursive, `--output` decides plain/json/yaml/markdown. Human help is one-level `--help` (and scoped `myapp sub --help`); agents/docs use `--help --recursive` and add `--output json|yaml|markdown` for a recursive export. A bare `--recursive` without `--help` falls through to the app's own parser. `markdown` is help-only. Rust: use `cli_handle_help_or_continue()` / `cli_render_help_with_options()` from the `cli-help` feature; wrappers `cli_render_help()` and `cli_render_help_markdown()` remain available for recursive output.
|
|
322
|
+
|
|
323
|
+
## Review Checklist
|
|
324
|
+
|
|
325
|
+
When reviewing code that produces structured output:
|
|
326
|
+
|
|
327
|
+
1. Every numeric field with a unit has the correct suffix (`_ms`, `_bytes`, `_sats`, `_percent`, etc.)
|
|
328
|
+
2. Timestamps use `_epoch_ms` / `_epoch_s` / `_rfc3339`; date-only/time-only strings use `_rfc3339_date` / `_rfc3339_time`
|
|
329
|
+
3. Sensitive values end in `_secret` and are redacted in all output paths
|
|
330
|
+
4. Protocol transport payloads and CLI events use `kind`, the same-named payload (`event[kind]`), and top-level `trace`; tool-defined codes live inside the payload
|
|
331
|
+
5. Config files use the same suffixes as output
|
|
332
|
+
6. No unit-less ambiguous fields (`timeout: 30` — 30 what?)
|
|
333
|
+
|
|
334
|
+
## Skill Evaluation Checklist
|
|
335
|
+
|
|
336
|
+
Use this checklist when the skill is evaluating a change, not just documenting conventions:
|
|
337
|
+
|
|
338
|
+
1. Trigger on any change to structured fields, configs, logs, CLI output, protocol envelopes, MCP/HTTP/SSE payloads, database columns, wire fields, or persisted JSON.
|
|
339
|
+
2. Use `registry.json` for suffix semantics and `protocol-v1.schema.json` for envelope validity. Do not invent substitute suffix rules or shrink the schema to fit a local implementation.
|
|
340
|
+
3. Require secret-bearing values to be named with `_secret` or configured exact secret names before serialization. Do not rely on arbitrary free-text scanning or `Debug`/string-rendered objects.
|
|
341
|
+
4. Run `afdata lint` for JSON/JSONL samples, JSON Schema, MCP schemas, or serialized outputs when available; run `afdata validate` for finite protocol events/streams. If unavailable, report the skipped check.
|
|
342
|
+
5. Before changing an external project's public API, wire format, database schema, or persisted field names, report the compatibility impact and request approval before editing.
|
|
343
|
+
6. Config size values use `_size` suffix (`buffer_size: "10MiB"`, not `buffer: "10MiB"`)
|
|
344
|
+
7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
|
|
345
|
+
8. One-time CLI output uses `json_log()`/`CliEmitter` + output helpers; long-running services use `afdata_tracing::try_init()` (Rust) to capture via tracing; other languages emit log events explicitly via builders
|
|
346
|
+
9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
|
|
347
|
+
10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so clap errors go to stdout as JSONL
|