context-overlay 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,13 @@
1
+ """OpenAI-compatible context overlay proxy."""
2
+
3
+ from .config import ContextOverlayConfig, load_config
4
+ from .skills import Skill, SkillStore
5
+ from .transforms import apply_rules
6
+
7
+ __all__ = [
8
+ "ContextOverlayConfig",
9
+ "Skill",
10
+ "SkillStore",
11
+ "apply_rules",
12
+ "load_config",
13
+ ]
context_overlay/cli.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ import uvicorn
6
+
7
+ from .config import load_config
8
+ from .server import create_app
9
+
10
+
11
+ def main() -> None:
12
+ parser = argparse.ArgumentParser(prog="context-overlay")
13
+ subparsers = parser.add_subparsers(dest="command", required=True)
14
+
15
+ serve_parser = subparsers.add_parser("serve", help="Run the OpenAI-compatible context overlay proxy")
16
+ serve_parser.add_argument("--config", required=True, help="Path to YAML config")
17
+ serve_parser.add_argument("--host", default="127.0.0.1")
18
+ serve_parser.add_argument("--port", type=int, default=8011)
19
+ serve_parser.add_argument("--reload", action="store_true")
20
+
21
+ args = parser.parse_args()
22
+ if args.command == "serve":
23
+ config = load_config(args.config)
24
+ app = create_app(config)
25
+ uvicorn.run(app, host=args.host, port=args.port, reload=args.reload)
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Literal
6
+
7
+ import yaml
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class UpstreamConfig(BaseModel):
12
+ base_url: str
13
+ api_key: str | None = None
14
+ timeout_seconds: float = 600.0
15
+
16
+
17
+ class AuthConfig(BaseModel):
18
+ api_key: str | None = None
19
+
20
+
21
+ class MatchConfig(BaseModel):
22
+ path: str | None = None
23
+ model_regex: str | None = None
24
+ messages_regex: list[str] = Field(default_factory=list)
25
+ extra_body: dict[str, Any] = Field(default_factory=dict)
26
+
27
+
28
+ class ContentSourceConfig(BaseModel):
29
+ type: Literal["text", "file", "skill_dir"]
30
+ text: str | None = None
31
+ path: str | None = None
32
+ top_k: int = 3
33
+ max_chars: int = 24000
34
+ title: str = "Context Overlay"
35
+
36
+
37
+ class TransformConfig(BaseModel):
38
+ type: Literal[
39
+ "prepend_system",
40
+ "append_system",
41
+ "insert_before",
42
+ "insert_after",
43
+ "regex_replace",
44
+ "prepend_user",
45
+ "append_user",
46
+ "route",
47
+ "reject",
48
+ ]
49
+ target: Literal["system", "user"] = "system"
50
+ pattern: str | None = None
51
+ replacement: str | None = None
52
+ content: str | ContentSourceConfig | None = None
53
+ upstream_base_url: str | None = None
54
+ model: str | None = None
55
+ reason: str | None = None
56
+
57
+
58
+ class RuleConfig(BaseModel):
59
+ name: str
60
+ match: MatchConfig = Field(default_factory=MatchConfig)
61
+ transforms: list[TransformConfig] = Field(default_factory=list)
62
+
63
+
64
+ class ContextOverlayConfig(BaseModel):
65
+ upstream: UpstreamConfig
66
+ auth: AuthConfig = Field(default_factory=AuthConfig)
67
+ rules: list[RuleConfig] = Field(default_factory=list)
68
+
69
+
70
+ def _expand_env(value: Any) -> Any:
71
+ if isinstance(value, str):
72
+ return os.path.expandvars(value)
73
+ if isinstance(value, list):
74
+ return [_expand_env(item) for item in value]
75
+ if isinstance(value, dict):
76
+ return {key: _expand_env(item) for key, item in value.items()}
77
+ return value
78
+
79
+
80
+ def load_config(path: str | Path) -> ContextOverlayConfig:
81
+ config_path = Path(path)
82
+ raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
83
+ expanded = _expand_env(raw)
84
+ return ContextOverlayConfig.model_validate(expanded)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from .config import MatchConfig
7
+
8
+
9
+ def message_text(messages: list[dict[str, Any]]) -> str:
10
+ parts: list[str] = []
11
+ for message in messages:
12
+ content = message.get("content")
13
+ if isinstance(content, str):
14
+ parts.append(content)
15
+ elif isinstance(content, list):
16
+ for item in content:
17
+ if isinstance(item, dict) and item.get("type") == "text":
18
+ parts.append(str(item.get("text", "")))
19
+ return "\n".join(parts)
20
+
21
+
22
+ def _contains_extra_body(body: dict[str, Any], expected: dict[str, Any]) -> bool:
23
+ for key, value in expected.items():
24
+ if body.get(key) != value:
25
+ return False
26
+ return True
27
+
28
+
29
+ def request_matches(path: str, body: dict[str, Any], match: MatchConfig) -> bool:
30
+ if match.path and match.path != path:
31
+ return False
32
+ if match.model_regex:
33
+ model = str(body.get("model", ""))
34
+ if not re.search(match.model_regex, model):
35
+ return False
36
+ if match.extra_body and not _contains_extra_body(body, match.extra_body):
37
+ return False
38
+ if match.messages_regex:
39
+ text = message_text(body.get("messages") or [])
40
+ if not all(re.search(pattern, text, flags=re.IGNORECASE) for pattern in match.messages_regex):
41
+ return False
42
+ return True
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from fastapi import FastAPI, Header, HTTPException, Request
7
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
8
+
9
+ from .config import ContextOverlayConfig
10
+ from .transforms import apply_rules
11
+
12
+
13
+ def _check_auth(config: ContextOverlayConfig, authorization: str | None) -> None:
14
+ expected = config.auth.api_key
15
+ if not expected:
16
+ return
17
+ if authorization == f"Bearer {expected}":
18
+ return
19
+ raise HTTPException(status_code=401, detail="Invalid context-overlay API key")
20
+
21
+
22
+ def _upstream_url(config: ContextOverlayConfig, path: str, body: dict[str, Any] | None = None) -> str:
23
+ base_url = config.upstream.base_url.rstrip("/")
24
+ if body and body.get("_context_overlay_upstream_base_url"):
25
+ base_url = str(body["_context_overlay_upstream_base_url"]).rstrip("/")
26
+ suffix = path.removeprefix("/v1")
27
+ return base_url + suffix
28
+
29
+
30
+ def _forward_headers(config: ContextOverlayConfig, request: Request) -> dict[str, str]:
31
+ headers = {
32
+ key: value
33
+ for key, value in request.headers.items()
34
+ if key.lower() not in {"host", "content-length", "authorization"}
35
+ }
36
+ api_key = config.upstream.api_key
37
+ if api_key:
38
+ headers["authorization"] = f"Bearer {api_key}"
39
+ return headers
40
+
41
+
42
+ def create_app(config: ContextOverlayConfig) -> FastAPI:
43
+ app = FastAPI(title="context-overlay")
44
+
45
+ @app.get("/health")
46
+ async def health() -> dict[str, str]:
47
+ return {"status": "ok"}
48
+
49
+ @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
50
+ async def proxy(path: str, request: Request, authorization: str | None = Header(default=None)) -> Response:
51
+ _check_auth(config, authorization)
52
+ full_path = f"/v1/{path}"
53
+ method = request.method
54
+ headers = _forward_headers(config, request)
55
+ timeout = httpx.Timeout(config.upstream.timeout_seconds)
56
+
57
+ if method == "POST" and full_path == "/v1/chat/completions":
58
+ body = await request.json()
59
+ try:
60
+ body = apply_rules(body, config, path=full_path)
61
+ except PermissionError as exc:
62
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
63
+ except Exception as exc: # noqa: BLE001 - return explicit transform errors.
64
+ raise HTTPException(status_code=400, detail=f"context-overlay transform error: {exc}") from exc
65
+ stream = bool(body.get("stream"))
66
+ upstream_url = _upstream_url(config, full_path, body)
67
+ body.pop("_context_overlay_upstream_base_url", None)
68
+ if stream:
69
+ return await _stream_request(method, upstream_url, headers, json_body=body, timeout=timeout)
70
+ async with httpx.AsyncClient(timeout=timeout) as client:
71
+ response = await client.request(method, upstream_url, headers=headers, json=body)
72
+ return Response(
73
+ content=response.content,
74
+ status_code=response.status_code,
75
+ media_type=response.headers.get("content-type"),
76
+ )
77
+
78
+ upstream_url = _upstream_url(config, full_path)
79
+ content = await request.body()
80
+ async with httpx.AsyncClient(timeout=timeout) as client:
81
+ response = await client.request(method, upstream_url, headers=headers, content=content, params=request.query_params)
82
+ return Response(
83
+ content=response.content,
84
+ status_code=response.status_code,
85
+ media_type=response.headers.get("content-type"),
86
+ )
87
+
88
+ return app
89
+
90
+
91
+ async def _stream_request(
92
+ method: str,
93
+ url: str,
94
+ headers: dict[str, str],
95
+ json_body: dict[str, Any],
96
+ timeout: httpx.Timeout,
97
+ ) -> StreamingResponse:
98
+ client = httpx.AsyncClient(timeout=timeout)
99
+ request = client.build_request(method, url, headers=headers, json=json_body)
100
+ response = await client.send(request, stream=True)
101
+
102
+ async def iterator():
103
+ try:
104
+ async for chunk in response.aiter_bytes():
105
+ yield chunk
106
+ finally:
107
+ await response.aclose()
108
+ await client.aclose()
109
+
110
+ return StreamingResponse(
111
+ iterator(),
112
+ status_code=response.status_code,
113
+ media_type=response.headers.get("content-type", "text/event-stream"),
114
+ )
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ TOKEN_RE = re.compile(r"[A-Za-z0-9_+\-.]+")
11
+
12
+
13
+ def tokenize(text: str) -> set[str]:
14
+ return {token.lower() for token in TOKEN_RE.findall(text) if len(token) > 2}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Skill:
19
+ path: Path
20
+ name: str
21
+ description: str
22
+ content: str
23
+ category: str | None = None
24
+ score: int | None = None
25
+
26
+ @classmethod
27
+ def from_json_file(cls, path: Path) -> "Skill":
28
+ data = json.loads(path.read_text(encoding="utf-8"))
29
+ return cls(
30
+ path=path,
31
+ name=str(data.get("name") or path.stem),
32
+ description=str(data.get("description") or ""),
33
+ content=str(data.get("content") or ""),
34
+ category=data.get("category"),
35
+ score=data.get("score"),
36
+ )
37
+
38
+ def searchable_text(self) -> str:
39
+ return "\n".join(part for part in [self.name, self.description, self.category or "", self.content] if part)
40
+
41
+
42
+ class SkillStore:
43
+ def __init__(self, skills: list[Skill]) -> None:
44
+ self.skills = skills
45
+ self._tokens = [tokenize(skill.searchable_text()) for skill in skills]
46
+
47
+ @classmethod
48
+ def from_dir(cls, path: str | Path) -> "SkillStore":
49
+ root = Path(path)
50
+ skills = [Skill.from_json_file(item) for item in sorted(root.glob("*.json"))]
51
+ return cls(skills)
52
+
53
+ def retrieve(self, query: str, top_k: int = 3) -> list[Skill]:
54
+ query_tokens = tokenize(query)
55
+ if not query_tokens:
56
+ return self.skills[:top_k]
57
+ scored: list[tuple[float, int, Skill]] = []
58
+ for idx, (skill, skill_tokens) in enumerate(zip(self.skills, self._tokens)):
59
+ if not skill_tokens:
60
+ continue
61
+ overlap = len(query_tokens & skill_tokens)
62
+ if overlap == 0:
63
+ continue
64
+ score = overlap / (len(query_tokens) ** 0.5 * len(skill_tokens) ** 0.5)
65
+ scored.append((score, -idx, skill))
66
+ scored.sort(reverse=True)
67
+ return [skill for _, _, skill in scored[:top_k]]
68
+
69
+
70
+ def render_skills(skills: list[Skill], title: str = "Context Overlay", max_chars: int = 24000) -> str:
71
+ if not skills:
72
+ return ""
73
+ blocks = [f"# {title}", "Use the following planning context when relevant. Do not mention that it was injected."]
74
+ for idx, skill in enumerate(skills, 1):
75
+ blocks.append(f"\n## Skill {idx}: {skill.name}")
76
+ if skill.description:
77
+ blocks.append(f"Description: {skill.description}")
78
+ if skill.category:
79
+ blocks.append(f"Category: {skill.category}")
80
+ blocks.append(skill.content)
81
+ text = "\n".join(blocks).strip()
82
+ if len(text) <= max_chars:
83
+ return text
84
+ return text[: max_chars - 80].rstrip() + "\n\n[Context overlay truncated to fit the configured budget.]"
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .config import ContentSourceConfig, ContextOverlayConfig, TransformConfig
9
+ from .matching import message_text, request_matches
10
+ from .skills import SkillStore, render_skills
11
+
12
+
13
+ def ensure_messages(body: dict[str, Any]) -> list[dict[str, Any]]:
14
+ messages = body.setdefault("messages", [])
15
+ if not isinstance(messages, list):
16
+ raise ValueError("Request body field 'messages' must be a list")
17
+ return messages
18
+
19
+
20
+ def ensure_system_message(messages: list[dict[str, Any]]) -> dict[str, Any]:
21
+ for message in messages:
22
+ if message.get("role") == "system":
23
+ if not isinstance(message.get("content"), str):
24
+ message["content"] = str(message.get("content", ""))
25
+ return message
26
+ system = {"role": "system", "content": ""}
27
+ messages.insert(0, system)
28
+ return system
29
+
30
+
31
+ def ensure_last_user_message(messages: list[dict[str, Any]]) -> dict[str, Any]:
32
+ for message in reversed(messages):
33
+ if message.get("role") == "user":
34
+ if not isinstance(message.get("content"), str):
35
+ message["content"] = _content_to_text(message.get("content"))
36
+ return message
37
+ user = {"role": "user", "content": ""}
38
+ messages.append(user)
39
+ return user
40
+
41
+
42
+ def _content_to_text(content: Any) -> str:
43
+ if isinstance(content, str):
44
+ return content
45
+ if isinstance(content, list):
46
+ parts = []
47
+ for item in content:
48
+ if isinstance(item, dict) and item.get("type") == "text":
49
+ parts.append(str(item.get("text", "")))
50
+ return "\n".join(parts)
51
+ return str(content or "")
52
+
53
+
54
+ def resolve_content(content: str | ContentSourceConfig | None, body: dict[str, Any]) -> str:
55
+ if content is None:
56
+ return ""
57
+ if isinstance(content, str):
58
+ return content
59
+ if content.type == "text":
60
+ return content.text or ""
61
+ if content.type == "file":
62
+ if not content.path:
63
+ raise ValueError("file content source requires path")
64
+ return Path(content.path).read_text(encoding="utf-8")
65
+ if content.type == "skill_dir":
66
+ if not content.path:
67
+ raise ValueError("skill_dir content source requires path")
68
+ store = SkillStore.from_dir(content.path)
69
+ skills = store.retrieve(message_text(body.get("messages") or []), top_k=content.top_k)
70
+ return render_skills(skills, title=content.title, max_chars=content.max_chars)
71
+ raise ValueError(f"Unsupported content source: {content.type}")
72
+
73
+
74
+ def _join(prefix: str, suffix: str) -> str:
75
+ if not prefix:
76
+ return suffix
77
+ if not suffix:
78
+ return prefix
79
+ return prefix.rstrip() + "\n\n" + suffix.lstrip()
80
+
81
+
82
+ def apply_transform(body: dict[str, Any], transform: TransformConfig) -> dict[str, Any]:
83
+ messages = ensure_messages(body)
84
+ overlay = resolve_content(transform.content, body)
85
+ if transform.type == "reject":
86
+ raise PermissionError(transform.reason or "Request rejected by context-overlay rule")
87
+ if transform.type == "route":
88
+ if transform.model:
89
+ body["model"] = transform.model
90
+ if transform.upstream_base_url:
91
+ body["_context_overlay_upstream_base_url"] = transform.upstream_base_url
92
+ return body
93
+ if transform.type in {"prepend_system", "append_system", "insert_before", "insert_after", "regex_replace"}:
94
+ target = ensure_system_message(messages)
95
+ elif transform.type in {"prepend_user", "append_user"}:
96
+ target = ensure_last_user_message(messages)
97
+ else:
98
+ raise ValueError(f"Unsupported transform type: {transform.type}")
99
+
100
+ content = str(target.get("content") or "")
101
+ if transform.type in {"prepend_system", "prepend_user"}:
102
+ target["content"] = _join(overlay, content)
103
+ elif transform.type in {"append_system", "append_user"}:
104
+ target["content"] = _join(content, overlay)
105
+ elif transform.type == "insert_before":
106
+ if not transform.pattern:
107
+ target["content"] = _join(overlay, content)
108
+ else:
109
+ target["content"] = re.sub(transform.pattern, overlay + "\n\n" + r"\g<0>", content, count=1)
110
+ elif transform.type == "insert_after":
111
+ if not transform.pattern:
112
+ target["content"] = _join(content, overlay)
113
+ else:
114
+ target["content"] = re.sub(transform.pattern, r"\g<0>" + "\n\n" + overlay, content, count=1)
115
+ elif transform.type == "regex_replace":
116
+ if not transform.pattern:
117
+ raise ValueError("regex_replace requires pattern")
118
+ replacement = transform.replacement if transform.replacement is not None else overlay
119
+ target["content"] = re.sub(transform.pattern, replacement, content)
120
+ return body
121
+
122
+
123
+ def apply_rules(body: dict[str, Any], config: ContextOverlayConfig, path: str = "/v1/chat/completions") -> dict[str, Any]:
124
+ transformed = copy.deepcopy(body)
125
+ for rule in config.rules:
126
+ if not request_matches(path, transformed, rule.match):
127
+ continue
128
+ for transform in rule.transforms:
129
+ transformed = apply_transform(transformed, transform)
130
+ return transformed
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: context-overlay
3
+ Version: 0.0.1
4
+ Summary: OpenAI-compatible context overlay proxy for injecting skills, memory, policies, and prompt patches.
5
+ Author: InternScience
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/black-yt/context-overlay
8
+ Project-URL: Repository, https://github.com/black-yt/context-overlay
9
+ Project-URL: Issues, https://github.com/black-yt/context-overlay/issues
10
+ Keywords: llm,openai,proxy,prompt,context,middleware
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: fastapi>=0.110
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: pydantic>=2
24
+ Requires-Dist: PyYAML>=6
25
+ Requires-Dist: uvicorn[standard]>=0.27
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1; extra == "dev"
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: respx>=0.21; extra == "dev"
30
+ Requires-Dist: twine>=5; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # context-overlay
34
+
35
+ `context-overlay` is an OpenAI-compatible request proxy that injects additional context into chat-completion requests without changing the upstream model server.
36
+
37
+ It can be used for:
38
+
39
+ - skill injection from local JSON skills
40
+ - prompt overlays and prompt patches
41
+ - policy or profile insertion
42
+ - lightweight request routing
43
+ - public demos through tools such as `ngrok` or `cloudflared`
44
+
45
+ The package does not run an agent loop and does not execute tools. It only transforms OpenAI-compatible HTTP requests and forwards them to an upstream OpenAI-compatible endpoint.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install context-overlay
51
+ ```
52
+
53
+ For local development:
54
+
55
+ ```bash
56
+ pip install -e ".[dev]"
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ Create `config.yaml`:
62
+
63
+ ```yaml
64
+ upstream:
65
+ base_url: "http://127.0.0.1:8010/v1"
66
+ api_key: "unused"
67
+
68
+ auth:
69
+ api_key: "proxy-key"
70
+
71
+ rules:
72
+ - name: inject_science_skills
73
+ match:
74
+ path: "/v1/chat/completions"
75
+ messages_regex:
76
+ - "scientific"
77
+ transforms:
78
+ - type: append_system
79
+ content:
80
+ type: skill_dir
81
+ path: "./skills/generated_skills"
82
+ top_k: 3
83
+ max_chars: 24000
84
+ ```
85
+
86
+ Run:
87
+
88
+ ```bash
89
+ context-overlay serve --config config.yaml --host 127.0.0.1 --port 8011
90
+ ```
91
+
92
+ Use it with the OpenAI SDK:
93
+
94
+ ```python
95
+ from openai import OpenAI
96
+
97
+ client = OpenAI(api_key="proxy-key", base_url="http://127.0.0.1:8011/v1")
98
+
99
+ response = client.chat.completions.create(
100
+ model="Qwen3.5-9B",
101
+ messages=[{"role": "user", "content": "Analyze this scientific task."}],
102
+ )
103
+
104
+ print(response.choices[0].message.content)
105
+ ```
106
+
107
+ ## Public URL With ngrok
108
+
109
+ ```bash
110
+ context-overlay serve --config config.yaml --host 127.0.0.1 --port 8011
111
+ ngrok http 8011
112
+ ```
113
+
114
+ Then set the SDK base URL to the public ngrok URL plus `/v1`.
115
+
116
+ ## Public URL With cloudflared
117
+
118
+ Temporary URL:
119
+
120
+ ```bash
121
+ context-overlay serve --config config.yaml --host 127.0.0.1 --port 8011
122
+ cloudflared tunnel --url http://127.0.0.1:8011
123
+ ```
124
+
125
+ Long-running production use should use a named Cloudflare Tunnel and your own access policy.
126
+
127
+ ## Rule Model
128
+
129
+ Each rule has:
130
+
131
+ - `match`: decides whether a request should be transformed.
132
+ - `transforms`: one or more operations applied to the request body before forwarding.
133
+
134
+ Supported match fields:
135
+
136
+ - `path`
137
+ - `model_regex`
138
+ - `messages_regex`
139
+ - `extra_body`
140
+
141
+ Supported transform types:
142
+
143
+ - `prepend_system`
144
+ - `append_system`
145
+ - `insert_before`
146
+ - `insert_after`
147
+ - `regex_replace`
148
+ - `prepend_user`
149
+ - `append_user`
150
+ - `route`
151
+ - `reject`
152
+
153
+ Skill injection is implemented as a content source, not a special runtime mode.
154
+
155
+ ## Security Notes
156
+
157
+ - Use `auth.api_key` before exposing the proxy publicly.
158
+ - Keep upstream API keys on the server side.
159
+ - Do not put secrets in skill files or prompt overlays.
160
+ - Set upstream and client-side timeouts appropriate for your deployment.
@@ -0,0 +1,13 @@
1
+ context_overlay/__init__.py,sha256=_Cr5Y-Ruiv1dOJHuZC8y50IJiATRG2_rnDciGzHVGfU,288
2
+ context_overlay/cli.py,sha256=SCKtVgRlMtz5GDnyx0cJVdo7RoseivaEX2KXnLuUSF0,881
3
+ context_overlay/config.py,sha256=sTeKsI8sCBWBLGpt-wPxZYJsuVv0NiksTikbZ62-yGo,2235
4
+ context_overlay/matching.py,sha256=tI8ZVzz-pBO_u9tUkjyvbp6eQ7DWzLwHQmaSrKma20Y,1384
5
+ context_overlay/server.py,sha256=SJ9auIVd1Tf5ZLsWd6VDVv-qvFc-NPfo1TF2YL3tIek,4381
6
+ context_overlay/skills.py,sha256=3im0p793_vV2bhbU-P1wDD8hNHtV4v5c6pph3_-lI88,2925
7
+ context_overlay/transforms.py,sha256=I2FjNtqfTKZ-_OZ7yd-dCFmUmE7Ri_lMtP1Er3SimmM,5218
8
+ context_overlay-0.0.1.dist-info/licenses/LICENSE,sha256=K1d9tbfSQ3rDkd7ALFcKEwhY38vrGhFXBaVCkR6AQd0,1070
9
+ context_overlay-0.0.1.dist-info/METADATA,sha256=8c20Wt4PrNUqcEfR0sx54Fe0LZm4EQ58qN2WTyB7Hzo,4091
10
+ context_overlay-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ context_overlay-0.0.1.dist-info/entry_points.txt,sha256=xRXzBHMeHRdgnk8nHqNrkyoa5an__7mdjlLh8KwFftk,61
12
+ context_overlay-0.0.1.dist-info/top_level.txt,sha256=RGebGsK7jYk8TyqUJby0YFSb6OGslb-xhexx03ppnQI,16
13
+ context_overlay-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ context-overlay = context_overlay.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InternScience
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ context_overlay