agent-first-data 0.35.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.
Files changed (19) hide show
  1. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/PKG-INFO +1 -1
  2. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/format.py +74 -14
  3. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/PKG-INFO +1 -1
  4. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/pyproject.toml +1 -1
  5. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/README.md +0 -0
  6. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/__init__.py +0 -0
  7. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-help-v2.schema.json +0 -0
  8. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/assets/cli-spec-v1.schema.json +0 -0
  9. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/assets/protocol-v1.schema.json +0 -0
  10. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/assets/registry.json +0 -0
  11. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data/cli.py +0 -0
  12. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/SOURCES.txt +0 -0
  13. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/dependency_links.txt +0 -0
  14. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/agent_first_data.egg-info/top_level.txt +0 -0
  15. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/setup.cfg +0 -0
  16. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/tests/test_cli.py +0 -0
  17. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/tests/test_format.py +0 -0
  18. {agent_first_data-0.35.0 → agent_first_data-0.35.1}/tests/test_no_stderr_policy.py +0 -0
  19. {agent_first_data-0.35.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.35.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
@@ -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
- new_fragment = f"#{_redact_query(fragment, context)}" if hash_sep else ""
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
 
@@ -1306,7 +1341,7 @@ def _redact_userinfo_password(authority: str) -> str:
1306
1341
  return f"{authority[:colon]}:{REDACTED_MARKER}{authority[at:]}"
1307
1342
 
1308
1343
 
1309
- def _redact_query(query: str, context: _RedactionContext) -> str:
1344
+ def _redact_query(query: str, context: _RedactionContext, depth: int) -> str:
1310
1345
  """Redact the values of secret-named query parameters.
1311
1346
 
1312
1347
  Preserves the raw bytes of every other segment (keys, benign values,
@@ -1324,11 +1359,33 @@ def _redact_query(query: str, context: _RedactionContext) -> str:
1324
1359
  name = unquote_plus(raw_key)
1325
1360
  if context.is_secret_key(name):
1326
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}")
1327
1373
  else:
1328
1374
  segments.append(segment)
1329
1375
  return "&".join(segments)
1330
1376
 
1331
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
+
1332
1389
  def _is_single_url(s: str) -> bool:
1333
1390
  """True when ``s`` is a single bare URL, not a URL embedded in prose.
1334
1391
 
@@ -1425,20 +1482,23 @@ def _as_int(value: Any) -> int | None:
1425
1482
  return None
1426
1483
 
1427
1484
 
1428
- def _as_non_neg_int(value: Any) -> int | None:
1429
- n = _as_int(value)
1430
- if n is not None and n >= 0:
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:
1431
1496
  return n
1432
1497
  return None
1433
1498
 
1434
1499
 
1435
1500
  def _as_i64(value: Any) -> int | None:
1436
- if isinstance(value, _RawNumber):
1437
- if not re.fullmatch(r"-?\d+", value.literal):
1438
- return None
1439
- n = int(value.literal)
1440
- else:
1441
- n = _as_int(value)
1501
+ n = _raw_as_int(value)
1442
1502
  if n is not None and -9_223_372_036_854_775_808 <= n <= 9_223_372_036_854_775_807:
1443
1503
  return n
1444
1504
  return None
@@ -1577,7 +1637,7 @@ def _try_process_field(key: str, value: Any) -> tuple[str, str] | None:
1577
1637
  return None
1578
1638
  stripped = _strip_suffix_ci(key, "_bytes")
1579
1639
  if stripped is not None:
1580
- n = _as_non_neg_int(numeric)
1640
+ n = _as_u64(value)
1581
1641
  if n is not None:
1582
1642
  return stripped, _format_bytes_human(n)
1583
1643
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.35.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.35.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"