@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
|
@@ -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
|
|
@@ -64,6 +66,7 @@ LOCAL_PROXY_FORWARD_SCHEMA_VERSION = "contextguard.experiments.local-proxy-forwa
|
|
|
64
66
|
LOCAL_PROXY_DIAGNOSTIC_SCHEMA_VERSION = "contextguard.experiments.local-proxy-forward-diagnostic.v1"
|
|
65
67
|
LOCAL_PROXY_READY_SCHEMA_VERSION = "contextguard.experiments.local-proxy-ready.v1"
|
|
66
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"
|
|
67
70
|
LOCAL_PROXY_DEFAULT_BIND_HOST = "127.0.0.1"
|
|
68
71
|
LOCAL_PROXY_DEFAULT_BIND_PORT = 0
|
|
69
72
|
LOCAL_PROXY_DEFAULT_TARGET_HOST = "127.0.0.1"
|
|
@@ -76,6 +79,7 @@ LOCAL_PROXY_DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024
|
|
|
76
79
|
LOCAL_PROXY_MAX_FORWARD_BYTES = 2 * 1024 * 1024
|
|
77
80
|
LOCAL_PROXY_DEFAULT_TIMEOUT_SECONDS = 5.0
|
|
78
81
|
LOCAL_PROXY_MAX_TIMEOUT_SECONDS = 30.0
|
|
82
|
+
LOCAL_PROXY_RESPONSE_SANDBOX_SCOPE = "local_proxy_sanitized_response_body"
|
|
79
83
|
LOCAL_PROXY_EXTERNAL_ALLOWED_SCHEMES = {"https"}
|
|
80
84
|
LOCAL_PROXY_EXTERNAL_CREDENTIAL_REDACTION_POLICY = "strip-sensitive-headers"
|
|
81
85
|
LOCAL_PROXY_EXTERNAL_PROVIDER_EVIDENCE_BOUNDARY = "diagnostic-only-provider-measured-required"
|
|
@@ -2646,6 +2650,9 @@ def read_local_proxy_payload(args: argparse.Namespace) -> tuple[dict[str, Any],
|
|
|
2646
2650
|
"max_response_bytes",
|
|
2647
2651
|
"timeout_seconds",
|
|
2648
2652
|
"diagnostic_ledger_jsonl",
|
|
2653
|
+
"response_sandbox",
|
|
2654
|
+
"response_artifact_dir",
|
|
2655
|
+
"show_artifact_paths",
|
|
2649
2656
|
}
|
|
2650
2657
|
ignored.extend(sanitize_self_hosted_ignored_key(key) for key in envelope if key not in allowed)
|
|
2651
2658
|
return dict(envelope), {
|
|
@@ -3038,8 +3045,28 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3038
3045
|
"diagnostic_ledger_jsonl",
|
|
3039
3046
|
"diagnostic_ledger_jsonl",
|
|
3040
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)
|
|
3041
3066
|
diagnostic_ledger_path = sanitize_local_proxy_value(diagnostic_ledger_raw) if diagnostic_ledger_raw else None
|
|
3042
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)
|
|
3043
3070
|
bind_host = payload["bind"]["host"]
|
|
3044
3071
|
target_host = payload["target"]["host"]
|
|
3045
3072
|
bind_ip_literal = is_loopback_ip_literal(bind_host)
|
|
@@ -3070,6 +3097,7 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3070
3097
|
"listener_started": False,
|
|
3071
3098
|
"traffic_forwarded": False,
|
|
3072
3099
|
"stable_runtime_behavior_changed": False,
|
|
3100
|
+
"response_sandbox_enabled": response_sandbox,
|
|
3073
3101
|
})
|
|
3074
3102
|
payload["forwarding"] = dict(payload["forwarding"])
|
|
3075
3103
|
payload["forwarding"].update({
|
|
@@ -3094,6 +3122,30 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3094
3122
|
"bytes_written": 0,
|
|
3095
3123
|
"reason": None if diagnostic_ledger_raw else "not_requested",
|
|
3096
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
|
+
}
|
|
3097
3149
|
payload["client_auth"] = {
|
|
3098
3150
|
"required": True,
|
|
3099
3151
|
"type": "nonce_header",
|
|
@@ -3104,6 +3156,8 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3104
3156
|
"nonce_forwarded_upstream": False,
|
|
3105
3157
|
}
|
|
3106
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
|
|
3107
3161
|
payload["forward_result"] = None
|
|
3108
3162
|
|
|
3109
3163
|
blockers = list(payload["review_plan"]["readiness_blockers"])
|
|
@@ -3135,6 +3189,10 @@ def local_proxy_forward_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3135
3189
|
blockers.append("invalid_max_response_bytes")
|
|
3136
3190
|
if not timeout_valid:
|
|
3137
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")
|
|
3138
3196
|
blockers = list(dict.fromkeys(blockers))
|
|
3139
3197
|
payload["review_plan"]["readiness_blockers"] = blockers
|
|
3140
3198
|
payload["review_plan"]["next_steps"] = [
|
|
@@ -3173,6 +3231,10 @@ def local_proxy_forward_diagnostic_row(payload: dict[str, Any]) -> dict[str, Any
|
|
|
3173
3231
|
"upstream_status": result.get("upstream_status"),
|
|
3174
3232
|
"upstream_response_bytes": result.get("upstream_response_bytes", 0),
|
|
3175
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"),
|
|
3176
3238
|
},
|
|
3177
3239
|
"runtime_limits": payload["runtime_limits"],
|
|
3178
3240
|
"network_actions": payload["network_actions"],
|
|
@@ -3186,6 +3248,7 @@ def local_proxy_forward_diagnostic_row(payload: dict[str, Any]) -> dict[str, Any
|
|
|
3186
3248
|
"dns_lookup_attempted": False,
|
|
3187
3249
|
"connect_tunneling_allowed": False,
|
|
3188
3250
|
"https_mitm_allowed": False,
|
|
3251
|
+
"response_sandboxed": bool(result.get("response_sandboxed")),
|
|
3189
3252
|
"hosted_api_token_savings_claim_allowed": False,
|
|
3190
3253
|
"hosted_api_cost_savings_claim_allowed": False,
|
|
3191
3254
|
},
|
|
@@ -3264,6 +3327,222 @@ def local_proxy_response_headers(headers: Any) -> list[tuple[str, str]]:
|
|
|
3264
3327
|
return result
|
|
3265
3328
|
|
|
3266
3329
|
|
|
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
|
+
|
|
3267
3546
|
def write_local_proxy_ready_file(path: str | None, *, bind_host: str, bind_port: int, auth_nonce: str) -> None:
|
|
3268
3547
|
if not path:
|
|
3269
3548
|
return
|
|
@@ -3300,6 +3579,10 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3300
3579
|
max_request_bytes = int(limits["max_request_bytes"])
|
|
3301
3580
|
max_response_bytes = int(limits["max_response_bytes"])
|
|
3302
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"))
|
|
3303
3586
|
auth_nonce = secrets.token_urlsafe(32)
|
|
3304
3587
|
server_result: dict[str, Any] = {
|
|
3305
3588
|
"served_once": False,
|
|
@@ -3321,6 +3604,13 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3321
3604
|
"client_auth_delivered": False,
|
|
3322
3605
|
"client_auth_nonce_forwarded": False,
|
|
3323
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,
|
|
3324
3614
|
}
|
|
3325
3615
|
|
|
3326
3616
|
def finish_blocked(
|
|
@@ -3416,6 +3706,9 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3416
3706
|
server_result.update(local_proxy_request_target_meta(self.path))
|
|
3417
3707
|
if not self.authorize_request():
|
|
3418
3708
|
return
|
|
3709
|
+
if response_sandbox_enabled and self.command != "GET":
|
|
3710
|
+
finish_blocked(self, 405, "response_sandbox_get_only")
|
|
3711
|
+
return
|
|
3419
3712
|
if local_proxy_secret_like(self.path):
|
|
3420
3713
|
finish_blocked(self, 400, "secret_like_request_target")
|
|
3421
3714
|
return
|
|
@@ -3462,14 +3755,54 @@ def serve_local_proxy_once(payload: dict[str, Any], *, ready_file: str | None =
|
|
|
3462
3755
|
if local_proxy_bytes_secret_like(response_body):
|
|
3463
3756
|
finish_blocked(self, 502, "upstream_response_sensitive_content_blocked")
|
|
3464
3757
|
return
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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)
|
|
3473
3806
|
server_result.update({
|
|
3474
3807
|
"served_once": True,
|
|
3475
3808
|
"forwarded": True,
|
|
@@ -3562,6 +3895,12 @@ def command_serve_local_proxy(args: argparse.Namespace) -> int:
|
|
|
3562
3895
|
payload["network_actions"]["external_services_called"] = False
|
|
3563
3896
|
payload["policy"]["listener_started"] = bool(result.get("listener_started"))
|
|
3564
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)
|
|
3565
3904
|
if result["forwarded"]:
|
|
3566
3905
|
payload["status"] = "served_once"
|
|
3567
3906
|
elif result.get("blocked_reason") == "ready_file_write_failed":
|
|
@@ -3571,6 +3910,8 @@ def command_serve_local_proxy(args: argparse.Namespace) -> int:
|
|
|
3571
3910
|
payload["status"] = "blocked_request"
|
|
3572
3911
|
maybe_write_local_proxy_forward_diagnostic(payload)
|
|
3573
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)
|
|
3574
3915
|
if args.json:
|
|
3575
3916
|
emit_json(payload)
|
|
3576
3917
|
else:
|
|
@@ -4348,6 +4689,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
4348
4689
|
"--diagnostic-ledger-jsonl",
|
|
4349
4690
|
help="Append one shifted-cost diagnostic JSONL row only after a successful loopback forwarded request.",
|
|
4350
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
|
+
)
|
|
4351
4707
|
serve_local_proxy.add_argument("--api-key", help="Blocked/redacted API key material; never persisted or emitted raw.")
|
|
4352
4708
|
serve_local_proxy.add_argument("--authorization-header", help="Blocked/redacted Authorization header; never persisted or emitted raw.")
|
|
4353
4709
|
serve_local_proxy.add_argument("--persist-api-key", action="store_true", help="Declare API-key persistence intent; blocked.")
|
|
@@ -92,12 +92,16 @@ NUDGE_TEXT = (
|
|
|
92
92
|
"prompt cache 도 매 retry 마다 재워밍됨을 의미합니다. "
|
|
93
93
|
"재시도 전에 사용자에게 `/clear` 또는 `/compact focus on …` 으로 세션을 정리한 뒤 "
|
|
94
94
|
"재현 명령·기대 결과·금지 사항을 더 좁혀 다시 prompt 하도록 안내하거나, "
|
|
95
|
-
"근본적으로 다른 방향(다른 모듈 / 검증 명령 / 더 작은 재현)을 제안하세요."
|
|
95
|
+
"근본적으로 다른 방향(다른 모듈 / 검증 명령 / 더 작은 재현)을 제안하세요. "
|
|
96
|
+
"직전 출력에 artifact_receipt 또는 contextguard-artifact:<id> 핸들이 있으면, 전체 로그를 다시 붙여넣거나 "
|
|
97
|
+
"동일한 broad 명령을 재실행하기 전에 context-guard-artifact receipt/get/search 로 필요한 줄·패턴만 "
|
|
98
|
+
"정확히 rehydrate 하도록 우선 제안하세요."
|
|
96
99
|
)
|
|
97
100
|
STRATEGY_SWITCH_TEXT = (
|
|
98
101
|
" Strategy-switch signal: the same failure direction has now repeated at least three times. "
|
|
99
102
|
"Stop retrying the identical command path; summarize the invariant failure, choose a different hypothesis "
|
|
100
|
-
"or smaller reproducer,
|
|
103
|
+
"or smaller reproducer, rehydrate exact artifact receipt slices when available, "
|
|
104
|
+
"and only rerun after changing code, inputs, or diagnostic scope."
|
|
101
105
|
)
|
|
102
106
|
|
|
103
107
|
|