agent-first-data 0.33.0__tar.gz → 0.35.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.
Files changed (19) hide show
  1. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/PKG-INFO +3 -3
  2. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/README.md +2 -2
  3. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/assets/cli-spec-v1.schema.json +4 -0
  4. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/cli.py +22 -4
  5. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/format.py +13 -12
  6. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data.egg-info/PKG-INFO +3 -3
  7. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/pyproject.toml +1 -1
  8. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/tests/test_cli.py +25 -0
  9. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/__init__.py +0 -0
  10. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/assets/cli-help-v2.schema.json +0 -0
  11. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
  12. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data/assets/registry.json +0 -0
  13. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
  14. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
  15. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/agent_first_data.egg-info/top_level.txt +0 -0
  16. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/setup.cfg +0 -0
  17. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/tests/test_format.py +0 -0
  18. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/tests/test_no_stderr_policy.py +0 -0
  19. {agent_first_data-0.33.0 → agent_first_data-0.35.0}/tests/test_requires_python_floor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.33.0
3
+ Version: 0.35.0
4
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 Markdown structure 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
@@ -33,11 +33,11 @@ Useful names use Python casing: `render` (the single value x format x options ->
33
33
  ## Behavior Notes
34
34
 
35
35
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
36
- - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
36
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed, and internal whitespace or a `://` that is not the start of a clean URL redacts the whole field, where whitespace is the explicit Unicode `White_Space` set the spec enumerates, not the language's native predicate.
37
37
  - `OutputOptions.url_names` applies URL treatment to exact legacy field names
38
38
  and recurses through collections. `redact_urls_in_text` is explicit and scans
39
39
  only complete scheme URLs, never arbitrary prose secrets.
40
- - 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.
40
+ - 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; it quotes a value only when it contains ASCII space, tab, newline, carriage return, form feed, vertical tab, NBSP, `=`, `"`, or `\`, and escapes `\`, `"`, newline, carriage return, tab, form feed, and vertical tab inside the quotes.
41
41
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
42
42
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
43
43
  - Do not add a raw version/help pre-parser. Until the Python `CliSpec` compiler lands, applications should keep lifecycle parsing in their own parser rather than claim AFDATA closed-world help compatibility.
@@ -24,11 +24,11 @@ Useful names use Python casing: `render` (the single value x format x options ->
24
24
  ## Behavior Notes
25
25
 
26
26
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
27
- - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
27
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed, and internal whitespace or a `://` that is not the start of a clean URL redacts the whole field, where whitespace is the explicit Unicode `White_Space` set the spec enumerates, not the language's native predicate.
28
28
  - `OutputOptions.url_names` applies URL treatment to exact legacy field names
29
29
  and recurses through collections. `redact_urls_in_text` is explicit and scans
30
30
  only complete scheme URLs, never arbitrary prose secrets.
31
- - 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.
31
+ - 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; it quotes a value only when it contains ASCII space, tab, newline, carriage return, form feed, vertical tab, NBSP, `=`, `"`, or `\`, and escapes `\`, `"`, newline, carriage return, tab, form feed, and vertical tab inside the quotes.
32
32
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
33
33
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
34
34
  - Do not add a raw version/help pre-parser. Until the Python `CliSpec` compiler lands, applications should keep lifecycle parsing in their own parser rather than claim AFDATA closed-world help compatibility.
@@ -100,6 +100,10 @@
100
100
  "about": {
101
101
  "$ref": "#/$defs/nonEmpty"
102
102
  },
103
+ "reference_note": {
104
+ "description": "Prose for the generated CLI reference, rendered after this command's argument table. For the paragraph that belongs to no single argument — where a default is read from, what two options mean together — which would otherwise be hand-written into a file whose header says it is generated.",
105
+ "$ref": "#/$defs/nonEmpty"
106
+ },
103
107
  "arguments": {
104
108
  "type": "array",
105
109
  "items": {
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import enum
6
+ import os
6
7
  import sys
7
8
  from typing import Any, Iterator
8
9
 
@@ -298,10 +299,27 @@ class CliEmitter:
298
299
  # mode (no diagnostic sink) keeps every event on the single writer.
299
300
  use_diagnostic = kind != "result" and self._diagnostic is not None
300
301
  sink = self._diagnostic if use_diagnostic else self._writer
301
- sink.write(render(envelope, self._format, options=self._output_options) + "\n")
302
- flush = getattr(sink, "flush", None)
303
- if flush is not None:
304
- flush()
302
+ try:
303
+ sink.write(render(envelope, self._format, options=self._output_options) + "\n")
304
+ flush = getattr(sink, "flush", None)
305
+ if flush is not None:
306
+ flush()
307
+ except BrokenPipeError:
308
+ # A failed flush leaves buffered bytes for interpreter shutdown to
309
+ # retry. Redirect the closed descriptor before finish returns 0.
310
+ fileno = getattr(sink, "fileno", None)
311
+ if fileno is not None:
312
+ try:
313
+ descriptor = fileno()
314
+ except (OSError, ValueError):
315
+ descriptor = None
316
+ if descriptor is not None:
317
+ null_descriptor = os.open(os.devnull, os.O_WRONLY)
318
+ try:
319
+ os.dup2(null_descriptor, descriptor)
320
+ finally:
321
+ os.close(null_descriptor)
322
+ raise
305
323
  if kind in ("result", "error"):
306
324
  self._terminal_emitted = True
307
325
 
@@ -1273,17 +1273,19 @@ def _redact_url_field_value(s: str, context: _RedactionContext) -> str:
1273
1273
  redacted = _redact_url_in_str(s, context)
1274
1274
  if redacted is not None:
1275
1275
  return redacted
1276
- trimmed = s.strip()
1276
+ trimmed = s.strip("".join(_SPAN_TERMINATOR_WHITESPACE))
1277
1277
  if trimmed != s:
1278
1278
  redacted = _redact_url_in_str(trimmed, context)
1279
1279
  if redacted is not None:
1280
1280
  return redacted
1281
1281
  # Fail closed: a _url value we could not parse as a clean scheme-prefixed
1282
- # URL, yet which carries a credential sigil ('@' userinfo) or internal
1283
- # whitespace, is redacted wholesale rather than passed through. A schemeless
1284
- # connection string like user:pass@host/db has no scheme anchor for the
1285
- # surgical span logic above, so blanket redaction is the safe default.
1286
- if any(c.isspace() for c in s) or "@" in s:
1282
+ # URL, yet which carries a credential sigil ('@' userinfo), internal
1283
+ # whitespace, or a '://' scheme separator, is redacted wholesale rather than
1284
+ # passed through. A schemeless connection string like user:pass@host/db
1285
+ # has no scheme anchor for the surgical span logic above, and a URL behind
1286
+ # a non-whitespace prefix (a BOM, a zero-width space, a label) has one the
1287
+ # span logic cannot reach, so blanket redaction is the safe default.
1288
+ if any(_is_span_terminator_whitespace(c) for c in s) or "@" in s or "://" in s:
1287
1289
  return REDACTED_MARKER
1288
1290
  return s
1289
1291
 
@@ -1840,11 +1842,10 @@ def _yaml_key(key: str) -> str:
1840
1842
  def _is_ambiguous_yaml_key(key: str) -> bool:
1841
1843
  if key.lower() in {"true", "false", "null", "~", ".nan", ".inf", "+.inf", "-.inf"}:
1842
1844
  return True
1843
- try:
1844
- float(key)
1845
- return True
1846
- except ValueError:
1847
- return False
1845
+ unsigned = key[1:] if key.startswith(("+", "-")) else key
1846
+ return unsigned.lower() in {"inf", "infinity", "nan"} or re.fullmatch(
1847
+ r"[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?", key
1848
+ ) is not None
1848
1849
 
1849
1850
 
1850
1851
  def _yaml_scalar(value: Any) -> str:
@@ -1945,7 +1946,7 @@ def _plain_scalar_raw(value: Any) -> str:
1945
1946
  def _quote_logfmt_value(value: str) -> str:
1946
1947
  if value == "":
1947
1948
  return ""
1948
- needs_quote = any(c.isspace() or c in '="\\' for c in value)
1949
+ needs_quote = any(c in ' \t\n\r\f\v\u00a0="\\' for c in value)
1949
1950
  if not needs_quote:
1950
1951
  return value
1951
1952
  escaped = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.33.0
3
+ Version: 0.35.0
4
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 Markdown structure 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
@@ -33,11 +33,11 @@ Useful names use Python casing: `render` (the single value x format x options ->
33
33
  ## Behavior Notes
34
34
 
35
35
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
36
- - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
36
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed, and internal whitespace or a `://` that is not the start of a clean URL redacts the whole field, where whitespace is the explicit Unicode `White_Space` set the spec enumerates, not the language's native predicate.
37
37
  - `OutputOptions.url_names` applies URL treatment to exact legacy field names
38
38
  and recurses through collections. `redact_urls_in_text` is explicit and scans
39
39
  only complete scheme URLs, never arbitrary prose secrets.
40
- - 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.
40
+ - 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; it quotes a value only when it contains ASCII space, tab, newline, carriage return, form feed, vertical tab, NBSP, `=`, `"`, or `\`, and escapes `\`, `"`, newline, carriage return, tab, form feed, and vertical tab inside the quotes.
41
41
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
42
42
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
43
43
  - Do not add a raw version/help pre-parser. Until the Python `CliSpec` compiler lands, applications should keep lifecycle parsing in their own parser rather than claim AFDATA closed-world help compatibility.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.33.0"
3
+ version = "0.35.0"
4
4
  description = "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents."
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -1,5 +1,6 @@
1
1
  """Tests for agent_first_data CLI helpers."""
2
2
  import json
3
+ import sys
3
4
 
4
5
  import pytest
5
6
  from io import StringIO
@@ -500,3 +501,27 @@ def test_cli_render_version_renders_json():
500
501
  assert '"code":"version"' in out
501
502
  assert '"name":"agent-cli"' in out
502
503
  assert '"version":"1.2.3"' in out
504
+
505
+
506
+ @pytest.mark.skipif(
507
+ sys.platform == "win32",
508
+ reason="closed-pipe writes surface as OSError(EINVAL) on Windows, not "
509
+ "BrokenPipeError; the reader-closed contract is asserted on POSIX only",
510
+ )
511
+ def test_real_stdout_closed_pipe_has_clean_process_exit():
512
+ import os
513
+ import subprocess
514
+ import sys
515
+
516
+ reader, writer = os.pipe()
517
+ os.close(reader)
518
+ try:
519
+ child = subprocess.run(
520
+ [sys.executable, "-c", "from agent_first_data import CliEmitter, OutputFormat; "
521
+ "raise SystemExit(CliEmitter.finite(OutputFormat.JSON).finish_result({'ok': True}))"],
522
+ stdout=writer, stderr=subprocess.PIPE, timeout=10,
523
+ )
524
+ finally:
525
+ os.close(writer)
526
+ assert child.returncode == 0, child.stderr
527
+ assert child.stderr == b""