agent-first-data 0.34.0__tar.gz → 0.35.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/PKG-INFO +3 -3
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/README.md +2 -2
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/cli.py +22 -4
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/format.py +87 -26
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/PKG-INFO +3 -3
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/pyproject.toml +1 -1
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/tests/test_cli.py +25 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/__init__.py +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-help-v2.schema.json +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-spec-v1.schema.json +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/protocol-v1.schema.json +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/registry.json +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/setup.cfg +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/tests/test_format.py +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/tests/test_no_stderr_policy.py +0 -0
- {agent_first_data-0.34.0 → agent_first_data-0.35.1}/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.
|
|
3
|
+
Version: 0.35.1
|
|
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.
|
|
@@ -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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
|
@@ -1117,7 +1117,7 @@ def _redact_url_value(value: Any, context: _RedactionContext, depth: int) -> Any
|
|
|
1117
1117
|
# ═══════════════════════════════════════════
|
|
1118
1118
|
|
|
1119
1119
|
|
|
1120
|
-
def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
1120
|
+
def _redact_url_in_str(s: str, context: _RedactionContext, depth: int = 0) -> str | None:
|
|
1121
1121
|
"""Redact secret components of a single URL string.
|
|
1122
1122
|
|
|
1123
1123
|
Returns the redacted string when ``s`` is a processable URL, or None when it
|
|
@@ -1127,6 +1127,9 @@ def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
|
1127
1127
|
# Fast path + precondition: a single, whitespace-free, scheme-prefixed URL.
|
|
1128
1128
|
if "://" not in s or not _is_single_url(s):
|
|
1129
1129
|
return None
|
|
1130
|
+
# Bound recursive redirect inspection, failing closed before stack growth.
|
|
1131
|
+
if depth >= 16:
|
|
1132
|
+
return REDACTED_MARKER
|
|
1130
1133
|
scheme_sep = s.find("://")
|
|
1131
1134
|
scheme = s[:scheme_sep]
|
|
1132
1135
|
rest = s[scheme_sep + 3 :]
|
|
@@ -1137,9 +1140,29 @@ def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
|
1137
1140
|
if c in "/?#":
|
|
1138
1141
|
auth_end = i
|
|
1139
1142
|
break
|
|
1143
|
+
# An unencoded '?' or '#' inside a password ends that authority early and
|
|
1144
|
+
# leaves the rest of the password in the query or fragment. When a ':'
|
|
1145
|
+
# precedes that early end and an '@' follows it before the first '/', the
|
|
1146
|
+
# userinfo runs to that '@' — how libpq reads such a connection string.
|
|
1147
|
+
first_slash = rest.find("/")
|
|
1148
|
+
if first_slash == -1:
|
|
1149
|
+
first_slash = len(rest)
|
|
1150
|
+
at = rest.rfind("@", 0, first_slash)
|
|
1151
|
+
if ":" in rest[:auth_end] and at > auth_end:
|
|
1152
|
+
auth_end = len(rest)
|
|
1153
|
+
for i in range(at, len(rest)):
|
|
1154
|
+
if rest[i] in "/?#":
|
|
1155
|
+
auth_end = i
|
|
1156
|
+
break
|
|
1140
1157
|
authority = rest[:auth_end]
|
|
1141
1158
|
remainder = rest[auth_end:]
|
|
1142
1159
|
|
|
1160
|
+
# An unencoded '/' inside a password ends the authority at a "port" that
|
|
1161
|
+
# is not a number, with the '@' somewhere after it. Where the password
|
|
1162
|
+
# ends cannot be known, so the whole value is replaced.
|
|
1163
|
+
if "@" not in authority and _has_non_numeric_port(authority) and "@" in remainder:
|
|
1164
|
+
return REDACTED_MARKER
|
|
1165
|
+
|
|
1143
1166
|
new_authority = _redact_userinfo_password(authority)
|
|
1144
1167
|
|
|
1145
1168
|
# `remainder` is `path[?query][#fragment]`; '#' ends the query, so split the
|
|
@@ -1147,7 +1170,7 @@ def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
|
1147
1170
|
before_fragment, hash_sep, fragment = remainder.partition("#")
|
|
1148
1171
|
path, question_sep, query = before_fragment.partition("?")
|
|
1149
1172
|
if question_sep:
|
|
1150
|
-
new_before_fragment = f"{path}?{_redact_query(query, context)}"
|
|
1173
|
+
new_before_fragment = f"{path}?{_redact_query(query, context, depth)}"
|
|
1151
1174
|
else:
|
|
1152
1175
|
new_before_fragment = before_fragment
|
|
1153
1176
|
# A fragment gets the same treatment as the query: `k=v&k=v` after the '#'
|
|
@@ -1155,7 +1178,19 @@ def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
|
1155
1178
|
# secret-named fragment parameter must not survive where the identically
|
|
1156
1179
|
# named query parameter would not. A fragment that is not in that shape has
|
|
1157
1180
|
# no '=' in its segments and passes through byte-for-byte.
|
|
1158
|
-
|
|
1181
|
+
# A hash route (`#/cb?k=v`) carries its parameters after its first '?',
|
|
1182
|
+
# while a form reader keeps that '?' inside a value; redact both readings:
|
|
1183
|
+
# the fragment as parameters first, so a secret value absorbs its '?',
|
|
1184
|
+
# then whatever follows the first '?' that survives.
|
|
1185
|
+
if not hash_sep:
|
|
1186
|
+
new_fragment = ""
|
|
1187
|
+
else:
|
|
1188
|
+
params = _redact_query(fragment, context, depth)
|
|
1189
|
+
route, route_sep, route_query = params.partition("?")
|
|
1190
|
+
if route_sep:
|
|
1191
|
+
new_fragment = f"#{route}?{_redact_query(route_query, context, depth)}"
|
|
1192
|
+
else:
|
|
1193
|
+
new_fragment = f"#{params}"
|
|
1159
1194
|
|
|
1160
1195
|
return f"{scheme}://{new_authority}{new_before_fragment}{new_fragment}"
|
|
1161
1196
|
|
|
@@ -1273,17 +1308,19 @@ def _redact_url_field_value(s: str, context: _RedactionContext) -> str:
|
|
|
1273
1308
|
redacted = _redact_url_in_str(s, context)
|
|
1274
1309
|
if redacted is not None:
|
|
1275
1310
|
return redacted
|
|
1276
|
-
trimmed = s.strip()
|
|
1311
|
+
trimmed = s.strip("".join(_SPAN_TERMINATOR_WHITESPACE))
|
|
1277
1312
|
if trimmed != s:
|
|
1278
1313
|
redacted = _redact_url_in_str(trimmed, context)
|
|
1279
1314
|
if redacted is not None:
|
|
1280
1315
|
return redacted
|
|
1281
1316
|
# Fail closed: a _url value we could not parse as a clean scheme-prefixed
|
|
1282
|
-
# URL, yet which carries a credential sigil ('@' userinfo)
|
|
1283
|
-
# whitespace, is redacted wholesale rather than
|
|
1284
|
-
# connection string like user:pass@host/db
|
|
1285
|
-
# surgical span logic above,
|
|
1286
|
-
|
|
1317
|
+
# URL, yet which carries a credential sigil ('@' userinfo), internal
|
|
1318
|
+
# whitespace, or a '://' scheme separator, is redacted wholesale rather than
|
|
1319
|
+
# passed through. A schemeless connection string like user:pass@host/db
|
|
1320
|
+
# has no scheme anchor for the surgical span logic above, and a URL behind
|
|
1321
|
+
# a non-whitespace prefix (a BOM, a zero-width space, a label) has one the
|
|
1322
|
+
# span logic cannot reach, so blanket redaction is the safe default.
|
|
1323
|
+
if any(_is_span_terminator_whitespace(c) for c in s) or "@" in s or "://" in s:
|
|
1287
1324
|
return REDACTED_MARKER
|
|
1288
1325
|
return s
|
|
1289
1326
|
|
|
@@ -1304,7 +1341,7 @@ def _redact_userinfo_password(authority: str) -> str:
|
|
|
1304
1341
|
return f"{authority[:colon]}:{REDACTED_MARKER}{authority[at:]}"
|
|
1305
1342
|
|
|
1306
1343
|
|
|
1307
|
-
def _redact_query(query: str, context: _RedactionContext) -> str:
|
|
1344
|
+
def _redact_query(query: str, context: _RedactionContext, depth: int) -> str:
|
|
1308
1345
|
"""Redact the values of secret-named query parameters.
|
|
1309
1346
|
|
|
1310
1347
|
Preserves the raw bytes of every other segment (keys, benign values,
|
|
@@ -1322,11 +1359,33 @@ def _redact_query(query: str, context: _RedactionContext) -> str:
|
|
|
1322
1359
|
name = unquote_plus(raw_key)
|
|
1323
1360
|
if context.is_secret_key(name):
|
|
1324
1361
|
segments.append(f"{raw_key}={REDACTED_MARKER}")
|
|
1362
|
+
continue
|
|
1363
|
+
# A redirect target carries its own parameters: redact a nested URL in
|
|
1364
|
+
# place, or the whole value when it arrived encoded.
|
|
1365
|
+
raw_value = segment[eq + 1 :]
|
|
1366
|
+
value = unquote_plus(raw_value)
|
|
1367
|
+
redacted = _redact_url_in_str(value, context, depth + 1)
|
|
1368
|
+
if redacted is not None and redacted != value:
|
|
1369
|
+
if value == raw_value:
|
|
1370
|
+
segments.append(f"{raw_key}={redacted}")
|
|
1371
|
+
else:
|
|
1372
|
+
segments.append(f"{raw_key}={REDACTED_MARKER}")
|
|
1325
1373
|
else:
|
|
1326
1374
|
segments.append(segment)
|
|
1327
1375
|
return "&".join(segments)
|
|
1328
1376
|
|
|
1329
1377
|
|
|
1378
|
+
def _has_non_numeric_port(authority: str) -> bool:
|
|
1379
|
+
"""True when the authority's text after its last ':' is not a port number.
|
|
1380
|
+
|
|
1381
|
+
A bracketed IPv6 literal without a port ends in ']' and has none.
|
|
1382
|
+
"""
|
|
1383
|
+
colon = authority.rfind(":")
|
|
1384
|
+
if colon == -1 or authority.endswith("]"):
|
|
1385
|
+
return False
|
|
1386
|
+
return not all("0" <= c <= "9" for c in authority[colon + 1 :])
|
|
1387
|
+
|
|
1388
|
+
|
|
1330
1389
|
def _is_single_url(s: str) -> bool:
|
|
1331
1390
|
"""True when ``s`` is a single bare URL, not a URL embedded in prose.
|
|
1332
1391
|
|
|
@@ -1423,20 +1482,23 @@ def _as_int(value: Any) -> int | None:
|
|
|
1423
1482
|
return None
|
|
1424
1483
|
|
|
1425
1484
|
|
|
1426
|
-
def
|
|
1427
|
-
|
|
1428
|
-
|
|
1485
|
+
def _raw_as_int(value: Any) -> int | None:
|
|
1486
|
+
if isinstance(value, _RawNumber):
|
|
1487
|
+
if re.fullmatch(r"-?\d+", value.literal):
|
|
1488
|
+
return int(value.literal)
|
|
1489
|
+
return _as_int(float(value.literal))
|
|
1490
|
+
return _as_int(value)
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
def _as_u64(value: Any) -> int | None:
|
|
1494
|
+
n = _raw_as_int(value)
|
|
1495
|
+
if n is not None and 0 <= n <= 18_446_744_073_709_551_615:
|
|
1429
1496
|
return n
|
|
1430
1497
|
return None
|
|
1431
1498
|
|
|
1432
1499
|
|
|
1433
1500
|
def _as_i64(value: Any) -> int | None:
|
|
1434
|
-
|
|
1435
|
-
if not re.fullmatch(r"-?\d+", value.literal):
|
|
1436
|
-
return None
|
|
1437
|
-
n = int(value.literal)
|
|
1438
|
-
else:
|
|
1439
|
-
n = _as_int(value)
|
|
1501
|
+
n = _raw_as_int(value)
|
|
1440
1502
|
if n is not None and -9_223_372_036_854_775_808 <= n <= 9_223_372_036_854_775_807:
|
|
1441
1503
|
return n
|
|
1442
1504
|
return None
|
|
@@ -1575,7 +1637,7 @@ def _try_process_field(key: str, value: Any) -> tuple[str, str] | None:
|
|
|
1575
1637
|
return None
|
|
1576
1638
|
stripped = _strip_suffix_ci(key, "_bytes")
|
|
1577
1639
|
if stripped is not None:
|
|
1578
|
-
n =
|
|
1640
|
+
n = _as_u64(value)
|
|
1579
1641
|
if n is not None:
|
|
1580
1642
|
return stripped, _format_bytes_human(n)
|
|
1581
1643
|
return None
|
|
@@ -1840,11 +1902,10 @@ def _yaml_key(key: str) -> str:
|
|
|
1840
1902
|
def _is_ambiguous_yaml_key(key: str) -> bool:
|
|
1841
1903
|
if key.lower() in {"true", "false", "null", "~", ".nan", ".inf", "+.inf", "-.inf"}:
|
|
1842
1904
|
return True
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
return False
|
|
1905
|
+
unsigned = key[1:] if key.startswith(("+", "-")) else key
|
|
1906
|
+
return unsigned.lower() in {"inf", "infinity", "nan"} or re.fullmatch(
|
|
1907
|
+
r"[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?", key
|
|
1908
|
+
) is not None
|
|
1848
1909
|
|
|
1849
1910
|
|
|
1850
1911
|
def _yaml_scalar(value: Any) -> str:
|
|
@@ -1945,7 +2006,7 @@ def _plain_scalar_raw(value: Any) -> str:
|
|
|
1945
2006
|
def _quote_logfmt_value(value: str) -> str:
|
|
1946
2007
|
if value == "":
|
|
1947
2008
|
return ""
|
|
1948
|
-
needs_quote = any(c
|
|
2009
|
+
needs_quote = any(c in ' \t\n\r\f\v\u00a0="\\' for c in value)
|
|
1949
2010
|
if not needs_quote:
|
|
1950
2011
|
return value
|
|
1951
2012
|
escaped = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.1
|
|
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.
|
|
3
|
+
version = "0.35.1"
|
|
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""
|
|
File without changes
|
{agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-help-v2.schema.json
RENAMED
|
File without changes
|
{agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-spec-v1.schema.json
RENAMED
|
File without changes
|
{agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data/assets/protocol-v1.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_first_data-0.34.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|