@ictechgy/context-guard 0.4.10 → 0.4.11
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/CHANGELOG.md +13 -1
- package/README.ko.md +32 -21
- package/README.md +38 -29
- package/docs/benchmark-fixtures/token-savings-12task.evidence.example.jsonl +24 -0
- package/docs/benchmark-workflow-examples.md +3 -0
- package/docs/benchmark-workflows/context-pack-byte-proxy.example.json +278 -137
- package/docs/benchmark-workflows/measured-token-workflow.example.json +279 -138
- package/docs/benchmark-workflows/provider-cache-telemetry.example.json +279 -138
- package/docs/experimental-benchmark-fixtures.md +24 -7
- package/package.json +2 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +14 -11
- package/plugins/context-guard/README.md +15 -14
- package/plugins/context-guard/bin/context-guard +46 -11
- package/plugins/context-guard/bin/context-guard-artifact +342 -33
- package/plugins/context-guard/bin/context-guard-audit +33 -2
- package/plugins/context-guard/bin/context-guard-bench +1542 -31
- package/plugins/context-guard/bin/context-guard-cache-score +318 -33
- package/plugins/context-guard/bin/context-guard-cost +7 -2
- package/plugins/context-guard/bin/context-guard-experiments +364 -8
- package/plugins/context-guard/bin/context-guard-failed-nudge +6 -2
- package/plugins/context-guard/bin/context-guard-pack +301 -17
- package/plugins/context-guard/bin/context-guard-sanitize-output +76 -12
- package/plugins/context-guard/bin/context-guard-tool-prune +241 -54
- package/plugins/context-guard/bin/context-guard-trim-output +288 -41
- package/plugins/context-guard/brief/README.md +5 -5
- package/plugins/context-guard/lib/context_guard_commands.py +214 -190
|
@@ -38,6 +38,11 @@ MAX_TOP = 200
|
|
|
38
38
|
MAX_DEFERRED_TOP = 1_000
|
|
39
39
|
MAX_NAMESPACE_TOP = 200
|
|
40
40
|
MAX_LABEL_CHARS = 160
|
|
41
|
+
NO_FOLLOW_SUPPORTED = hasattr(os, "O_NOFOLLOW")
|
|
42
|
+
DIR_FD_OPEN_SUPPORTED = bool(os.supports_dir_fd and os.open in os.supports_dir_fd)
|
|
43
|
+
DIR_FD_MKDIR_SUPPORTED = bool(os.supports_dir_fd and os.mkdir in os.supports_dir_fd)
|
|
44
|
+
DIR_FD_STAT_SUPPORTED = bool(os.supports_dir_fd and os.stat in os.supports_dir_fd)
|
|
45
|
+
DIR_FD_UNLINK_SUPPORTED = bool(os.supports_dir_fd and os.unlink in os.supports_dir_fd)
|
|
41
46
|
MAX_DESCRIPTION_CHARS = 360
|
|
42
47
|
MAX_OMITTED_TOOLS = 30
|
|
43
48
|
TOKEN_PROXY_CHARS_PER_TOKEN = 4
|
|
@@ -201,12 +206,26 @@ def sanitize_value(value: Any, *, sensitive_context: bool = False, sensitive_sch
|
|
|
201
206
|
|
|
202
207
|
|
|
203
208
|
def read_limited_path(path: Path, max_bytes: int) -> str:
|
|
209
|
+
if not NO_FOLLOW_SUPPORTED:
|
|
210
|
+
fail("catalog reads require O_NOFOLLOW support")
|
|
211
|
+
reject_parent_traversal(path, label="catalog")
|
|
212
|
+
# Preserve clear diagnostics for stable symlink paths, then anchor the real
|
|
213
|
+
# read to an opened no-follow parent fd so parent/leaf swaps after this
|
|
214
|
+
# precheck still fail closed.
|
|
204
215
|
reject_symlink_components(path)
|
|
205
|
-
|
|
216
|
+
parent_fd = open_private_directory_no_follow(path.parent, label="catalog directory", create=False)
|
|
217
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
218
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
219
|
+
flags |= os.O_CLOEXEC
|
|
220
|
+
leaf = path.name
|
|
221
|
+
if leaf in {"", ".", ".."}:
|
|
222
|
+
os.close(parent_fd)
|
|
223
|
+
fail("catalog must name a regular file")
|
|
206
224
|
try:
|
|
207
|
-
fd = os.open(
|
|
225
|
+
fd = os.open(leaf, flags, dir_fd=parent_fd)
|
|
208
226
|
except OSError as exc:
|
|
209
|
-
|
|
227
|
+
os.close(parent_fd)
|
|
228
|
+
fail(f"catalog read failed: {os_error_detail(exc)}")
|
|
210
229
|
try:
|
|
211
230
|
st = os.fstat(fd)
|
|
212
231
|
if not stat.S_ISREG(st.st_mode):
|
|
@@ -216,6 +235,7 @@ def read_limited_path(path: Path, max_bytes: int) -> str:
|
|
|
216
235
|
data = os.read(fd, max_bytes + 1)
|
|
217
236
|
finally:
|
|
218
237
|
os.close(fd)
|
|
238
|
+
os.close(parent_fd)
|
|
219
239
|
if len(data) > max_bytes:
|
|
220
240
|
fail(f"catalog exceeds --max-catalog-bytes: > {max_bytes}")
|
|
221
241
|
return data.decode("utf-8", errors="replace")
|
|
@@ -409,15 +429,142 @@ def reject_symlink_components(path: Path) -> None:
|
|
|
409
429
|
fail(f"refusing path through non-directory component: {current}")
|
|
410
430
|
|
|
411
431
|
|
|
432
|
+
def dir_fd_replace_supported() -> bool:
|
|
433
|
+
try:
|
|
434
|
+
import inspect
|
|
435
|
+
|
|
436
|
+
signature = inspect.signature(os.replace)
|
|
437
|
+
except (TypeError, ValueError):
|
|
438
|
+
return True
|
|
439
|
+
return "src_dir_fd" in signature.parameters and "dst_dir_fd" in signature.parameters
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
DIR_FD_REPLACE_SUPPORTED = dir_fd_replace_supported()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def reject_parent_traversal(path: Path, *, label: str) -> None:
|
|
446
|
+
if ".." in path.parts:
|
|
447
|
+
fail(f"{label} must not contain parent traversal")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def os_error_detail(exc: OSError) -> str:
|
|
451
|
+
detail = exc.strerror or str(exc) or exc.__class__.__name__
|
|
452
|
+
if exc.errno is not None:
|
|
453
|
+
return f"{detail} (errno {exc.errno})"
|
|
454
|
+
return detail
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def no_follow_dir_flags() -> int:
|
|
458
|
+
if not NO_FOLLOW_SUPPORTED:
|
|
459
|
+
fail("private store IO requires O_NOFOLLOW support")
|
|
460
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
461
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
462
|
+
flags |= os.O_CLOEXEC
|
|
463
|
+
if hasattr(os, "O_DIRECTORY"):
|
|
464
|
+
flags |= os.O_DIRECTORY
|
|
465
|
+
return flags
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def private_temp_file_flags() -> int:
|
|
469
|
+
if not NO_FOLLOW_SUPPORTED:
|
|
470
|
+
fail("private store IO requires O_NOFOLLOW support")
|
|
471
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
|
|
472
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
473
|
+
flags |= os.O_CLOEXEC
|
|
474
|
+
if hasattr(os, "O_NOCTTY"):
|
|
475
|
+
flags |= os.O_NOCTTY
|
|
476
|
+
return flags
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def open_private_directory_no_follow(path: Path, *, label: str, create: bool) -> int:
|
|
480
|
+
reject_parent_traversal(path, label=label)
|
|
481
|
+
path = normalize_allowed_first_absolute_symlink(path.expanduser())
|
|
482
|
+
if not DIR_FD_OPEN_SUPPORTED:
|
|
483
|
+
fail(f"{label} requires dir_fd open support")
|
|
484
|
+
if create and not DIR_FD_MKDIR_SUPPORTED:
|
|
485
|
+
fail(f"{label} requires dir_fd mkdir support")
|
|
486
|
+
flags = no_follow_dir_flags()
|
|
487
|
+
if path.is_absolute():
|
|
488
|
+
root_flags = os.O_RDONLY | (os.O_CLOEXEC if hasattr(os, "O_CLOEXEC") else 0)
|
|
489
|
+
current_fd = os.open(path.anchor or os.sep, root_flags)
|
|
490
|
+
parts = path.parts[1:]
|
|
491
|
+
else:
|
|
492
|
+
current_fd = os.open(".", flags)
|
|
493
|
+
parts = path.parts
|
|
494
|
+
try:
|
|
495
|
+
for part in parts:
|
|
496
|
+
if part in {"", "."}:
|
|
497
|
+
continue
|
|
498
|
+
if part == "..":
|
|
499
|
+
fail(f"{label} must not contain parent traversal")
|
|
500
|
+
try:
|
|
501
|
+
next_fd = os.open(part, flags, dir_fd=current_fd)
|
|
502
|
+
except FileNotFoundError:
|
|
503
|
+
if not create:
|
|
504
|
+
raise
|
|
505
|
+
os.mkdir(part, 0o700, dir_fd=current_fd)
|
|
506
|
+
next_fd = os.open(part, flags, dir_fd=current_fd)
|
|
507
|
+
try:
|
|
508
|
+
if not stat.S_ISDIR(os.fstat(next_fd).st_mode):
|
|
509
|
+
fail(f"{label} must not traverse non-directory components")
|
|
510
|
+
except Exception:
|
|
511
|
+
os.close(next_fd)
|
|
512
|
+
raise
|
|
513
|
+
os.close(current_fd)
|
|
514
|
+
current_fd = next_fd
|
|
515
|
+
owned_fd = current_fd
|
|
516
|
+
current_fd = -1
|
|
517
|
+
return owned_fd
|
|
518
|
+
except OSError as exc:
|
|
519
|
+
fail(f"could not inspect {label}: {os_error_detail(exc)}")
|
|
520
|
+
finally:
|
|
521
|
+
if current_fd >= 0:
|
|
522
|
+
os.close(current_fd)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def precheck_private_leaf(parent_fd: int, leaf: str, *, label: str) -> None:
|
|
526
|
+
if not DIR_FD_STAT_SUPPORTED:
|
|
527
|
+
fail(f"{label} requires dir_fd stat support")
|
|
528
|
+
try:
|
|
529
|
+
st = os.stat(leaf, dir_fd=parent_fd, follow_symlinks=False)
|
|
530
|
+
except FileNotFoundError:
|
|
531
|
+
return
|
|
532
|
+
except OSError as exc:
|
|
533
|
+
fail(f"could not inspect {label}: {os_error_detail(exc)}")
|
|
534
|
+
if not stat.S_ISREG(st.st_mode):
|
|
535
|
+
fail(f"{label} must be missing or a regular file")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def write_all_fd(fd: int, data: bytes) -> None:
|
|
539
|
+
view = memoryview(data)
|
|
540
|
+
offset = 0
|
|
541
|
+
while offset < len(view):
|
|
542
|
+
written = os.write(fd, view[offset:])
|
|
543
|
+
if written <= 0:
|
|
544
|
+
raise OSError("short write")
|
|
545
|
+
offset += written
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def fsync_best_effort(fd: int) -> None:
|
|
549
|
+
try:
|
|
550
|
+
os.fsync(fd)
|
|
551
|
+
except OSError:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
|
|
412
555
|
def ensure_private_dir(path: Path) -> None:
|
|
413
|
-
path =
|
|
414
|
-
reject_symlink_components(path)
|
|
556
|
+
reject_parent_traversal(path, label="store directory")
|
|
415
557
|
try:
|
|
416
|
-
path
|
|
417
|
-
reject_symlink_components(path)
|
|
418
|
-
os.chmod(path, 0o700)
|
|
558
|
+
fd = open_private_directory_no_follow(path, label="store directory", create=True)
|
|
419
559
|
except OSError as exc:
|
|
420
560
|
fail(f"store directory unavailable: {exc}")
|
|
561
|
+
try:
|
|
562
|
+
try:
|
|
563
|
+
os.fchmod(fd, 0o700)
|
|
564
|
+
except OSError:
|
|
565
|
+
pass
|
|
566
|
+
finally:
|
|
567
|
+
os.close(fd)
|
|
421
568
|
|
|
422
569
|
|
|
423
570
|
def write_private_json_atomic(path: Path, data: dict[str, Any], *, max_bytes: int, label: str) -> int:
|
|
@@ -425,43 +572,77 @@ def write_private_json_atomic(path: Path, data: dict[str, Any], *, max_bytes: in
|
|
|
425
572
|
size = byte_len_text(text)
|
|
426
573
|
if size > max_bytes:
|
|
427
574
|
fail(f"{label} exceeds size cap: {size} > {max_bytes}")
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
fail(f"{label} write
|
|
575
|
+
reject_parent_traversal(path, label=label)
|
|
576
|
+
if not DIR_FD_REPLACE_SUPPORTED:
|
|
577
|
+
fail(f"{label} write requires dir_fd replace support")
|
|
578
|
+
if not DIR_FD_UNLINK_SUPPORTED:
|
|
579
|
+
fail(f"{label} write requires dir_fd unlink support")
|
|
580
|
+
if not DIR_FD_STAT_SUPPORTED:
|
|
581
|
+
fail(f"{label} write requires dir_fd stat support")
|
|
582
|
+
parent_fd = open_private_directory_no_follow(path.parent, label="store directory", create=True)
|
|
583
|
+
fd = -1
|
|
584
|
+
temp_leaf: str | None = None
|
|
435
585
|
try:
|
|
436
|
-
with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
|
|
437
|
-
handle.write(text)
|
|
438
|
-
handle.flush()
|
|
439
|
-
try:
|
|
440
|
-
os.fsync(handle.fileno())
|
|
441
|
-
except OSError:
|
|
442
|
-
pass
|
|
443
|
-
os.replace(tmp, path)
|
|
444
586
|
try:
|
|
445
|
-
os.
|
|
587
|
+
os.fchmod(parent_fd, 0o700)
|
|
446
588
|
except OSError:
|
|
447
589
|
pass
|
|
590
|
+
leaf = path.name
|
|
591
|
+
if leaf in {"", ".", ".."}:
|
|
592
|
+
fail(f"{label} must name a regular file")
|
|
593
|
+
precheck_private_leaf(parent_fd, leaf, label=label)
|
|
594
|
+
for _attempt in range(20):
|
|
595
|
+
candidate = f".{leaf}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
596
|
+
try:
|
|
597
|
+
fd = os.open(candidate, private_temp_file_flags(), 0o600, dir_fd=parent_fd)
|
|
598
|
+
temp_leaf = candidate
|
|
599
|
+
break
|
|
600
|
+
except FileExistsError:
|
|
601
|
+
continue
|
|
602
|
+
if fd < 0 or temp_leaf is None:
|
|
603
|
+
fail(f"{label} write failed: could not create temporary file")
|
|
604
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
605
|
+
fail(f"{label} temporary file must be a regular file")
|
|
606
|
+
os.fchmod(fd, 0o600)
|
|
607
|
+
write_all_fd(fd, text.encode("utf-8"))
|
|
608
|
+
fsync_best_effort(fd)
|
|
609
|
+
os.close(fd)
|
|
610
|
+
fd = -1
|
|
611
|
+
fsync_best_effort(parent_fd)
|
|
612
|
+
os.replace(temp_leaf, leaf, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
613
|
+
temp_leaf = None
|
|
614
|
+
fsync_best_effort(parent_fd)
|
|
615
|
+
except OSError as exc:
|
|
616
|
+
fail(f"{label} write failed: {os_error_detail(exc)}")
|
|
448
617
|
except Exception:
|
|
449
|
-
try:
|
|
450
|
-
tmp.unlink()
|
|
451
|
-
except OSError:
|
|
452
|
-
pass
|
|
453
618
|
raise
|
|
619
|
+
finally:
|
|
620
|
+
if fd >= 0:
|
|
621
|
+
os.close(fd)
|
|
622
|
+
if temp_leaf is not None:
|
|
623
|
+
try:
|
|
624
|
+
os.unlink(temp_leaf, dir_fd=parent_fd)
|
|
625
|
+
except OSError:
|
|
626
|
+
pass
|
|
627
|
+
os.close(parent_fd)
|
|
454
628
|
return size
|
|
455
629
|
|
|
456
630
|
|
|
457
631
|
def read_private_text(path: Path, *, max_bytes: int, label: str) -> tuple[str, int]:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
flags = os.O_RDONLY |
|
|
632
|
+
reject_parent_traversal(path, label=label)
|
|
633
|
+
parent_fd = open_private_directory_no_follow(path.parent, label=f"{label} directory", create=False)
|
|
634
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
635
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
636
|
+
flags |= os.O_CLOEXEC
|
|
637
|
+
leaf = path.name
|
|
638
|
+
if leaf in {"", ".", ".."}:
|
|
639
|
+
os.close(parent_fd)
|
|
640
|
+
fail(f"{label} must name a regular file")
|
|
461
641
|
try:
|
|
462
|
-
fd = os.open(
|
|
642
|
+
fd = os.open(leaf, flags, dir_fd=parent_fd)
|
|
463
643
|
except OSError as exc:
|
|
464
|
-
|
|
644
|
+
os.close(parent_fd)
|
|
645
|
+
fail(f"{label} read failed: {os_error_detail(exc)}")
|
|
465
646
|
try:
|
|
466
647
|
st = os.fstat(fd)
|
|
467
648
|
if not stat.S_ISREG(st.st_mode):
|
|
@@ -471,32 +652,16 @@ def read_private_text(path: Path, *, max_bytes: int, label: str) -> tuple[str, i
|
|
|
471
652
|
data = os.read(fd, max_bytes + 1)
|
|
472
653
|
finally:
|
|
473
654
|
os.close(fd)
|
|
655
|
+
os.close(parent_fd)
|
|
474
656
|
if len(data) > max_bytes:
|
|
475
657
|
fail(f"{label} exceeds trusted size cap: > {max_bytes}")
|
|
476
658
|
return data.decode("utf-8", errors="replace"), len(data)
|
|
477
659
|
|
|
478
660
|
|
|
479
661
|
def read_private_json(path: Path, *, max_bytes: int, label: str) -> dict[str, Any]:
|
|
480
|
-
|
|
481
|
-
fail(f"{label} must not be a symlink")
|
|
482
|
-
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
483
|
-
try:
|
|
484
|
-
fd = os.open(str(path), flags)
|
|
485
|
-
except OSError as exc:
|
|
486
|
-
fail(f"{label} read failed: {exc}")
|
|
487
|
-
try:
|
|
488
|
-
st = os.fstat(fd)
|
|
489
|
-
if not stat.S_ISREG(st.st_mode):
|
|
490
|
-
fail(f"{label} must be a regular file")
|
|
491
|
-
if st.st_size > max_bytes:
|
|
492
|
-
fail(f"{label} exceeds trusted size cap: {st.st_size} > {max_bytes}")
|
|
493
|
-
data = os.read(fd, max_bytes + 1)
|
|
494
|
-
finally:
|
|
495
|
-
os.close(fd)
|
|
496
|
-
if len(data) > max_bytes:
|
|
497
|
-
fail(f"{label} exceeds trusted size cap: > {max_bytes}")
|
|
662
|
+
text, _size = read_private_text(path, max_bytes=max_bytes, label=label)
|
|
498
663
|
try:
|
|
499
|
-
parsed = json.loads(
|
|
664
|
+
parsed = json.loads(text)
|
|
500
665
|
except json.JSONDecodeError as exc:
|
|
501
666
|
fail(f"{label} is malformed JSON: {exc.msg}")
|
|
502
667
|
if not isinstance(parsed, dict):
|
|
@@ -844,7 +1009,14 @@ def defer_report(args: argparse.Namespace) -> str:
|
|
|
844
1009
|
namespace_top=namespace_top,
|
|
845
1010
|
)
|
|
846
1011
|
all_schema_bytes = sum(byte_len_json(cand.schema) for cand in ranked)
|
|
1012
|
+
listed_deferred_schema_bytes = sum(byte_len_json(cand.schema) for cand in deferred_candidates)
|
|
1013
|
+
total_deferred_schema_bytes = sum(byte_len_json(cand.schema) for cand in ranked[core_top:])
|
|
847
1014
|
tool_stub_report_bytes = byte_len_json(core_tools) + byte_len_json(deferred_tools)
|
|
1015
|
+
all_schema_tokens = proxy_tokens(all_schema_bytes)
|
|
1016
|
+
inline_core_schema_tokens = proxy_tokens(core_schema_bytes)
|
|
1017
|
+
listed_deferred_schema_tokens = proxy_tokens(listed_deferred_schema_bytes)
|
|
1018
|
+
total_deferred_schema_tokens = proxy_tokens(total_deferred_schema_bytes)
|
|
1019
|
+
tool_stub_report_tokens = proxy_tokens(tool_stub_report_bytes)
|
|
848
1020
|
result = {
|
|
849
1021
|
"tool": TOOL_NAME,
|
|
850
1022
|
"schema_version": DEFER_SCHEMA_VERSION,
|
|
@@ -862,6 +1034,7 @@ def defer_report(args: argparse.Namespace) -> str:
|
|
|
862
1034
|
"deferred_tools_truncated_count": max(0, len(ranked) - core_top - len(deferred_tools)),
|
|
863
1035
|
"deferred_namespaces": deferred_namespaces,
|
|
864
1036
|
"deferred_namespaces_truncated_count": deferred_namespaces_truncated_count,
|
|
1037
|
+
"deferred_schema_retrieval_required_before_use": True,
|
|
865
1038
|
"receipt": {
|
|
866
1039
|
**receipt,
|
|
867
1040
|
"bytes": receipt_size,
|
|
@@ -871,9 +1044,21 @@ def defer_report(args: argparse.Namespace) -> str:
|
|
|
871
1044
|
"method": "char4_proxy",
|
|
872
1045
|
"chars_per_token": TOKEN_PROXY_CHARS_PER_TOKEN,
|
|
873
1046
|
"all_schema_bytes": all_schema_bytes,
|
|
1047
|
+
"inline_core_schema_bytes": core_schema_bytes,
|
|
1048
|
+
"listed_deferred_schema_bytes": listed_deferred_schema_bytes,
|
|
1049
|
+
"total_deferred_schema_bytes": total_deferred_schema_bytes,
|
|
874
1050
|
"tool_stub_report_bytes": tool_stub_report_bytes,
|
|
875
|
-
"all_schema_tokens_estimated":
|
|
876
|
-
"
|
|
1051
|
+
"all_schema_tokens_estimated": all_schema_tokens,
|
|
1052
|
+
"inline_core_schema_tokens_estimated": inline_core_schema_tokens,
|
|
1053
|
+
"listed_deferred_schema_tokens_estimated": listed_deferred_schema_tokens,
|
|
1054
|
+
"total_deferred_schema_tokens_estimated": total_deferred_schema_tokens,
|
|
1055
|
+
"tool_stub_report_tokens_estimated": tool_stub_report_tokens,
|
|
1056
|
+
"gross_listed_deferred_schema_tokens_avoided": listed_deferred_schema_tokens,
|
|
1057
|
+
"gross_total_deferred_schema_tokens_avoided": total_deferred_schema_tokens,
|
|
1058
|
+
"net_initial_report_tokens_delta": tool_stub_report_tokens - all_schema_tokens,
|
|
1059
|
+
"net_initial_report_tokens_delta_semantics": "tool_stub_report_tokens_estimated_minus_all_schema_tokens_estimated",
|
|
1060
|
+
"estimated_initial_schema_tokens_avoided": max(0, all_schema_tokens - tool_stub_report_tokens),
|
|
1061
|
+
"estimated_initial_schema_tokens_avoided_semantics": "max(0, all_schema_tokens_estimated - tool_stub_report_tokens_estimated)",
|
|
877
1062
|
"claim_boundary": "proxy_only_not_provider_billed_tokens",
|
|
878
1063
|
},
|
|
879
1064
|
"provider_patterns": [
|
|
@@ -899,11 +1084,13 @@ def defer_report(args: argparse.Namespace) -> str:
|
|
|
899
1084
|
"provider_tool_search_configured": False,
|
|
900
1085
|
"hosted_api_token_or_cost_savings_claim_allowed": False,
|
|
901
1086
|
"requires_provider_measured_matched_tasks_for_savings_claims": True,
|
|
1087
|
+
"deferred_schema_retrieval_required_before_use": True,
|
|
902
1088
|
},
|
|
903
1089
|
"redaction": {"redacted_values": total_redactions},
|
|
904
1090
|
"caveats": [
|
|
905
1091
|
"Deferred loading is an application strategy report, not a native provider integration.",
|
|
906
1092
|
"Token proxy values are char/4 estimates over sanitized local JSON, not billed provider tokens.",
|
|
1093
|
+
"Deferred schema token fields are initial-prompt proxy accounting; full schemas must be retrieved before deferred tool use.",
|
|
907
1094
|
"Use receipt get commands to retrieve full sanitized schemas before using deferred tools.",
|
|
908
1095
|
],
|
|
909
1096
|
}
|