@ictechgy/context-guard 0.4.9 → 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 +28 -0
- package/README.ko.md +59 -31
- package/README.md +85 -36
- package/docs/benchmark-fixtures/token-savings-12task-baseline.prompt.example.md +7 -0
- package/docs/benchmark-fixtures/token-savings-12task-contextguard.prompt.example.md +7 -0
- package/docs/benchmark-fixtures/token-savings-12task.evidence.example.jsonl +24 -0
- package/docs/benchmark-fixtures/token-savings-12task.tasks.example.json +182 -0
- package/docs/benchmark-fixtures/token-savings-12task.variants.example.json +10 -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/distribution.md +10 -7
- package/docs/experimental-benchmark-fixtures.md +30 -6
- package/package.json +4 -6
- package/packaging/homebrew/context-guard.rb.template +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +20 -14
- package/plugins/context-guard/README.md +26 -17
- package/plugins/context-guard/bin/context-guard +147 -25
- package/plugins/context-guard/bin/context-guard-artifact +884 -79
- 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 +665 -0
- package/plugins/context-guard/bin/context-guard-compress +146 -1
- package/plugins/context-guard/bin/context-guard-cost +790 -6
- package/plugins/context-guard/bin/context-guard-experiments +463 -26
- package/plugins/context-guard/bin/context-guard-failed-nudge +9 -2
- package/plugins/context-guard/bin/context-guard-filter +163 -7
- package/plugins/context-guard/bin/context-guard-guard-read +3 -0
- package/plugins/context-guard/bin/context-guard-pack +892 -49
- package/plugins/context-guard/bin/context-guard-rewrite-bash +3 -0
- package/plugins/context-guard/bin/context-guard-sanitize-output +76 -12
- package/plugins/context-guard/bin/context-guard-setup +165 -31
- package/plugins/context-guard/bin/context-guard-statusline +490 -283
- package/plugins/context-guard/bin/context-guard-statusline-merged +5 -0
- package/plugins/context-guard/bin/context-guard-tool-prune +480 -53
- 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 +230 -0
- package/plugins/context-guard/skills/setup/SKILL.md +1 -0
- package/context-guard-kit/README.md +0 -91
- package/context-guard-kit/benchmark_runner.py +0 -2401
- package/context-guard-kit/claude_transcript_cost_audit.py +0 -2346
- package/context-guard-kit/context_compress.py +0 -695
- package/context-guard-kit/context_escrow.py +0 -935
- package/context-guard-kit/context_filter.py +0 -637
- package/context-guard-kit/context_guard_cli.py +0 -325
- package/context-guard-kit/context_guard_diet.py +0 -1711
- package/context-guard-kit/context_pack.py +0 -2713
- package/context-guard-kit/cost_guard.py +0 -2349
- package/context-guard-kit/experimental_registry.py +0 -4348
- package/context-guard-kit/failed_attempt_nudge.py +0 -567
- package/context-guard-kit/guard_large_read.py +0 -690
- package/context-guard-kit/hook_secret_patterns.py +0 -43
- package/context-guard-kit/read_symbol.py +0 -483
- package/context-guard-kit/rewrite_bash_for_token_budget.py +0 -501
- package/context-guard-kit/sanitize_output.py +0 -725
- package/context-guard-kit/settings.example.json +0 -67
- package/context-guard-kit/setup_wizard.py +0 -2515
- package/context-guard-kit/statusline.sh +0 -362
- package/context-guard-kit/statusline_merged.sh +0 -157
- package/context-guard-kit/tool_schema_pruner.py +0 -837
- package/context-guard-kit/trim_command_output.py +0 -1449
|
@@ -14,6 +14,8 @@ from datetime import datetime, timezone
|
|
|
14
14
|
import http.client
|
|
15
15
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
16
16
|
import hashlib
|
|
17
|
+
import importlib.machinery
|
|
18
|
+
import importlib.util
|
|
17
19
|
import ipaddress
|
|
18
20
|
import json
|
|
19
21
|
import math
|
|
@@ -26,6 +28,7 @@ from socketserver import TCPServer
|
|
|
26
28
|
from pathlib import Path
|
|
27
29
|
import stat
|
|
28
30
|
import sys
|
|
31
|
+
import time
|
|
29
32
|
from typing import Any, NoReturn
|
|
30
33
|
import unicodedata
|
|
31
34
|
from urllib.parse import urlparse
|
|
@@ -63,6 +66,7 @@ LOCAL_PROXY_FORWARD_SCHEMA_VERSION = "contextguard.experiments.local-proxy-forwa
|
|
|
63
66
|
LOCAL_PROXY_DIAGNOSTIC_SCHEMA_VERSION = "contextguard.experiments.local-proxy-forward-diagnostic.v1"
|
|
64
67
|
LOCAL_PROXY_READY_SCHEMA_VERSION = "contextguard.experiments.local-proxy-ready.v1"
|
|
65
68
|
LOCAL_PROXY_EXTERNAL_DESIGN_SCHEMA_VERSION = "contextguard.experiments.local-proxy-external-forwarding-design.v1"
|
|
69
|
+
LOCAL_PROXY_RESPONSE_SANDBOX_SCHEMA_VERSION = "contextguard.experiments.local-proxy-response-sandbox.v1"
|
|
66
70
|
LOCAL_PROXY_DEFAULT_BIND_HOST = "127.0.0.1"
|
|
67
71
|
LOCAL_PROXY_DEFAULT_BIND_PORT = 0
|
|
68
72
|
LOCAL_PROXY_DEFAULT_TARGET_HOST = "127.0.0.1"
|
|
@@ -75,6 +79,7 @@ LOCAL_PROXY_DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024
|
|
|
75
79
|
LOCAL_PROXY_MAX_FORWARD_BYTES = 2 * 1024 * 1024
|
|
76
80
|
LOCAL_PROXY_DEFAULT_TIMEOUT_SECONDS = 5.0
|
|
77
81
|
LOCAL_PROXY_MAX_TIMEOUT_SECONDS = 30.0
|
|
82
|
+
LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE = "local_proxy_sanitized_response_body"
|
|
78
83
|
LOCAL_PROXY_EXTERNAL_ALLOWED_SCHEMES = {"https"}
|
|
79
84
|
LOCAL_PROXY_EXTERNAL_CREDENTIAL_REDACTION_POLICY = "strip-sensitive-headers"
|
|
80
85
|
LOCAL_PROXY_EXTERNAL_PROVIDER_EVIDENCE_BOUNDARY = "diagnostic-only-provider-measured-required"
|
|
@@ -89,6 +94,7 @@ LOCAL_PROXY_SENSITIVE_HEADER_NAMES = {
|
|
|
89
94
|
"cookie",
|
|
90
95
|
"set-cookie",
|
|
91
96
|
}
|
|
97
|
+
LOCAL_PROXY_NONCE_HEADER = "X-ContextGuard-Proxy-Nonce"
|
|
92
98
|
LOCAL_PROXY_HOP_BY_HOP_HEADERS = {
|
|
93
99
|
"connection",
|
|
94
100
|
"keep-alive",
|
|
@@ -336,8 +342,8 @@ EXPERIMENTS: tuple[Experiment, ...] = (
|
|
|
336
342
|
"context-guard experiments plan local-proxy",
|
|
337
343
|
"context-guard experiments plan local-proxy-external-forwarding",
|
|
338
344
|
"context-guard experiments record local-proxy-runtime-gate --ledger-jsonl <path>",
|
|
339
|
-
"context-guard experiments serve local-proxy --bind-host 127.0.0.1 --bind-port <port> --target-host 127.0.0.1 --target-port <port> --runtime-gate-ack --forwarding-gate-ack --once",
|
|
340
|
-
"context-guard experiments serve local-proxy --diagnostic-ledger-jsonl <path> ...",
|
|
345
|
+
"context-guard experiments serve local-proxy --bind-host 127.0.0.1 --bind-port <port> --target-host 127.0.0.1 --target-port <port> --runtime-gate-ack --forwarding-gate-ack --once --ready-file <path>",
|
|
346
|
+
"context-guard experiments serve local-proxy --ready-file <ready-file> --diagnostic-ledger-jsonl <path> ...",
|
|
341
347
|
),
|
|
342
348
|
opt_in_flags=(
|
|
343
349
|
"plan local-proxy",
|
|
@@ -356,6 +362,7 @@ EXPERIMENTS: tuple[Experiment, ...] = (
|
|
|
356
362
|
"--max-request-bytes",
|
|
357
363
|
"--max-response-bytes",
|
|
358
364
|
"--diagnostic-ledger-jsonl",
|
|
365
|
+
"--ready-file",
|
|
359
366
|
"--external-forwarding-intent",
|
|
360
367
|
"--external-forwarding-design-ack",
|
|
361
368
|
"--allow-host",
|
|
@@ -372,10 +379,11 @@ EXPERIMENTS: tuple[Experiment, ...] = (
|
|
|
372
379
|
),
|
|
373
380
|
evidence_contract=(
|
|
374
381
|
"Gate rows require localhost-only bind/target metadata and explicit runtime gate acknowledgement. Serve "
|
|
375
|
-
"evidence requires loopback-only bind/target IPs,
|
|
376
|
-
"forwarding or persistence, bounded bytes/timeouts, and optional diagnostic
|
|
377
|
-
"shifted-cost evidence only. External-forwarding design plans require threat model
|
|
378
|
-
"allowlists, credential redaction policy, and provider-evidence boundaries before any future
|
|
382
|
+
"evidence requires loopback-only bind/target IPs, a private ready-file nonce handoff, explicit forwarding "
|
|
383
|
+
"acknowledgement, no credential forwarding or persistence, bounded bytes/timeouts, and optional diagnostic "
|
|
384
|
+
"ledger rows that remain shifted-cost evidence only. External-forwarding design plans require threat model "
|
|
385
|
+
"notes, explicit allowlists, credential redaction policy, and provider-evidence boundaries before any future "
|
|
386
|
+
"runtime."
|
|
379
387
|
),
|
|
380
388
|
),
|
|
381
389
|
)
|
|
@@ -2642,6 +2650,9 @@ def read_local_proxy_payload(args: argparse.Namespace) -> tuple[dict[str, Any],
|
|
|
2642
2650
|
"max_response_bytes",
|
|
2643
2651
|
"timeout_seconds",
|
|
2644
2652
|
"diagnostic_ledger_jsonl",
|
|
2653
|
+
"response_sandbox",
|
|
2654
|
+
"response_artifact_dir",
|
|
2655
|
+
"show_artifact_paths",
|
|
2645
2656
|
}
|
|
2646
2657
|
ignored.extend(sanitize_self_hosted_ignored_key(key) for key in envelope if key not in allowed)
|
|
2647
2658
|
return dict(envelope), {
|
|
@@ -2684,8 +2695,13 @@ def coalesce_local_proxy_bool(args: argparse.Namespace, payload: dict[str, Any],
|
|
|
2684
2695
|
return parse_local_proxy_json_bool(payload.get(key))
|
|
2685
2696
|
|
|
2686
2697
|
|
|
2687
|
-
def local_proxy_plan_payload(
|
|
2688
|
-
|
|
2698
|
+
def local_proxy_plan_payload(
|
|
2699
|
+
args: argparse.Namespace,
|
|
2700
|
+
input_payload: dict[str, Any] | None = None,
|
|
2701
|
+
input_meta: dict[str, Any] | None = None,
|
|
2702
|
+
) -> dict[str, Any]:
|
|
2703
|
+
if input_payload is None or input_meta is None:
|
|
2704
|
+
input_payload, input_meta = read_local_proxy_payload(args)
|
|
2689
2705
|
bind_host_raw = coalesce_local_proxy_value(args, input_payload, "bind_host", "bind_host")
|
|
2690
2706
|
bind_port_raw = coalesce_local_proxy_value(args, input_payload, "bind_port", "bind_port")
|
|
2691
2707
|
target_host_raw = coalesce_local_proxy_value(args, input_payload, "target_host", "target_host")
|
|
@@ -3001,8 +3017,8 @@ def command_record_local_proxy_runtime_gate(args: argparse.Namespace) -> int:
|
|
|
3001
3017
|
|
|
3002
3018
|
|
|
3003
3019
|
def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
3004
|
-
|
|
3005
|
-
|
|
3020
|
+
input_payload, input_meta = read_local_proxy_payload(args)
|
|
3021
|
+
payload = local_proxy_plan_payload(args, input_payload=input_payload, input_meta=input_meta)
|
|
3006
3022
|
forwarding_gate_ack, forwarding_gate_ack_valid = coalesce_local_proxy_bool(
|
|
3007
3023
|
args,
|
|
3008
3024
|
input_payload,
|
|
@@ -3029,8 +3045,28 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3029
3045
|
"diagnostic_ledger_jsonl",
|
|
3030
3046
|
"diagnostic_ledger_jsonl",
|
|
3031
3047
|
)
|
|
3048
|
+
response_sandbox, response_sandbox_valid = coalesce_local_proxy_bool(
|
|
3049
|
+
args,
|
|
3050
|
+
input_payload,
|
|
3051
|
+
"response_sandbox",
|
|
3052
|
+
"response_sandbox",
|
|
3053
|
+
)
|
|
3054
|
+
show_artifact_paths, show_artifact_paths_valid = coalesce_local_proxy_bool(
|
|
3055
|
+
args,
|
|
3056
|
+
input_payload,
|
|
3057
|
+
"show_artifact_paths",
|
|
3058
|
+
"show_artifact_paths",
|
|
3059
|
+
)
|
|
3060
|
+
response_artifact_dir_raw = coalesce_local_proxy_value(
|
|
3061
|
+
args,
|
|
3062
|
+
input_payload,
|
|
3063
|
+
"response_artifact_dir",
|
|
3064
|
+
"response_artifact_dir",
|
|
3065
|
+
) or str(DEFAULT_CONTEXT_DIFF_ARTIFACT_DIR)
|
|
3032
3066
|
diagnostic_ledger_path = sanitize_local_proxy_value(diagnostic_ledger_raw) if diagnostic_ledger_raw else None
|
|
3033
3067
|
diagnostic_ledger_write_path = str(diagnostic_ledger_raw) if diagnostic_ledger_raw else None
|
|
3068
|
+
response_artifact_dir = sanitize_local_proxy_value(response_artifact_dir_raw)
|
|
3069
|
+
response_artifact_write_dir = str(response_artifact_dir_raw)
|
|
3034
3070
|
bind_host = payload["bind"]["host"]
|
|
3035
3071
|
target_host = payload["target"]["host"]
|
|
3036
3072
|
bind_ip_literal = is_loopback_ip_literal(bind_host)
|
|
@@ -3061,6 +3097,7 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3061
3097
|
"listener_started": False,
|
|
3062
3098
|
"traffic_forwarded": False,
|
|
3063
3099
|
"stable_runtime_behavior_changed": False,
|
|
3100
|
+
"response_sandbox_enabled": response_sandbox,
|
|
3064
3101
|
})
|
|
3065
3102
|
payload["forwarding"] = dict(payload["forwarding"])
|
|
3066
3103
|
payload["forwarding"].update({
|
|
@@ -3085,7 +3122,42 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3085
3122
|
"bytes_written": 0,
|
|
3086
3123
|
"reason": None if diagnostic_ledger_raw else "not_requested",
|
|
3087
3124
|
}
|
|
3125
|
+
payload["response_sandbox"] = {
|
|
3126
|
+
"schema_version": LOCAL_PROXY_RESPONSE_SANDBOX_SCHEMA_VERSION,
|
|
3127
|
+
"enabled": response_sandbox,
|
|
3128
|
+
"artifact_dir": response_artifact_dir,
|
|
3129
|
+
"artifact_dir_sha256": hashlib.sha256(response_artifact_write_dir.encode("utf-8", errors="replace")).hexdigest(),
|
|
3130
|
+
"show_artifact_paths": show_artifact_paths,
|
|
3131
|
+
"exact_rehydration_commands": (
|
|
3132
|
+
Path(response_artifact_write_dir).expanduser() == Path(DEFAULT_CONTEXT_DIFF_ARTIFACT_DIR)
|
|
3133
|
+
or show_artifact_paths
|
|
3134
|
+
),
|
|
3135
|
+
"text_policy": "utf8_text_only",
|
|
3136
|
+
"stored_scope": LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE,
|
|
3137
|
+
"downstream_envelope_only": True,
|
|
3138
|
+
"performed": False,
|
|
3139
|
+
"artifact_id": None,
|
|
3140
|
+
"artifact_handle": None,
|
|
3141
|
+
"sanitized_text_sha256": None,
|
|
3142
|
+
"envelope_bytes": 0,
|
|
3143
|
+
"claim_boundary": {
|
|
3144
|
+
"local_only": True,
|
|
3145
|
+
"stored_content_is_sanitized_utf8_text": True,
|
|
3146
|
+
"hosted_api_token_or_cost_savings_claim_allowed": False,
|
|
3147
|
+
},
|
|
3148
|
+
}
|
|
3149
|
+
payload["client_auth"] = {
|
|
3150
|
+
"required": True,
|
|
3151
|
+
"type": "nonce_header",
|
|
3152
|
+
"header": LOCAL_PROXY_NONCE_HEADER,
|
|
3153
|
+
"delivery": "ready_file",
|
|
3154
|
+
"ready_file_required": True,
|
|
3155
|
+
"nonce_in_public_output": False,
|
|
3156
|
+
"nonce_forwarded_upstream": False,
|
|
3157
|
+
}
|
|
3088
3158
|
payload["_diagnostic_ledger_write_path"] = diagnostic_ledger_write_path
|
|
3159
|
+
payload["_response_artifact_dir_write_path"] = response_artifact_write_dir
|
|
3160
|
+
payload["_response_artifact_show_paths"] = show_artifact_paths
|
|
3089
3161
|
payload["forward_result"] = None
|
|
3090
3162
|
|
|
3091
3163
|
blockers = list(payload["review_plan"]["readiness_blockers"])
|
|
@@ -3117,6 +3189,10 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3117
3189
|
blockers.append("invalid_max_response_bytes")
|
|
3118
3190
|
if not timeout_valid:
|
|
3119
3191
|
blockers.append("invalid_timeout_seconds")
|
|
3192
|
+
if not response_sandbox_valid:
|
|
3193
|
+
blockers.append("invalid_response_sandbox")
|
|
3194
|
+
if not show_artifact_paths_valid:
|
|
3195
|
+
blockers.append("invalid_show_artifact_paths")
|
|
3120
3196
|
blockers = list(dict.fromkeys(blockers))
|
|
3121
3197
|
payload["review_plan"]["readiness_blockers"] = blockers
|
|
3122
3198
|
payload["review_plan"]["next_steps"] = [
|
|
@@ -3155,6 +3231,10 @@ def local_proxy_forward_diagnostic_row(payload: dict[str, Any]) -> dict[str, Any
|
|
|
3155
3231
|
"upstream_status": result.get("upstream_status"),
|
|
3156
3232
|
"upstream_response_bytes": result.get("upstream_response_bytes", 0),
|
|
3157
3233
|
"body_persisted": False,
|
|
3234
|
+
"response_sandboxed": bool(result.get("response_sandboxed")),
|
|
3235
|
+
"artifact_id": result.get("response_artifact_id"),
|
|
3236
|
+
"artifact_handle": result.get("response_artifact_handle"),
|
|
3237
|
+
"sanitized_text_sha256": result.get("sanitized_text_sha256"),
|
|
3158
3238
|
},
|
|
3159
3239
|
"runtime_limits": payload["runtime_limits"],
|
|
3160
3240
|
"network_actions": payload["network_actions"],
|
|
@@ -3168,6 +3248,7 @@ def local_proxy_forward_diagnostic_row(payload: dict[str, Any]) -> dict[str, Any
|
|
|
3168
3248
|
"dns_lookup_attempted": False,
|
|
3169
3249
|
"connect_tunneling_allowed": False,
|
|
3170
3250
|
"https_mitm_allowed": False,
|
|
3251
|
+
"response_sandboxed": bool(result.get("response_sandboxed")),
|
|
3171
3252
|
"hosted_api_token_savings_claim_allowed": False,
|
|
3172
3253
|
"hosted_api_cost_savings_claim_allowed": False,
|
|
3173
3254
|
},
|
|
@@ -3210,6 +3291,12 @@ def local_proxy_has_sensitive_headers(headers: Any) -> list[str]:
|
|
|
3210
3291
|
found: list[str] = []
|
|
3211
3292
|
for name, value in headers.items():
|
|
3212
3293
|
lower = str(name).lower()
|
|
3294
|
+
if lower == LOCAL_PROXY_NONCE_HEADER.lower():
|
|
3295
|
+
# The per-run proxy nonce is a local client-auth secret delivered only
|
|
3296
|
+
# through the 0600 ready file. It is validated before this check and is
|
|
3297
|
+
# never forwarded upstream; do not let random nonce bytes
|
|
3298
|
+
# probabilistically trip the generic secret-like header detector.
|
|
3299
|
+
continue
|
|
3213
3300
|
if lower in LOCAL_PROXY_SENSITIVE_HEADER_NAMES:
|
|
3214
3301
|
found.append(lower)
|
|
3215
3302
|
elif local_proxy_secret_like(name):
|
|
@@ -3240,7 +3327,223 @@ def local_proxy_response_headers(headers: Any) -> list[tuple[str, str]]:
|
|
|
3240
3327
|
return result
|
|
3241
3328
|
|
|
3242
3329
|
|
|
3243
|
-
|
|
3330
|
+
_LOCAL_PROXY_ARTIFACT_MODULE: Any | None = None
|
|
3331
|
+
|
|
3332
|
+
|
|
3333
|
+
def load_local_proxy_artifact_module() -> Any:
|
|
3334
|
+
"""Load the artifact escrow helper from kit source or packaged plugin bin.
|
|
3335
|
+
|
|
3336
|
+
The source tree exposes ``context_escrow.py`` while the packaged plugin ships
|
|
3337
|
+
the same implementation as an extensionless executable named
|
|
3338
|
+
``context-guard-artifact``. Load either without adding non-stdlib runtime
|
|
3339
|
+
dependencies to the experimental registry.
|
|
3340
|
+
"""
|
|
3341
|
+
global _LOCAL_PROXY_ARTIFACT_MODULE
|
|
3342
|
+
if _LOCAL_PROXY_ARTIFACT_MODULE is not None:
|
|
3343
|
+
return _LOCAL_PROXY_ARTIFACT_MODULE
|
|
3344
|
+
try:
|
|
3345
|
+
import context_escrow as artifact_module # type: ignore[import-not-found]
|
|
3346
|
+
except ImportError:
|
|
3347
|
+
current_dir = Path(__file__).resolve().parent
|
|
3348
|
+
candidates = [
|
|
3349
|
+
current_dir / "context_escrow.py",
|
|
3350
|
+
current_dir / "context-guard-artifact",
|
|
3351
|
+
]
|
|
3352
|
+
for candidate in candidates:
|
|
3353
|
+
if not candidate.is_file():
|
|
3354
|
+
continue
|
|
3355
|
+
loader = importlib.machinery.SourceFileLoader("_context_guard_local_proxy_artifact", str(candidate))
|
|
3356
|
+
spec = importlib.util.spec_from_loader("_context_guard_local_proxy_artifact", loader)
|
|
3357
|
+
if spec is None:
|
|
3358
|
+
continue
|
|
3359
|
+
module = importlib.util.module_from_spec(spec)
|
|
3360
|
+
loader.exec_module(module)
|
|
3361
|
+
_LOCAL_PROXY_ARTIFACT_MODULE = module
|
|
3362
|
+
return module
|
|
3363
|
+
raise RegistryError("could not load local artifact helper for response sandbox")
|
|
3364
|
+
_LOCAL_PROXY_ARTIFACT_MODULE = artifact_module
|
|
3365
|
+
return artifact_module
|
|
3366
|
+
|
|
3367
|
+
|
|
3368
|
+
def local_proxy_response_text_policy(body: bytes, content_type: str) -> tuple[str | None, str | None, str]:
|
|
3369
|
+
"""Return decoded UTF-8 text for response sandbox or a block reason.
|
|
3370
|
+
|
|
3371
|
+
G003 intentionally defines exact rehydration as exact sanitized UTF-8 text
|
|
3372
|
+
retrieval, not original arbitrary HTTP bytes.
|
|
3373
|
+
"""
|
|
3374
|
+
base_content_type = content_type.split(";", 1)[0].strip().lower()
|
|
3375
|
+
text_like = (
|
|
3376
|
+
not base_content_type
|
|
3377
|
+
or base_content_type.startswith("text/")
|
|
3378
|
+
or base_content_type in {
|
|
3379
|
+
"application/json",
|
|
3380
|
+
"application/javascript",
|
|
3381
|
+
"application/xml",
|
|
3382
|
+
"application/x-ndjson",
|
|
3383
|
+
"application/problem+json",
|
|
3384
|
+
}
|
|
3385
|
+
or base_content_type.endswith("+json")
|
|
3386
|
+
or base_content_type.endswith("+xml")
|
|
3387
|
+
)
|
|
3388
|
+
binary_like = (
|
|
3389
|
+
base_content_type.startswith("image/")
|
|
3390
|
+
or base_content_type.startswith("audio/")
|
|
3391
|
+
or base_content_type.startswith("video/")
|
|
3392
|
+
or base_content_type in {
|
|
3393
|
+
"application/octet-stream",
|
|
3394
|
+
"application/pdf",
|
|
3395
|
+
"application/zip",
|
|
3396
|
+
"application/gzip",
|
|
3397
|
+
}
|
|
3398
|
+
)
|
|
3399
|
+
if binary_like or not text_like:
|
|
3400
|
+
return None, "response_sandbox_text_required", base_content_type or "unknown"
|
|
3401
|
+
try:
|
|
3402
|
+
text = body.decode("utf-8")
|
|
3403
|
+
except UnicodeDecodeError:
|
|
3404
|
+
return None, "response_sandbox_text_required", base_content_type or "unknown"
|
|
3405
|
+
if any((ord(ch) < 32 and ch not in "\n\r\t") or ord(ch) == 127 for ch in text):
|
|
3406
|
+
return None, "response_sandbox_text_required", base_content_type or "unknown"
|
|
3407
|
+
return text, None, base_content_type or "none"
|
|
3408
|
+
|
|
3409
|
+
|
|
3410
|
+
def local_proxy_store_response_artifact(
|
|
3411
|
+
response_text: str,
|
|
3412
|
+
*,
|
|
3413
|
+
artifact_dir: str,
|
|
3414
|
+
show_artifact_paths: bool,
|
|
3415
|
+
upstream_status: int,
|
|
3416
|
+
) -> dict[str, Any]:
|
|
3417
|
+
artifact = load_local_proxy_artifact_module()
|
|
3418
|
+
directory = artifact.normalize_allowed_first_absolute_symlink(Path(artifact_dir).expanduser())
|
|
3419
|
+
sanitized_text, redacted_lines = artifact.sanitize_text(response_text, show_paths=show_artifact_paths)
|
|
3420
|
+
content_bytes = len(sanitized_text.encode("utf-8", errors="replace"))
|
|
3421
|
+
content_sha = hashlib.sha256(sanitized_text.encode("utf-8", errors="replace")).hexdigest()
|
|
3422
|
+
command_preview = f"local-proxy response sandbox upstream_status={upstream_status}"
|
|
3423
|
+
id_basis = json.dumps(
|
|
3424
|
+
{
|
|
3425
|
+
"content_sha256": content_sha,
|
|
3426
|
+
"command_preview": command_preview,
|
|
3427
|
+
"scope": LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE,
|
|
3428
|
+
},
|
|
3429
|
+
sort_keys=True,
|
|
3430
|
+
)
|
|
3431
|
+
artifact_id = hashlib.sha256(id_basis.encode("utf-8")).hexdigest()[:20]
|
|
3432
|
+
content_path, meta_path = artifact.artifact_paths(directory, artifact_id)
|
|
3433
|
+
total_lines = sanitized_text.count("\n") + (1 if sanitized_text and not sanitized_text.endswith("\n") else 0)
|
|
3434
|
+
content_type = artifact.classify_content_type(sanitized_text)
|
|
3435
|
+
strategy = artifact.recommended_strategy(content_type)
|
|
3436
|
+
metadata: dict[str, Any] = {
|
|
3437
|
+
"artifact_id": artifact_id,
|
|
3438
|
+
"created_at": int(time.time()),
|
|
3439
|
+
"command_preview": command_preview,
|
|
3440
|
+
"content_type": content_type,
|
|
3441
|
+
"input": {
|
|
3442
|
+
"bytes_read": len(response_text.encode("utf-8", errors="replace")),
|
|
3443
|
+
"truncated": False,
|
|
3444
|
+
"max_bytes": LOCAL_PROXY_MAX_FORWARD_BYTES,
|
|
3445
|
+
},
|
|
3446
|
+
"stored_output": {
|
|
3447
|
+
"bytes": content_bytes,
|
|
3448
|
+
"lines": total_lines,
|
|
3449
|
+
"sha256": content_sha,
|
|
3450
|
+
"sanitized_text_sha256": content_sha,
|
|
3451
|
+
"content_file": content_path.name,
|
|
3452
|
+
"metadata_file": meta_path.name,
|
|
3453
|
+
"scope": LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE,
|
|
3454
|
+
},
|
|
3455
|
+
"digest": artifact.build_digest(
|
|
3456
|
+
sanitized_text,
|
|
3457
|
+
artifact_id=artifact_id,
|
|
3458
|
+
redacted_lines=redacted_lines,
|
|
3459
|
+
raw_dir=artifact_dir,
|
|
3460
|
+
show_paths=show_artifact_paths,
|
|
3461
|
+
),
|
|
3462
|
+
"retrieval": {
|
|
3463
|
+
"strategy": strategy,
|
|
3464
|
+
"deterministic": True,
|
|
3465
|
+
"hints": artifact.build_retrieval_hints(
|
|
3466
|
+
artifact_id,
|
|
3467
|
+
sanitized_text,
|
|
3468
|
+
content_type=content_type,
|
|
3469
|
+
strategy=strategy,
|
|
3470
|
+
total_lines=total_lines,
|
|
3471
|
+
raw_dir=artifact_dir,
|
|
3472
|
+
show_paths=show_artifact_paths,
|
|
3473
|
+
),
|
|
3474
|
+
},
|
|
3475
|
+
}
|
|
3476
|
+
artifact.shrink_digest_for_metadata_cap(metadata)
|
|
3477
|
+
artifact.write_private_text(content_path, sanitized_text)
|
|
3478
|
+
artifact.write_private_text(meta_path, artifact.metadata_json_text(metadata))
|
|
3479
|
+
return artifact.receipt_for(metadata, raw_dir=artifact_dir, show_paths=show_artifact_paths)
|
|
3480
|
+
|
|
3481
|
+
|
|
3482
|
+
def minimized_local_proxy_rehydration(receipt: dict[str, Any]) -> dict[str, Any]:
|
|
3483
|
+
sandbox = receipt.get("output_sandbox")
|
|
3484
|
+
rehydration = sandbox.get("rehydration") if isinstance(sandbox, dict) else None
|
|
3485
|
+
commands = rehydration.get("commands") if isinstance(rehydration, dict) else None
|
|
3486
|
+
kept_commands: list[dict[str, Any]] = []
|
|
3487
|
+
if isinstance(commands, list):
|
|
3488
|
+
for command in commands[:5]:
|
|
3489
|
+
if not isinstance(command, dict) or not isinstance(command.get("cli"), str):
|
|
3490
|
+
continue
|
|
3491
|
+
kept_commands.append({
|
|
3492
|
+
key: command[key]
|
|
3493
|
+
for key in ("type", "selector", "cli", "exact", "note")
|
|
3494
|
+
if key in command
|
|
3495
|
+
})
|
|
3496
|
+
return {
|
|
3497
|
+
"commands": kept_commands,
|
|
3498
|
+
"dir_argument": rehydration.get("dir_argument") if isinstance(rehydration, dict) else None,
|
|
3499
|
+
"exact_commands": rehydration.get("exact_commands") if isinstance(rehydration, dict) else None,
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
|
|
3503
|
+
def local_proxy_response_sandbox_envelope(
|
|
3504
|
+
*,
|
|
3505
|
+
receipt: dict[str, Any],
|
|
3506
|
+
upstream_status: int,
|
|
3507
|
+
upstream_response_bytes: int,
|
|
3508
|
+
content_type: str,
|
|
3509
|
+
) -> dict[str, Any]:
|
|
3510
|
+
sandbox = receipt.get("output_sandbox")
|
|
3511
|
+
handle = sandbox.get("handle") if isinstance(sandbox, dict) else f"contextguard-artifact:{receipt.get('artifact_id')}"
|
|
3512
|
+
stored = receipt.get("stored_output")
|
|
3513
|
+
stored_output = stored if isinstance(stored, dict) else {}
|
|
3514
|
+
return {
|
|
3515
|
+
"schema_version": LOCAL_PROXY_RESPONSE_SANDBOX_SCHEMA_VERSION,
|
|
3516
|
+
"status": "response_sandboxed",
|
|
3517
|
+
"mode": "local_proxy_response_sandbox",
|
|
3518
|
+
"artifact_id": receipt.get("artifact_id"),
|
|
3519
|
+
"artifact_handle": handle,
|
|
3520
|
+
"upstream": {
|
|
3521
|
+
"status": upstream_status,
|
|
3522
|
+
"response_bytes": upstream_response_bytes,
|
|
3523
|
+
"content_type": content_type,
|
|
3524
|
+
},
|
|
3525
|
+
"stored_output": {
|
|
3526
|
+
"scope": LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE,
|
|
3527
|
+
"bytes": stored_output.get("bytes"),
|
|
3528
|
+
"lines": stored_output.get("lines"),
|
|
3529
|
+
"sanitized_text_sha256": stored_output.get("sanitized_text_sha256") or stored_output.get("sha256"),
|
|
3530
|
+
},
|
|
3531
|
+
"rehydration": minimized_local_proxy_rehydration(receipt),
|
|
3532
|
+
"agent_guidance": [
|
|
3533
|
+
"Keep this compact local proxy response envelope in context instead of the full response body.",
|
|
3534
|
+
"Use rehydration.commands to retrieve exact sanitized UTF-8 response slices before relying on omitted details.",
|
|
3535
|
+
],
|
|
3536
|
+
"claim_boundary": {
|
|
3537
|
+
"local_only": True,
|
|
3538
|
+
"stored_content_is_sanitized_utf8_text": True,
|
|
3539
|
+
"original_http_bytes_rehydration_available": False,
|
|
3540
|
+
"hosted_api_token_or_cost_savings_claim_allowed": False,
|
|
3541
|
+
"provider_measured_matched_tasks_required_for_hosted_claims": True,
|
|
3542
|
+
},
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
|
|
3546
|
+
def write_local_proxy_ready_file(path: str | None, *, bind_host: str, bind_port: int, auth_nonce: str) -> None:
|
|
3244
3547
|
if not path:
|
|
3245
3548
|
return
|
|
3246
3549
|
ready_payload = {
|
|
@@ -3254,6 +3557,14 @@ def write_local_proxy_ready_file(path: str | None, *, bind_host: str, bind_port:
|
|
|
3254
3557
|
"host": bind_host,
|
|
3255
3558
|
"port": bind_port,
|
|
3256
3559
|
},
|
|
3560
|
+
"client_auth": {
|
|
3561
|
+
"required": True,
|
|
3562
|
+
"type": "nonce_header",
|
|
3563
|
+
"header": LOCAL_PROXY_NONCE_HEADER,
|
|
3564
|
+
"nonce": auth_nonce,
|
|
3565
|
+
"forwarded_upstream": False,
|
|
3566
|
+
"public_output": False,
|
|
3567
|
+
},
|
|
3257
3568
|
}
|
|
3258
3569
|
data = json.dumps(ready_payload, sort_keys=True).encode("utf-8") + b"\n"
|
|
3259
3570
|
write_regular_file_no_follow_exclusive(Path(path), data, label="local proxy ready file", mode=0o600)
|
|
@@ -3268,6 +3579,11 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3268
3579
|
max_request_bytes = int(limits["max_request_bytes"])
|
|
3269
3580
|
max_response_bytes = int(limits["max_response_bytes"])
|
|
3270
3581
|
timeout_seconds = float(limits["timeout_seconds"])
|
|
3582
|
+
response_sandbox = payload.get("response_sandbox")
|
|
3583
|
+
response_sandbox_enabled = bool(response_sandbox.get("enabled")) if isinstance(response_sandbox, dict) else False
|
|
3584
|
+
response_artifact_dir = str(payload.get("_response_artifact_dir_write_path") or DEFAULT_CONTEXT_DIFF_ARTIFACT_DIR)
|
|
3585
|
+
response_show_artifact_paths = bool(payload.get("_response_artifact_show_paths"))
|
|
3586
|
+
auth_nonce = secrets.token_urlsafe(32)
|
|
3271
3587
|
server_result: dict[str, Any] = {
|
|
3272
3588
|
"served_once": False,
|
|
3273
3589
|
"forwarded": False,
|
|
@@ -3283,16 +3599,37 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3283
3599
|
"sensitive_headers_blocked": [],
|
|
3284
3600
|
"listener_started": False,
|
|
3285
3601
|
"ready_file_written": False,
|
|
3602
|
+
"client_auth_required": True,
|
|
3603
|
+
"client_auth_header": LOCAL_PROXY_NONCE_HEADER,
|
|
3604
|
+
"client_auth_delivered": False,
|
|
3605
|
+
"client_auth_nonce_forwarded": False,
|
|
3606
|
+
"auth_failures": 0,
|
|
3607
|
+
"response_sandbox_requested": response_sandbox_enabled,
|
|
3608
|
+
"response_sandboxed": False,
|
|
3609
|
+
"response_artifact_id": None,
|
|
3610
|
+
"response_artifact_handle": None,
|
|
3611
|
+
"response_envelope_bytes": 0,
|
|
3612
|
+
"sanitized_text_sha256": None,
|
|
3613
|
+
"response_text_policy": "utf8_text_only" if response_sandbox_enabled else None,
|
|
3286
3614
|
}
|
|
3287
3615
|
|
|
3288
|
-
def finish_blocked(
|
|
3289
|
-
|
|
3290
|
-
|
|
3616
|
+
def finish_blocked(
|
|
3617
|
+
handler: BaseHTTPRequestHandler,
|
|
3618
|
+
status_code: int,
|
|
3619
|
+
reason: str,
|
|
3620
|
+
*,
|
|
3621
|
+
sensitive: list[str] | None = None,
|
|
3622
|
+
consume_once: bool = True,
|
|
3623
|
+
) -> None:
|
|
3624
|
+
updates = {
|
|
3291
3625
|
"forwarded": False,
|
|
3292
3626
|
"blocked_reason": reason,
|
|
3293
3627
|
"downstream_status": status_code,
|
|
3294
3628
|
"sensitive_headers_blocked": sorted(set(sensitive or [])),
|
|
3295
|
-
}
|
|
3629
|
+
}
|
|
3630
|
+
if consume_once:
|
|
3631
|
+
updates["served_once"] = True
|
|
3632
|
+
server_result.update(updates)
|
|
3296
3633
|
body = json.dumps({"status": "blocked", "reason": reason}, sort_keys=True).encode("utf-8")
|
|
3297
3634
|
handler.send_response(status_code)
|
|
3298
3635
|
handler.send_header("Content-Type", "application/json")
|
|
@@ -3309,9 +3646,28 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3309
3646
|
def log_message(self, format: str, *args: Any) -> None: # noqa: A002 - BaseHTTPRequestHandler API.
|
|
3310
3647
|
return
|
|
3311
3648
|
|
|
3649
|
+
def authorize_request(self) -> bool:
|
|
3650
|
+
values = self.headers.get_all(LOCAL_PROXY_NONCE_HEADER, [])
|
|
3651
|
+
if len(values) == 0:
|
|
3652
|
+
server_result["auth_failures"] = int(server_result.get("auth_failures", 0)) + 1
|
|
3653
|
+
finish_blocked(self, 403, "missing_proxy_nonce", consume_once=False)
|
|
3654
|
+
return False
|
|
3655
|
+
if len(values) != 1:
|
|
3656
|
+
server_result["auth_failures"] = int(server_result.get("auth_failures", 0)) + 1
|
|
3657
|
+
finish_blocked(self, 403, "duplicate_proxy_nonce", consume_once=False)
|
|
3658
|
+
return False
|
|
3659
|
+
candidate = str(values[0])
|
|
3660
|
+
if not secrets.compare_digest(candidate, auth_nonce):
|
|
3661
|
+
server_result["auth_failures"] = int(server_result.get("auth_failures", 0)) + 1
|
|
3662
|
+
finish_blocked(self, 403, "invalid_proxy_nonce", consume_once=False)
|
|
3663
|
+
return False
|
|
3664
|
+
return True
|
|
3665
|
+
|
|
3312
3666
|
def do_CONNECT(self) -> None:
|
|
3313
3667
|
server_result["request_method"] = "CONNECT"
|
|
3314
3668
|
server_result.update(local_proxy_request_target_meta(self.path))
|
|
3669
|
+
if not self.authorize_request():
|
|
3670
|
+
return
|
|
3315
3671
|
finish_blocked(self, 405, "connect_tunneling_not_allowed")
|
|
3316
3672
|
|
|
3317
3673
|
def do_HEAD(self) -> None:
|
|
@@ -3332,6 +3688,8 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3332
3688
|
def block_method(self) -> None:
|
|
3333
3689
|
server_result["request_method"] = self.command
|
|
3334
3690
|
server_result.update(local_proxy_request_target_meta(self.path))
|
|
3691
|
+
if not self.authorize_request():
|
|
3692
|
+
return
|
|
3335
3693
|
finish_blocked(self, 405, "method_not_allowed")
|
|
3336
3694
|
|
|
3337
3695
|
def do_DELETE(self) -> None:
|
|
@@ -3346,6 +3704,11 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3346
3704
|
def forward_request(self) -> None:
|
|
3347
3705
|
server_result["request_method"] = self.command
|
|
3348
3706
|
server_result.update(local_proxy_request_target_meta(self.path))
|
|
3707
|
+
if not self.authorize_request():
|
|
3708
|
+
return
|
|
3709
|
+
if response_sandbox_enabled and self.command != "GET":
|
|
3710
|
+
finish_blocked(self, 405, "response_sandbox_get_only")
|
|
3711
|
+
return
|
|
3349
3712
|
if local_proxy_secret_like(self.path):
|
|
3350
3713
|
finish_blocked(self, 400, "secret_like_request_target")
|
|
3351
3714
|
return
|
|
@@ -3392,14 +3755,54 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3392
3755
|
if local_proxy_bytes_secret_like(response_body):
|
|
3393
3756
|
finish_blocked(self, 502, "upstream_response_sensitive_content_blocked")
|
|
3394
3757
|
return
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3758
|
+
if response_sandbox_enabled:
|
|
3759
|
+
content_type = str(response.headers.get("Content-Type", ""))
|
|
3760
|
+
response_text, blocked_reason, normalized_content_type = local_proxy_response_text_policy(response_body, content_type)
|
|
3761
|
+
if response_text is None:
|
|
3762
|
+
finish_blocked(self, 502, blocked_reason or "response_sandbox_text_required")
|
|
3763
|
+
return
|
|
3764
|
+
try:
|
|
3765
|
+
receipt = local_proxy_store_response_artifact(
|
|
3766
|
+
response_text,
|
|
3767
|
+
artifact_dir=response_artifact_dir,
|
|
3768
|
+
show_artifact_paths=response_show_artifact_paths,
|
|
3769
|
+
upstream_status=int(response.status),
|
|
3770
|
+
)
|
|
3771
|
+
envelope = local_proxy_response_sandbox_envelope(
|
|
3772
|
+
receipt=receipt,
|
|
3773
|
+
upstream_status=int(response.status),
|
|
3774
|
+
upstream_response_bytes=len(response_body),
|
|
3775
|
+
content_type=normalized_content_type,
|
|
3776
|
+
)
|
|
3777
|
+
envelope_body = json.dumps(envelope, ensure_ascii=False, sort_keys=True).encode("utf-8") + b"\n"
|
|
3778
|
+
except (OSError, RuntimeError, ValueError, RegistryError, json.JSONDecodeError) as exc:
|
|
3779
|
+
finish_blocked(self, 502, "response_sandbox_artifact_store_failed")
|
|
3780
|
+
server_result["error"] = sanitize_local_proxy_value(str(exc))
|
|
3781
|
+
return
|
|
3782
|
+
self.send_response(response.status, response.reason)
|
|
3783
|
+
self.send_header("Content-Type", "application/json")
|
|
3784
|
+
self.send_header("Content-Length", str(len(envelope_body)))
|
|
3785
|
+
self.send_header("Connection", "close")
|
|
3786
|
+
self.end_headers()
|
|
3787
|
+
self.wfile.write(envelope_body)
|
|
3788
|
+
stored = receipt.get("stored_output") if isinstance(receipt, dict) else {}
|
|
3789
|
+
sandbox = receipt.get("output_sandbox") if isinstance(receipt, dict) else {}
|
|
3790
|
+
server_result.update({
|
|
3791
|
+
"response_sandboxed": True,
|
|
3792
|
+
"response_artifact_id": receipt.get("artifact_id"),
|
|
3793
|
+
"response_artifact_handle": sandbox.get("handle") if isinstance(sandbox, dict) else None,
|
|
3794
|
+
"response_envelope_bytes": len(envelope_body),
|
|
3795
|
+
"sanitized_text_sha256": stored.get("sanitized_text_sha256") or stored.get("sha256") if isinstance(stored, dict) else None,
|
|
3796
|
+
})
|
|
3797
|
+
else:
|
|
3798
|
+
self.send_response(response.status, response.reason)
|
|
3799
|
+
for header_name, header_value in local_proxy_response_headers(response.headers):
|
|
3800
|
+
self.send_header(header_name, header_value)
|
|
3801
|
+
self.send_header("Content-Length", str(len(response_body)))
|
|
3802
|
+
self.send_header("Connection", "close")
|
|
3803
|
+
self.end_headers()
|
|
3804
|
+
if self.command != "HEAD":
|
|
3805
|
+
self.wfile.write(response_body)
|
|
3403
3806
|
server_result.update({
|
|
3404
3807
|
"served_once": True,
|
|
3405
3808
|
"forwarded": True,
|
|
@@ -3435,8 +3838,9 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3435
3838
|
httpd.timeout = timeout_seconds
|
|
3436
3839
|
try:
|
|
3437
3840
|
try:
|
|
3438
|
-
write_local_proxy_ready_file(ready_file, bind_host=bind_host, bind_port=bind_port)
|
|
3841
|
+
write_local_proxy_ready_file(ready_file, bind_host=bind_host, bind_port=bind_port, auth_nonce=auth_nonce)
|
|
3439
3842
|
server_result["ready_file_written"] = bool(ready_file)
|
|
3843
|
+
server_result["client_auth_delivered"] = bool(ready_file)
|
|
3440
3844
|
server_result["listener_started"] = True
|
|
3441
3845
|
except RegistryError as exc:
|
|
3442
3846
|
server_result.update({
|
|
@@ -3447,8 +3851,14 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3447
3851
|
"error": sanitize_local_proxy_value(str(exc)),
|
|
3448
3852
|
})
|
|
3449
3853
|
return server_result
|
|
3450
|
-
|
|
3451
|
-
|
|
3854
|
+
deadline = time.monotonic() + timeout_seconds
|
|
3855
|
+
while not server_result["served_once"]:
|
|
3856
|
+
remaining = deadline - time.monotonic()
|
|
3857
|
+
if remaining <= 0:
|
|
3858
|
+
break
|
|
3859
|
+
httpd.timeout = max(0.001, min(timeout_seconds, remaining))
|
|
3860
|
+
httpd.handle_request()
|
|
3861
|
+
if not server_result["served_once"] and not server_result.get("blocked_reason"):
|
|
3452
3862
|
server_result.update({
|
|
3453
3863
|
"blocked_reason": "timeout_waiting_for_request",
|
|
3454
3864
|
"downstream_status": None,
|
|
@@ -3461,6 +3871,10 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3461
3871
|
def command_serve_local_proxy(args: argparse.Namespace) -> int:
|
|
3462
3872
|
payload = local_proxy_forward_payload(args)
|
|
3463
3873
|
diagnostic_ledger = payload.get("diagnostic_ledger") if isinstance(payload.get("diagnostic_ledger"), dict) else {}
|
|
3874
|
+
if payload["status"] == "ready_to_serve" and not args.ready_file:
|
|
3875
|
+
payload["status"] = "blocked_until_local_proxy_forwarding_ready"
|
|
3876
|
+
payload["review_plan"]["readiness_blockers"].append("missing_ready_file_for_proxy_nonce")
|
|
3877
|
+
diagnostic_ledger["reason"] = "not_forwarded" if diagnostic_ledger.get("write_requested") else diagnostic_ledger.get("reason")
|
|
3464
3878
|
if payload["status"] == "ready_to_serve" and diagnostic_ledger.get("write_requested"):
|
|
3465
3879
|
try:
|
|
3466
3880
|
preflight_append_jsonl_no_follow(
|
|
@@ -3481,6 +3895,12 @@ def command_serve_local_proxy(args: argparse.Namespace) -> int:
|
|
|
3481
3895
|
payload["network_actions"]["external_services_called"] = False
|
|
3482
3896
|
payload["policy"]["listener_started"] = bool(result.get("listener_started"))
|
|
3483
3897
|
payload["policy"]["traffic_forwarded"] = bool(result["forwarded"])
|
|
3898
|
+
if isinstance(payload.get("response_sandbox"), dict):
|
|
3899
|
+
payload["response_sandbox"]["performed"] = bool(result.get("response_sandboxed"))
|
|
3900
|
+
payload["response_sandbox"]["artifact_id"] = result.get("response_artifact_id")
|
|
3901
|
+
payload["response_sandbox"]["artifact_handle"] = result.get("response_artifact_handle")
|
|
3902
|
+
payload["response_sandbox"]["sanitized_text_sha256"] = result.get("sanitized_text_sha256")
|
|
3903
|
+
payload["response_sandbox"]["envelope_bytes"] = result.get("response_envelope_bytes", 0)
|
|
3484
3904
|
if result["forwarded"]:
|
|
3485
3905
|
payload["status"] = "served_once"
|
|
3486
3906
|
elif result.get("blocked_reason") == "ready_file_write_failed":
|
|
@@ -3490,6 +3910,8 @@ def command_serve_local_proxy(args: argparse.Namespace) -> int:
|
|
|
3490
3910
|
payload["status"] = "blocked_request"
|
|
3491
3911
|
maybe_write_local_proxy_forward_diagnostic(payload)
|
|
3492
3912
|
payload.pop("_diagnostic_ledger_write_path", None)
|
|
3913
|
+
payload.pop("_response_artifact_dir_write_path", None)
|
|
3914
|
+
payload.pop("_response_artifact_show_paths", None)
|
|
3493
3915
|
if args.json:
|
|
3494
3916
|
emit_json(payload)
|
|
3495
3917
|
else:
|
|
@@ -4267,6 +4689,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4267
4689
|
"--diagnostic-ledger-jsonl",
|
|
4268
4690
|
help="Append one shifted-cost diagnostic JSONL row only after a successful loopback forwarded request.",
|
|
4269
4691
|
)
|
|
4692
|
+
serve_local_proxy.add_argument(
|
|
4693
|
+
"--response-sandbox",
|
|
4694
|
+
action="store_true",
|
|
4695
|
+
help="Return a compact JSON response envelope and store the sanitized upstream response body as a local artifact receipt.",
|
|
4696
|
+
)
|
|
4697
|
+
serve_local_proxy.add_argument(
|
|
4698
|
+
"--response-artifact-dir",
|
|
4699
|
+
default=None,
|
|
4700
|
+
help="Artifact directory for --response-sandbox receipts; defaults to .context-guard/artifacts.",
|
|
4701
|
+
)
|
|
4702
|
+
serve_local_proxy.add_argument(
|
|
4703
|
+
"--show-artifact-paths",
|
|
4704
|
+
action="store_true",
|
|
4705
|
+
help="Include exact local artifact paths in response-sandbox rehydration commands; otherwise custom dirs are redacted.",
|
|
4706
|
+
)
|
|
4270
4707
|
serve_local_proxy.add_argument("--api-key", help="Blocked/redacted API key material; never persisted or emitted raw.")
|
|
4271
4708
|
serve_local_proxy.add_argument("--authorization-header", help="Blocked/redacted Authorization header; never persisted or emitted raw.")
|
|
4272
4709
|
serve_local_proxy.add_argument("--persist-api-key", action="store_true", help="Declare API-key persistence intent; blocked.")
|