agent-first-data 0.17.3__tar.gz → 0.18.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.17.3 → agent_first_data-0.18.0}/PKG-INFO +7 -7
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/README.md +5 -5
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/__init__.py +4 -10
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/skills/agent-first-data/SKILL.md +56 -11
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/skills/agent-first-data/references/rules.md +20 -14
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/cli.py +55 -38
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/format.py +55 -79
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/skill.py +3 -3
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data.egg-info/PKG-INFO +7 -7
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/pyproject.toml +2 -2
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/tests/test_cli.py +75 -15
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/tests/test_format.py +99 -50
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/registry.json +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/assets/skills/agent-first-data/references/registry.json +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data/stream_redirect.py +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/setup.cfg +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/tests/test_no_stderr_policy.py +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/tests/test_skill.py +0 -0
- {agent_first_data-0.17.3 → agent_first_data-0.18.0}/tests/test_stream_redirect.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A naming convention that lets AI agents understand your data without being told what it means.
|
|
3
|
+
Version: 0.18.0
|
|
4
|
+
Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
7
7
|
Requires-Python: >=3.9
|
|
@@ -14,7 +14,7 @@ pip install agent-first-data
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
|
-
from agent_first_data import json_result,
|
|
17
|
+
from agent_first_data import json_result, render, OutputFormat
|
|
18
18
|
|
|
19
19
|
value = json_result(
|
|
20
20
|
{
|
|
@@ -24,11 +24,11 @@ value = json_result(
|
|
|
24
24
|
}
|
|
25
25
|
).build()
|
|
26
26
|
|
|
27
|
-
print(
|
|
28
|
-
print(
|
|
27
|
+
print(render(value, OutputFormat.JSON))
|
|
28
|
+
print(render(value, OutputFormat.PLAIN))
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Useful names use Python casing: `
|
|
31
|
+
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
|
|
32
32
|
|
|
33
33
|
Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
|
|
34
34
|
|
|
@@ -42,7 +42,7 @@ init_logging_json("INFO", secret_names=("authorization",))
|
|
|
42
42
|
|
|
43
43
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
44
44
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
|
|
45
|
-
- YAML
|
|
45
|
+
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
|
|
46
46
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
47
47
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
48
48
|
- Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
|
|
@@ -5,7 +5,7 @@ pip install agent-first-data
|
|
|
5
5
|
```
|
|
6
6
|
|
|
7
7
|
```python
|
|
8
|
-
from agent_first_data import json_result,
|
|
8
|
+
from agent_first_data import json_result, render, OutputFormat
|
|
9
9
|
|
|
10
10
|
value = json_result(
|
|
11
11
|
{
|
|
@@ -15,11 +15,11 @@ value = json_result(
|
|
|
15
15
|
}
|
|
16
16
|
).build()
|
|
17
17
|
|
|
18
|
-
print(
|
|
19
|
-
print(
|
|
18
|
+
print(render(value, OutputFormat.JSON))
|
|
19
|
+
print(render(value, OutputFormat.PLAIN))
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Useful names use Python casing: `
|
|
22
|
+
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
|
|
23
23
|
|
|
24
24
|
Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
|
|
25
25
|
|
|
@@ -33,7 +33,7 @@ init_logging_json("INFO", secret_names=("authorization",))
|
|
|
33
33
|
|
|
34
34
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
35
35
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
|
|
36
|
-
- YAML
|
|
36
|
+
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
|
|
37
37
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
38
38
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
39
39
|
- Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
|
|
@@ -21,11 +21,8 @@ from agent_first_data.format import (
|
|
|
21
21
|
DecodedLog,
|
|
22
22
|
decode_protocol_event,
|
|
23
23
|
RedactionPolicy,
|
|
24
|
-
|
|
24
|
+
PlainStyle,
|
|
25
25
|
OutputOptions,
|
|
26
|
-
output_json,
|
|
27
|
-
output_yaml,
|
|
28
|
-
output_plain,
|
|
29
26
|
redacted_value,
|
|
30
27
|
redact_url_secrets,
|
|
31
28
|
normalize_utc_offset,
|
|
@@ -41,7 +38,7 @@ from agent_first_data.cli import (
|
|
|
41
38
|
CliEmitter,
|
|
42
39
|
cli_parse_output,
|
|
43
40
|
cli_parse_log_filters,
|
|
44
|
-
|
|
41
|
+
render,
|
|
45
42
|
build_cli_error,
|
|
46
43
|
build_cli_version,
|
|
47
44
|
cli_render_version,
|
|
@@ -69,11 +66,8 @@ __all__ = [
|
|
|
69
66
|
"DecodedLog",
|
|
70
67
|
"decode_protocol_event",
|
|
71
68
|
"RedactionPolicy",
|
|
72
|
-
"
|
|
69
|
+
"PlainStyle",
|
|
73
70
|
"OutputOptions",
|
|
74
|
-
"output_json",
|
|
75
|
-
"output_yaml",
|
|
76
|
-
"output_plain",
|
|
77
71
|
"redacted_value",
|
|
78
72
|
"redact_url_secrets",
|
|
79
73
|
"normalize_utc_offset",
|
|
@@ -86,7 +80,7 @@ __all__ = [
|
|
|
86
80
|
"CliEmitter",
|
|
87
81
|
"cli_parse_output",
|
|
88
82
|
"cli_parse_log_filters",
|
|
89
|
-
"
|
|
83
|
+
"render",
|
|
90
84
|
"build_cli_error",
|
|
91
85
|
"build_cli_version",
|
|
92
86
|
"cli_render_version",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-first-data
|
|
3
|
-
description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, and
|
|
3
|
+
description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, validation, and reading or safely editing structured JSON/TOML/YAML/dotenv/INI documents by dot-path. Use proactively when writing or reviewing structured data, configs, logs, transport payloads, database/wire fields, CLI output, compatibility-sensitive public and persistent field names, or when reading or editing a config/document file from the shell.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!-- Canonical source: skills/agent-first-data/SKILL.md.
|
|
@@ -47,22 +47,21 @@ go get github.com/agentfirstkit/agent-first-data/go
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```rust
|
|
50
|
-
use agent_first_data::{json_result,
|
|
50
|
+
use agent_first_data::{json_result, render, OutputFormat, OutputOptions};
|
|
51
51
|
use serde_json::json;
|
|
52
52
|
|
|
53
53
|
let event = json_result(json!({
|
|
54
54
|
"latency_ms": 1280,
|
|
55
55
|
"api_key_secret": "sk-live-example",
|
|
56
|
-
})).build()
|
|
57
|
-
let rendered =
|
|
56
|
+
})).build();
|
|
57
|
+
let rendered = render(event.as_value(), OutputFormat::Json, &OutputOptions::default());
|
|
58
58
|
assert!(rendered.contains("\"api_key_secret\":\"***\""));
|
|
59
|
-
# Ok::<(), agent_first_data::BuildError>(())
|
|
60
59
|
```
|
|
61
60
|
|
|
62
61
|
For a finite CLI execution implemented in Rust, create one `CliEmitter`, select
|
|
63
62
|
`OutputFormat` through `cli_parse_output`, enable strict protocol, and emit one
|
|
64
63
|
terminal result or error. Handle version before clap with
|
|
65
|
-
`cli_handle_version_or_continue
|
|
64
|
+
`cli_handle_version_or_continue(raw_args, name, version)`.
|
|
66
65
|
For non-CLI HTTP/MCP/SSE serialization, call `redacted_value()` or
|
|
67
66
|
`redact_url_secrets()` explicitly at the boundary.
|
|
68
67
|
|
|
@@ -94,11 +93,11 @@ working in that runtime.
|
|
|
94
93
|
|
|
95
94
|
## Redaction behavior contract
|
|
96
95
|
|
|
97
|
-
Fields named `_secret` or `_url` and values passing through AFDATA output
|
|
96
|
+
Fields named `_secret` or `_url` and values passing through AFDATA output rendering (`render`, in any format) are automatically redacted: secrets become `***`, URL userinfo passwords and suffix-named query parameters are scrubbed. When serializing outside output formatters (HTTP, MCP, SSE), call `redacted_value()` or `redact_url_secrets()` at the serialization boundary. PII redaction, domain-specific privacy policies (header allowlists, API scope sensitivity), and per-field secret naming are each spore's responsibility; the library provides field-name-based mechanics only.
|
|
98
97
|
|
|
99
98
|
## Logging behavior contract
|
|
100
99
|
|
|
101
|
-
One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `
|
|
100
|
+
One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `render()` applies output formatting. Long-running services or processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) initialize with `afdata_tracing::try_init()` (Rust only) to wire process-wide logging; other languages emit log events explicitly via builders or integrate their own structured logging.
|
|
102
101
|
|
|
103
102
|
## CLI workflow
|
|
104
103
|
|
|
@@ -114,16 +113,62 @@ Use it for deterministic checks:
|
|
|
114
113
|
```bash
|
|
115
114
|
afdata lint payload.json
|
|
116
115
|
afdata validate events.jsonl
|
|
117
|
-
afdata
|
|
116
|
+
afdata render payload.json --output yaml
|
|
118
117
|
afdata skill validate skills/example-skill
|
|
119
118
|
```
|
|
120
119
|
|
|
121
120
|
- Run `afdata lint` for ordinary JSON/JSONL, JSON Schema, or MCP input/output schemas.
|
|
122
121
|
- Run `afdata validate` for AFDATA protocol events or finite event streams.
|
|
123
|
-
- Omit the input path to read stdin. `--output` accepts `json`, `yaml`, or
|
|
124
|
-
`plain
|
|
122
|
+
- Run `afdata render` to apply AFDATA redaction/formatting to arbitrary JSON or JSONL. Omit the input path to read stdin. `--output` accepts `json`, `yaml`, or
|
|
123
|
+
`plain`; JSON and YAML keep original keys/values (structure-preserving), Plain is the lossy human renderer.
|
|
125
124
|
- Treat findings as contract issues unless the owning tool intentionally documents a non-AFDATA field.
|
|
126
125
|
|
|
126
|
+
## Document workflow
|
|
127
|
+
|
|
128
|
+
Use `show`/`get`/`value` to read a JSON, TOML, YAML, dotenv, or INI document
|
|
129
|
+
and `set`/`add`/`remove`/`unset` to edit one in place, instead of `sed`,
|
|
130
|
+
regular expressions, or a generic reserializer — those can reorder keys, drop
|
|
131
|
+
comments, or change quoting/anchors in the rest of the file.
|
|
132
|
+
|
|
133
|
+
- `show` returns the whole document as one AFDATA record; use it for batch
|
|
134
|
+
inspection, and pass every known sensitive legacy field name with
|
|
135
|
+
`--secret-name FIELD` (an exact field name, not a dot-path).
|
|
136
|
+
- `get <KEY>` returns one dot-path value as an AFDATA record. A secret-named
|
|
137
|
+
leaf (`_secret`/`_SECRET`, or an exact `--secret-name` match) is still
|
|
138
|
+
redacted to `"***"` even though the caller targeted it directly — use
|
|
139
|
+
`value --reveal-secret` when the real value is genuinely needed.
|
|
140
|
+
- `value <KEY>` writes only the raw scalar to stdout, with no AFDATA envelope
|
|
141
|
+
and no forced trailing newline, for direct use in shell substitution:
|
|
142
|
+
`value=$(afdata value server.host config.toml)` (KEY then FILE). Only
|
|
143
|
+
scalars are supported; an array or object errors with `path <KEY> is not a
|
|
144
|
+
scalar` (exit 1) — use `get`/`show` for a subtree. A secret-named leaf errors unless
|
|
145
|
+
`--reveal-secret` is passed — there is no default bypass for a targeted read.
|
|
146
|
+
- Read commands (`show`/`get`/`value`) take a positional FILE or fall back to
|
|
147
|
+
piped stdin, defaulting to JSON and rejecting an interactive TTY rather than
|
|
148
|
+
blocking. Mutation commands (`set`/`add`/`remove`/`unset`) always require
|
|
149
|
+
`--input-file PATH` and never read stdin.
|
|
150
|
+
- Escape a literal dot as `\.` and a literal backslash as `\\` in a dot-path;
|
|
151
|
+
every command shares this grammar, so an unrecognized escape or empty
|
|
152
|
+
segment is always an error, never a guess.
|
|
153
|
+
- `add`/`remove` operate on a keyed list — an array of objects addressed by a
|
|
154
|
+
slug — and always require an explicit `--slug-field FIELD`; the CLI never
|
|
155
|
+
infers an identity field. Keyed editing is implemented for JSON and YAML
|
|
156
|
+
only; TOML, dotenv, and INI return a structured "not implemented" error, so
|
|
157
|
+
edit their list entries by writing the whole value another way.
|
|
158
|
+
- For `set`, choose exactly one secret source instead of a positional value
|
|
159
|
+
when the value must not land in shell history or `ps`: `--value-secret
|
|
160
|
+
VALUE` (convenient, but visible to argv/process listings),
|
|
161
|
+
`--value-secret-stdin` (pipe), `--value-secret-prompt` (human, terminal echo
|
|
162
|
+
off), or `--value-secret-fd FD` (Unix automation that needs stdin for
|
|
163
|
+
something else).
|
|
164
|
+
- Mutations preserve the rest of the source document — comments, key order,
|
|
165
|
+
unrelated formatting — and refuse to write through a symlink or (on Unix) a
|
|
166
|
+
multi-link hardlink target. Treat a failed mutation as leaving the original
|
|
167
|
+
file untouched rather than assuming a partial write, and reread before
|
|
168
|
+
retrying. A format backend that isn't compiled in, or input outside the
|
|
169
|
+
documented dotenv/INI dialect, is a structured error to report, not a cue to
|
|
170
|
+
hand-edit the file.
|
|
171
|
+
|
|
127
172
|
## Review checklist
|
|
128
173
|
|
|
129
174
|
1. Numeric fields with units carry the right suffix and safe integer/string representation.
|
|
@@ -107,7 +107,7 @@ Sub-cent precision — `_{code}_micro`, integer millionths (10⁻⁶) of the maj
|
|
|
107
107
|
| `_secret` | redact the entire value/subtree to `***` | `api_key_secret: "sk-or-v1-abc..."` |
|
|
108
108
|
| `_url` | scrub secrets *inside* the URL value, keep the rest | `callback_url: "https://h/cb?code_secret=..."` |
|
|
109
109
|
|
|
110
|
-
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
|
|
110
|
+
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 policy `All`, `secret_names`, `TraceOnly`, or `Off` deliberately at the serialization boundary. YAML is always schema-preserving, like JSON. Callers that need schema-preserving Plain rendering can pass `OutputOptions` with the `Raw` output style.
|
|
111
111
|
|
|
112
112
|
`***` means only one thing: AFDATA redacted a sensitive value. Do not use it for serialization failure, truncation, unsupported types, or “maybe secret” guesses.
|
|
113
113
|
|
|
@@ -158,15 +158,17 @@ ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
|
|
|
158
158
|
|
|
159
159
|
## Part 2: Output Processing
|
|
160
160
|
|
|
161
|
-
Three output formats via `
|
|
161
|
+
Three output formats via one entry point `render(value, format, options)` (`format` = json/yaml/plain). JSON and YAML are both **structure-preserving**: original keys, scalar types, and numeric semantics survive after redaction. Plain is the one **lossy/human** renderer — it strips formatting suffixes and reformats values for scanning. `options` is an optional keyword arg in Python/TS; Rust/Go pass `OutputOptions` explicitly.
|
|
162
162
|
|
|
163
163
|
### Formats
|
|
164
164
|
|
|
165
165
|
- **JSON** — single-line, original keys, raw values, no sorting (machine-readable), secrets redacted
|
|
166
|
-
- **YAML** — multi-line,
|
|
166
|
+
- **YAML** — multi-line, original keys, raw values (same semantics as JSON), keys sorted, secrets redacted by default
|
|
167
167
|
- **Plain** — single-line logfmt, formatting suffixes stripped, values formatted, secrets redacted by default
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
Rust gates YAML output behind the `yaml` Cargo feature (on by default, along with the rest of the default feature set). With `default-features = false` and `yaml` not re-enabled, `OutputFormat::Yaml` does not exist and `--output yaml` is rejected with a feature-requirement error instead of silently falling back to another format.
|
|
170
|
+
|
|
171
|
+
### Key stripping (Plain only)
|
|
170
172
|
|
|
171
173
|
Remove recognized formatting suffix from key. Longest match first, exact lowercase or uppercase only:
|
|
172
174
|
|
|
@@ -176,9 +178,9 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
|
|
|
176
178
|
4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
|
|
177
179
|
5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
|
|
178
180
|
|
|
179
|
-
`_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.
|
|
181
|
+
`_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. JSON and YAML never strip suffixes — every key renders exactly as written.
|
|
180
182
|
|
|
181
|
-
### Value formatting (
|
|
183
|
+
### Value formatting (Plain only)
|
|
182
184
|
|
|
183
185
|
- `_ms` with absolute value < 1000 → `{n}ms`; absolute value ≥ 1000 → seconds (`1280` → `1.28s`, `-1500` → `-1.5s`)
|
|
184
186
|
- `_s`, `_ns`, `_us` → append unit (`3600s`, `450000ns`, `830μs`)
|
|
@@ -192,7 +194,7 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
|
|
|
192
194
|
- `_secret` → `***` (the redaction phase already replaced the subtree)
|
|
193
195
|
- `_bcp47`, `_utc_offset`, `_rfc3339_date`, `_rfc3339_time` → pass through unchanged
|
|
194
196
|
|
|
195
|
-
**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.
|
|
197
|
+
**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. JSON and YAML never format values — a value renders exactly as its JSON type (numbers stay numbers, strings stay quoted strings, no unit/date/currency formatting).
|
|
196
198
|
|
|
197
199
|
### Plain logfmt details
|
|
198
200
|
|
|
@@ -204,7 +206,11 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
|
|
|
204
206
|
|
|
205
207
|
### Key ordering
|
|
206
208
|
|
|
207
|
-
YAML
|
|
209
|
+
YAML sorts keys by UTF-16 code unit order (JCS, RFC 8785) without stripping suffixes. Plain sorts keys the same way, but after stripping. For ASCII keys this equals byte-order sorting.
|
|
210
|
+
|
|
211
|
+
### `PlainStyle` (Plain only)
|
|
212
|
+
|
|
213
|
+
`PlainStyle` (`Readable`/`Raw`) only affects Plain: `Readable` (default) strips suffixes and formats values; `Raw` keeps original keys/values (still redacted). YAML ignores `PlainStyle` entirely — it is always structure-preserving, matching JSON.
|
|
208
214
|
|
|
209
215
|
---
|
|
210
216
|
|
|
@@ -269,17 +275,17 @@ Use the local language README for installation and the full overview/spec for AP
|
|
|
269
275
|
|
|
270
276
|
Required cross-language behavior to rely on:
|
|
271
277
|
|
|
272
|
-
-
|
|
273
|
-
- Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `
|
|
278
|
+
- The single output entry point `render(value, format, options)` redacts before formatting (with optional `options` in Python/TS).
|
|
279
|
+
- Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `render()`.
|
|
274
280
|
- Use `redact_url_secrets()` for URLs embedded in log messages or prose before interpolating them into output.
|
|
275
|
-
- Use `cli_parse_output()`, `cli_parse_log_filters()`, `
|
|
281
|
+
- Use `cli_parse_output()`, `cli_parse_log_filters()`, `render()`, `build_cli_error()`, and the version helper for CLI tools instead of custom parsing/error envelopes.
|
|
276
282
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
277
|
-
- 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.
|
|
283
|
+
- Rust CLIs should call `cli_handle_version_or_continue(raw_args, name, version)` before clap parsing so `--version --output json|yaml|plain` emits a structured `kind:"result"` event with `result.code == "version"` and `result.version` instead of clap's plain text. Bare `--version` always stays human `<name> <version>` text; explicit `--output` is always structured — there is no configuration.
|
|
278
284
|
- Use the protocol reader `decode_protocol_event(text)` to parse and strict-validate a single JSON text line, receiving a typed decoded event.
|
|
279
285
|
|
|
280
286
|
## AFDATA Logging
|
|
281
287
|
|
|
282
|
-
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()` + `
|
|
288
|
+
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()` + `render()` or `CliEmitter`.
|
|
283
289
|
|
|
284
290
|
### Init (Rust only; pick one format per process)
|
|
285
291
|
|
|
@@ -312,7 +318,7 @@ Log redaction is **by field name** (the same `_secret`/`_url` rule as all output
|
|
|
312
318
|
CLI tools that use AFDATA should support output and logging flags:
|
|
313
319
|
|
|
314
320
|
```
|
|
315
|
-
--output json|yaml|plain # default is tool-defined (
|
|
321
|
+
--output json|yaml|plain # default is tool-defined (structured/scripting → json or yaml, human scanning → plain)
|
|
316
322
|
--log startup,request,progress,retry,redirect
|
|
317
323
|
--verbose # shorthand for all log categories
|
|
318
324
|
```
|
|
@@ -13,9 +13,9 @@ from agent_first_data.format import (
|
|
|
13
13
|
json_log,
|
|
14
14
|
json_progress,
|
|
15
15
|
json_result,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
_format_json,
|
|
17
|
+
_format_yaml,
|
|
18
|
+
_format_plain,
|
|
19
19
|
validate_protocol_event,
|
|
20
20
|
)
|
|
21
21
|
|
|
@@ -38,13 +38,15 @@ class LogFilters:
|
|
|
38
38
|
def enabled(self, event: str) -> bool:
|
|
39
39
|
"""Check if event should be logged.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Otherwise returns True
|
|
41
|
+
An empty filter list returns False (filtering is opt-in). The single
|
|
42
|
+
wildcard word 'all' returns True ('*' is not special — one wildcard
|
|
43
|
+
spelling, not two). Otherwise returns True iff event (lowercased) starts
|
|
44
|
+
with any filter (prefix match); a mistyped filter simply matches nothing
|
|
45
|
+
and silently emits no output.
|
|
44
46
|
"""
|
|
45
47
|
if not self._filters:
|
|
46
48
|
return False
|
|
47
|
-
if
|
|
49
|
+
if "all" in self._filters:
|
|
48
50
|
return True
|
|
49
51
|
event_lower = event.lower()
|
|
50
52
|
return any(event_lower.startswith(f) for f in self._filters)
|
|
@@ -104,22 +106,22 @@ def cli_parse_log_filters(entries: list[str]) -> LogFilters:
|
|
|
104
106
|
return LogFilters(out)
|
|
105
107
|
|
|
106
108
|
|
|
107
|
-
def
|
|
108
|
-
"""
|
|
109
|
+
def render(value: Any, format: OutputFormat, *, options: OutputOptions | None = None) -> str:
|
|
110
|
+
"""Render a value as JSON, YAML, or plain (logfmt) text for OutputFormat.
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
JSON ignores
|
|
112
|
+
The single public render entry point: value x format x options -> str.
|
|
113
|
+
JSON ignores PlainStyle and preserves original keys and values after redaction.
|
|
112
114
|
|
|
113
115
|
>>> import json
|
|
114
116
|
>>> v = {"code": "ok"}
|
|
115
|
-
>>>
|
|
117
|
+
>>> render(v, OutputFormat.JSON).startswith('{"code"')
|
|
116
118
|
True
|
|
117
119
|
"""
|
|
118
120
|
if format is OutputFormat.YAML:
|
|
119
|
-
return
|
|
121
|
+
return _format_yaml(value, options=options)
|
|
120
122
|
if format is OutputFormat.PLAIN:
|
|
121
|
-
return
|
|
122
|
-
return
|
|
123
|
+
return _format_plain(value, options=options)
|
|
124
|
+
return _format_json(value, options=options)
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
class CliEmitter:
|
|
@@ -175,7 +177,7 @@ class CliEmitter:
|
|
|
175
177
|
merged_log.update(log_payload)
|
|
176
178
|
envelope["log"] = merged_log
|
|
177
179
|
|
|
178
|
-
self._writer.write(
|
|
180
|
+
self._writer.write(render(envelope, self._format, options=self._output_options) + "\n")
|
|
179
181
|
flush = getattr(self._writer, "flush", None)
|
|
180
182
|
if flush is not None:
|
|
181
183
|
flush()
|
|
@@ -219,8 +221,13 @@ class CliEmitter:
|
|
|
219
221
|
|
|
220
222
|
|
|
221
223
|
def build_cli_version(version: str) -> dict:
|
|
222
|
-
"""Build a standard CLI version
|
|
223
|
-
|
|
224
|
+
"""Build a standard CLI version result event.
|
|
225
|
+
|
|
226
|
+
The result payload always carries ``code: "version"`` alongside
|
|
227
|
+
``version``, so structured consumers can dispatch on ``code`` the same
|
|
228
|
+
way they would for any other result shape.
|
|
229
|
+
"""
|
|
230
|
+
return json_result({"code": "version", "version": version}).build().to_dict()
|
|
224
231
|
|
|
225
232
|
|
|
226
233
|
def cli_render_version(
|
|
@@ -234,21 +241,24 @@ def cli_render_version(
|
|
|
234
241
|
conventional "<name> <version>" text.
|
|
235
242
|
"""
|
|
236
243
|
rendered = (
|
|
237
|
-
f"{name} {version}" if format is None else
|
|
244
|
+
f"{name} {version}" if format is None else render(build_cli_version(version), format)
|
|
238
245
|
)
|
|
239
246
|
return rendered.rstrip("\n") + "\n"
|
|
240
247
|
|
|
241
248
|
|
|
242
|
-
def cli_handle_version_or_continue(
|
|
243
|
-
raw_args: list[str],
|
|
244
|
-
name: str,
|
|
245
|
-
version: str,
|
|
246
|
-
default_output: OutputFormat | None = None,
|
|
247
|
-
output_flag: str = "--output",
|
|
248
|
-
allow_output_format: bool = True,
|
|
249
|
-
) -> str | None:
|
|
249
|
+
def cli_handle_version_or_continue(raw_args: list[str], name: str, version: str) -> str | None:
|
|
250
250
|
"""Render version output if --version/-V is present; otherwise return None.
|
|
251
251
|
|
|
252
|
+
One blessed behavior, no configurable knobs: a bare version request
|
|
253
|
+
(``--version``/``-V`` with no output flag) always renders conventional
|
|
254
|
+
``"<name> <version>\\n"`` text; adding ``--json`` or ``--output
|
|
255
|
+
<json|yaml|plain>`` selects the structured AFDATA rendering instead.
|
|
256
|
+
|
|
257
|
+
Only a top-level version request is recognized: scanning stops at the first
|
|
258
|
+
positional argument (the subcommand), so ``tool sub --version <value>``
|
|
259
|
+
leaves ``--version`` for the subcommand's parser rather than printing the
|
|
260
|
+
tool version.
|
|
261
|
+
|
|
252
262
|
Raises ValueError for malformed version requests, for example
|
|
253
263
|
``--version --output xml``. The caller should convert that to a CLI error
|
|
254
264
|
with ``build_cli_error``.
|
|
@@ -262,11 +272,16 @@ def cli_handle_version_or_continue(
|
|
|
262
272
|
arg = raw_args[i]
|
|
263
273
|
if arg == "--":
|
|
264
274
|
break
|
|
275
|
+
# The first positional argument marks the subcommand boundary. Past it,
|
|
276
|
+
# --version and -V belong to the subcommand's own parser, matching
|
|
277
|
+
# git/cargo/clap: this pre-parser only owns a top-level version request.
|
|
278
|
+
if not arg.startswith("-"):
|
|
279
|
+
break
|
|
265
280
|
if arg in ("--version", "-V"):
|
|
266
281
|
version_requested = True
|
|
267
282
|
i += 1
|
|
268
283
|
continue
|
|
269
|
-
if
|
|
284
|
+
if arg == "--json":
|
|
270
285
|
if output_format is not None and output_format is not OutputFormat.JSON:
|
|
271
286
|
output_error = ValueError(
|
|
272
287
|
"conflicting output formats: --json conflicts with previous output format"
|
|
@@ -275,9 +290,9 @@ def cli_handle_version_or_continue(
|
|
|
275
290
|
output_format = OutputFormat.JSON
|
|
276
291
|
i += 1
|
|
277
292
|
continue
|
|
278
|
-
if
|
|
293
|
+
if arg == "--output" or arg.startswith("--output="):
|
|
279
294
|
value: str | None
|
|
280
|
-
if arg.startswith(
|
|
295
|
+
if arg.startswith("--output="):
|
|
281
296
|
value = arg.split("=", 1)[1]
|
|
282
297
|
step = 1
|
|
283
298
|
elif i + 1 < len(raw_args) and not raw_args[i + 1].startswith("-"):
|
|
@@ -288,14 +303,14 @@ def cli_handle_version_or_continue(
|
|
|
288
303
|
step = 1
|
|
289
304
|
if value is None:
|
|
290
305
|
output_error = ValueError(
|
|
291
|
-
|
|
306
|
+
"missing value for --output: expected json, yaml, or plain"
|
|
292
307
|
)
|
|
293
308
|
else:
|
|
294
309
|
try:
|
|
295
310
|
parsed_output = cli_parse_output(value)
|
|
296
311
|
if output_format is not None and output_format is not parsed_output:
|
|
297
312
|
output_error = ValueError(
|
|
298
|
-
f"conflicting output formats:
|
|
313
|
+
f"conflicting output formats: --output {value} conflicts with previous output format"
|
|
299
314
|
)
|
|
300
315
|
else:
|
|
301
316
|
output_format = parsed_output
|
|
@@ -309,21 +324,23 @@ def cli_handle_version_or_continue(
|
|
|
309
324
|
return None
|
|
310
325
|
if output_error is not None:
|
|
311
326
|
raise output_error
|
|
312
|
-
return cli_render_version(
|
|
313
|
-
name,
|
|
314
|
-
version,
|
|
315
|
-
output_format if allow_output_format and output_format is not None else default_output,
|
|
316
|
-
)
|
|
327
|
+
return cli_render_version(name, version, output_format)
|
|
317
328
|
|
|
318
329
|
|
|
319
330
|
def build_cli_error(message: str, hint: str | None = None) -> dict | Event:
|
|
320
331
|
"""Build a standard CLI parse error event.
|
|
321
332
|
|
|
322
333
|
Use when argument parsing fails or a flag value is invalid.
|
|
323
|
-
Print with
|
|
334
|
+
Print with render(..., OutputFormat.JSON) and exit with code 2.
|
|
335
|
+
|
|
336
|
+
Always returns a strict-valid ``kind: "error"`` event with code
|
|
337
|
+
``"cli_error"``. Never raises: an empty ``message`` is replaced with a
|
|
338
|
+
generic placeholder so the returned event stays strict-valid.
|
|
324
339
|
|
|
325
340
|
Returns the event envelope as a dict.
|
|
326
341
|
"""
|
|
342
|
+
if not message:
|
|
343
|
+
message = "unspecified error"
|
|
327
344
|
event = json_error("cli_error", message)
|
|
328
345
|
if hint is not None:
|
|
329
346
|
event = event.hint(hint)
|