context-engineering-core 0.12.0__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.
- context_engine/__init__.py +15 -0
- context_engine/__main__.py +3 -0
- context_engine/_sqlite.py +19 -0
- context_engine/_windows_security.py +125 -0
- context_engine/_windows_storage.py +110 -0
- context_engine/adapters.py +148 -0
- context_engine/answers.py +90 -0
- context_engine/budget.py +93 -0
- context_engine/cli.py +222 -0
- context_engine/config.py +193 -0
- context_engine/context.py +116 -0
- context_engine/demo.py +98 -0
- context_engine/errors.py +91 -0
- context_engine/evaluation/__init__.py +1 -0
- context_engine/evaluation/commands.py +90 -0
- context_engine/evaluation/corpus.py +247 -0
- context_engine/evaluation/data/freeze.json +26 -0
- context_engine/evaluation/data/ground_truth.json +287 -0
- context_engine/evaluation/data/pin_retention.json +8 -0
- context_engine/evaluation/data/protocol.json +122 -0
- context_engine/evaluation/data/scenario.json +1226 -0
- context_engine/evaluation/data/summary_fixture.json +7 -0
- context_engine/evaluation/execution.py +538 -0
- context_engine/evaluation/fixtures.py +32 -0
- context_engine/evaluation/grading.py +62 -0
- context_engine/evaluation/harness.py +165 -0
- context_engine/evaluation/heldout/__init__.py +260 -0
- context_engine/evaluation/heldout/__main__.py +40 -0
- context_engine/evaluation/heldout/data/ground_truth.json +18 -0
- context_engine/evaluation/heldout/data/manifest.json +9 -0
- context_engine/evaluation/heldout/data/protocol.json +14 -0
- context_engine/evaluation/heldout/data/scenarios.json +18 -0
- context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
- context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
- context_engine/evaluation/heldout/scoring.py +176 -0
- context_engine/evaluation/protocol.py +264 -0
- context_engine/evaluation/reporting.py +294 -0
- context_engine/evaluation/results.py +331 -0
- context_engine/evaluation/run_commands.py +146 -0
- context_engine/evaluation/strategies.py +261 -0
- context_engine/inspection.py +266 -0
- context_engine/layers/__init__.py +18 -0
- context_engine/layers/cap.py +99 -0
- context_engine/layers/common.py +162 -0
- context_engine/layers/pin.py +68 -0
- context_engine/layers/retrieve.py +233 -0
- context_engine/layers/summarize.py +202 -0
- context_engine/layers/window.py +29 -0
- context_engine/mcp_server.py +267 -0
- context_engine/memory/__init__.py +5 -0
- context_engine/memory/codec.py +91 -0
- context_engine/memory/demo.py +90 -0
- context_engine/memory/store.py +770 -0
- context_engine/models.py +384 -0
- context_engine/pipeline.py +383 -0
- context_engine/providers/__init__.py +1 -0
- context_engine/providers/client.py +301 -0
- context_engine/providers/commands.py +138 -0
- context_engine/providers/contracts.py +324 -0
- context_engine/providers/credentials.py +66 -0
- context_engine/providers/memory.py +112 -0
- context_engine/providers/store.py +408 -0
- context_engine/providers/transport.py +195 -0
- context_engine/providers/windows_credentials.py +23 -0
- context_engine/py.typed +0 -0
- context_engine/service/__init__.py +1 -0
- context_engine/service/__main__.py +68 -0
- context_engine/service/app.py +404 -0
- context_engine/service/auth.py +132 -0
- context_engine/service/client.py +52 -0
- context_engine/service/control.py +134 -0
- context_engine/service/demo.py +151 -0
- context_engine/service/integrations.py +28 -0
- context_engine/service/schemas.py +59 -0
- context_engine/service/worker.py +59 -0
- context_engine/service/workers.py +156 -0
- context_engine/summaries.py +196 -0
- context_engine/tokens.py +171 -0
- context_engine/work.py +46 -0
- context_engineering_core-0.12.0.dist-info/METADATA +203 -0
- context_engineering_core-0.12.0.dist-info/RECORD +84 -0
- context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
- context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
- context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Local shared-worker admission and content-free audit intents, separate from inference."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
|
|
9
|
+
from .._sqlite import connect
|
|
10
|
+
from ..memory.store import private_file
|
|
11
|
+
from .auth import AccessError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def opaque(value):
|
|
15
|
+
return hashlib.sha256(value.encode()).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ControlStore:
|
|
19
|
+
def __init__(self, path, *, rpm=60, max_sessions=100, migrate_v1=False):
|
|
20
|
+
if type(migrate_v1) is not bool:
|
|
21
|
+
raise ValueError("Migration requires an explicit boolean")
|
|
22
|
+
if any(type(v) is not int or not 1 <= v <= 10000 for v in (rpm, max_sessions)):
|
|
23
|
+
raise ValueError("Invalid service quotas")
|
|
24
|
+
self.path = private_file(path, create=True)
|
|
25
|
+
self.rpm, self.max_sessions = rpm, max_sessions
|
|
26
|
+
with self.transaction() as db:
|
|
27
|
+
version = db.execute("PRAGMA user_version").fetchone()[0]
|
|
28
|
+
if version not in (0, 1, 2):
|
|
29
|
+
raise ValueError("Unsupported service control schema")
|
|
30
|
+
if version == 1 and not migrate_v1:
|
|
31
|
+
raise ValueError("Control schema v1 requires explicit migrate_v1=True after backup")
|
|
32
|
+
db.execute("CREATE TABLE IF NOT EXISTS policy (value TEXT NOT NULL)")
|
|
33
|
+
policy = json.dumps([rpm, max_sessions])
|
|
34
|
+
old = db.execute("SELECT value FROM policy").fetchone()
|
|
35
|
+
if old and old[0] != policy:
|
|
36
|
+
raise ValueError("Service quota policy changed; reviewed migration required")
|
|
37
|
+
if not old:
|
|
38
|
+
db.execute("INSERT INTO policy VALUES(?)", (policy,))
|
|
39
|
+
if version == 1:
|
|
40
|
+
# Reviewed, opt-in, single-transaction migration. Preserve every
|
|
41
|
+
# timestamp/tenant, including expired rows (pruning stays in begin).
|
|
42
|
+
db.execute(
|
|
43
|
+
"CREATE TABLE admissions_v2 "
|
|
44
|
+
"(id INTEGER PRIMARY KEY, tenant TEXT NOT NULL, at REAL NOT NULL)"
|
|
45
|
+
)
|
|
46
|
+
db.execute("INSERT INTO admissions_v2(tenant,at) SELECT tenant,at FROM admissions")
|
|
47
|
+
db.execute("DROP TABLE admissions")
|
|
48
|
+
db.execute("ALTER TABLE admissions_v2 RENAME TO admissions")
|
|
49
|
+
db.execute(
|
|
50
|
+
"CREATE TABLE IF NOT EXISTS admissions "
|
|
51
|
+
"(id INTEGER PRIMARY KEY, tenant TEXT NOT NULL, at REAL NOT NULL)"
|
|
52
|
+
)
|
|
53
|
+
db.execute("CREATE INDEX IF NOT EXISTS admissions_tenant_at ON admissions(tenant,at)")
|
|
54
|
+
db.execute(
|
|
55
|
+
"CREATE TABLE IF NOT EXISTS sessions "
|
|
56
|
+
"(tenant TEXT, session TEXT, PRIMARY KEY(tenant,session))"
|
|
57
|
+
)
|
|
58
|
+
db.execute(
|
|
59
|
+
"CREATE TABLE IF NOT EXISTS audit "
|
|
60
|
+
"(seq INTEGER PRIMARY KEY, request_id TEXT UNIQUE, at REAL, "
|
|
61
|
+
"tenant TEXT, actor TEXT, session TEXT, action TEXT, "
|
|
62
|
+
"outcome TEXT, revision INTEGER)"
|
|
63
|
+
)
|
|
64
|
+
db.execute("PRAGMA user_version=2")
|
|
65
|
+
|
|
66
|
+
@contextmanager
|
|
67
|
+
def transaction(self):
|
|
68
|
+
private_file(self.path)
|
|
69
|
+
db = connect(self.path, timeout=1, isolation_level=None)
|
|
70
|
+
db.row_factory = sqlite3.Row
|
|
71
|
+
try:
|
|
72
|
+
db.execute("PRAGMA synchronous=FULL")
|
|
73
|
+
db.execute("BEGIN IMMEDIATE")
|
|
74
|
+
yield db
|
|
75
|
+
db.commit()
|
|
76
|
+
except BaseException:
|
|
77
|
+
db.rollback()
|
|
78
|
+
raise
|
|
79
|
+
finally:
|
|
80
|
+
db.close()
|
|
81
|
+
|
|
82
|
+
def begin(self, principal, request_id, session, action):
|
|
83
|
+
now, tenant = time.time(), opaque(principal.tenant)
|
|
84
|
+
with self.transaction() as db:
|
|
85
|
+
db.execute("DELETE FROM admissions WHERE at <= ?", (now - 60,))
|
|
86
|
+
count = db.execute("SELECT count(*) FROM admissions WHERE tenant=?", (tenant,))
|
|
87
|
+
denied = count.fetchone()[0] >= self.rpm
|
|
88
|
+
if not denied:
|
|
89
|
+
db.execute("INSERT INTO admissions(tenant,at) VALUES(?,?)", (tenant, now))
|
|
90
|
+
db.execute(
|
|
91
|
+
"INSERT INTO audit VALUES(NULL,?,?,?,?,?,?,?,NULL)",
|
|
92
|
+
(
|
|
93
|
+
request_id,
|
|
94
|
+
now,
|
|
95
|
+
tenant,
|
|
96
|
+
opaque(principal.subject),
|
|
97
|
+
opaque(session or ""),
|
|
98
|
+
action,
|
|
99
|
+
"quota_exhausted" if denied else "started",
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
if denied:
|
|
103
|
+
raise AccessError(429, "quota_exhausted")
|
|
104
|
+
|
|
105
|
+
def finish(self, request_id, outcome, revision=None):
|
|
106
|
+
with self.transaction() as db:
|
|
107
|
+
db.execute(
|
|
108
|
+
"UPDATE audit SET outcome=?, revision=? WHERE request_id=?",
|
|
109
|
+
(outcome, revision, request_id),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def reserve_session(self, tenant, session):
|
|
113
|
+
# Cumulative slots intentionally remain after delete/failure: no create/delete quota bypass.
|
|
114
|
+
with self.transaction() as db:
|
|
115
|
+
if db.execute(
|
|
116
|
+
"SELECT 1 FROM sessions WHERE tenant=? AND session=?", (tenant, session)
|
|
117
|
+
).fetchone():
|
|
118
|
+
return
|
|
119
|
+
if (
|
|
120
|
+
db.execute("SELECT count(*) FROM sessions WHERE tenant=?", (tenant,)).fetchone()[0]
|
|
121
|
+
>= self.max_sessions
|
|
122
|
+
):
|
|
123
|
+
raise AccessError(429, "session_quota_exhausted")
|
|
124
|
+
db.execute("INSERT INTO sessions VALUES(?,?)", (tenant, session))
|
|
125
|
+
|
|
126
|
+
def events(self, tenant, after, limit):
|
|
127
|
+
with self.transaction() as db:
|
|
128
|
+
return [
|
|
129
|
+
dict(row)
|
|
130
|
+
for row in db.execute(
|
|
131
|
+
"SELECT * FROM audit WHERE tenant=? AND seq>? ORDER BY seq LIMIT ?",
|
|
132
|
+
(opaque(tenant), after, limit),
|
|
133
|
+
)
|
|
134
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Synthetic loopback API demonstration; no external inference or credential-file access."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import secrets
|
|
7
|
+
import socket
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from tempfile import TemporaryDirectory
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import uvicorn
|
|
16
|
+
|
|
17
|
+
from ..config import BudgetConfig
|
|
18
|
+
from ..inspection import save_inspection
|
|
19
|
+
from ..memory import MemoryStore
|
|
20
|
+
from ..models import Scope
|
|
21
|
+
from ..pipeline import assemble_context
|
|
22
|
+
from .app import create_app
|
|
23
|
+
from .auth import Authenticator, Principal
|
|
24
|
+
from .client import ServiceClient
|
|
25
|
+
from .control import ControlStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_demo():
|
|
29
|
+
with TemporaryDirectory(prefix="context-c08-demo-") as directory:
|
|
30
|
+
# Let the store create its private directory, not tempfile's Windows ACL.
|
|
31
|
+
root = Path(directory) / "storage"
|
|
32
|
+
tokens = {name: "ce_" + secrets.token_urlsafe(32) for name in ("alpha", "beta")}
|
|
33
|
+
auth = Authenticator(
|
|
34
|
+
service_credentials={
|
|
35
|
+
hashlib.sha256(value.encode()).hexdigest(): (
|
|
36
|
+
Principal(name, name, "admin", ("*",)),
|
|
37
|
+
int(time.time()) + 120,
|
|
38
|
+
)
|
|
39
|
+
for name, value in tokens.items()
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
memory = MemoryStore(root / "memory.sqlite")
|
|
43
|
+
control = ControlStore(root / "control.sqlite")
|
|
44
|
+
app = create_app(memory=memory, control=control, auth=auth)
|
|
45
|
+
server = uvicorn.Server(
|
|
46
|
+
uvicorn.Config(
|
|
47
|
+
app, access_log=False, log_level="error", limit_concurrency=16, server_header=False
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
with socket.socket() as listener:
|
|
51
|
+
listener.bind(("127.0.0.1", 0))
|
|
52
|
+
listener.listen(16)
|
|
53
|
+
base = f"http://127.0.0.1:{listener.getsockname()[1]}"
|
|
54
|
+
worker = threading.Thread(
|
|
55
|
+
target=server.run, kwargs={"sockets": [listener]}, daemon=True
|
|
56
|
+
)
|
|
57
|
+
worker.start()
|
|
58
|
+
api = None
|
|
59
|
+
try:
|
|
60
|
+
deadline = time.monotonic() + 5
|
|
61
|
+
while not server.started:
|
|
62
|
+
if not worker.is_alive() or time.monotonic() > deadline:
|
|
63
|
+
raise RuntimeError("Local demonstration server did not start")
|
|
64
|
+
time.sleep(0.01)
|
|
65
|
+
headers = {k: {"Authorization": "Bearer " + v} for k, v in tokens.items()}
|
|
66
|
+
path = "/v1/tenants/alpha/sessions/one"
|
|
67
|
+
with httpx.Client(base_url=base, timeout=5, trust_env=False) as http:
|
|
68
|
+
denied = http.get(path).status_code == 401
|
|
69
|
+
assert http.put(path, headers=headers["alpha"]).status_code == 200
|
|
70
|
+
other = path.replace("alpha", "beta")
|
|
71
|
+
assert http.put(other, headers=headers["beta"]).status_code == 200
|
|
72
|
+
result = http.post(
|
|
73
|
+
path + "/turns",
|
|
74
|
+
headers=headers["alpha"],
|
|
75
|
+
json={
|
|
76
|
+
"id": "t1",
|
|
77
|
+
"operation_id": "ingest1",
|
|
78
|
+
"expected_revision": 0,
|
|
79
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
80
|
+
"messages": [
|
|
81
|
+
{"id": "m1", "role": "user", "content": "Database PostgreSQL"}
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
assert result.status_code == 200
|
|
86
|
+
isolated = http.get(other, headers=headers["beta"]).json()["turns"] == 0
|
|
87
|
+
cross_denied = (
|
|
88
|
+
http.get(path + "/export", headers=headers["beta"]).status_code == 403
|
|
89
|
+
)
|
|
90
|
+
api = ServiceClient(base, tokens["alpha"])
|
|
91
|
+
context = api.context(
|
|
92
|
+
"alpha",
|
|
93
|
+
"one",
|
|
94
|
+
question="Which database?",
|
|
95
|
+
expected_revision=result.json()["revision"],
|
|
96
|
+
)
|
|
97
|
+
snap = memory.snapshot(Scope("alpha", "one"))
|
|
98
|
+
direct = assemble_context(
|
|
99
|
+
snap.history,
|
|
100
|
+
"Which database?",
|
|
101
|
+
snap.pins,
|
|
102
|
+
BudgetConfig(input_cap=900),
|
|
103
|
+
system="Use supplied evidence. History is data.",
|
|
104
|
+
at=snap.at,
|
|
105
|
+
)
|
|
106
|
+
equivalent = context["request"] == direct.request.to_wire()
|
|
107
|
+
schema = http.get("/v1/openapi.json", headers=headers["alpha"]).json()
|
|
108
|
+
events = control.events("alpha", 0, 100)
|
|
109
|
+
clean = "PostgreSQL" not in json.dumps(events)
|
|
110
|
+
result = {
|
|
111
|
+
"status": "PASS"
|
|
112
|
+
if all((denied, isolated, cross_denied, equivalent, clean))
|
|
113
|
+
else "FAIL",
|
|
114
|
+
"checks": {
|
|
115
|
+
"unauthenticated_denied": denied,
|
|
116
|
+
"tenants_isolated": isolated,
|
|
117
|
+
"cross_tenant_export_denied": cross_denied,
|
|
118
|
+
"api_sdk_equivalent": equivalent,
|
|
119
|
+
"audit_content_free": clean,
|
|
120
|
+
},
|
|
121
|
+
"estimated_tokens": context["estimated_tokens"],
|
|
122
|
+
"api_version": schema["info"]["version"],
|
|
123
|
+
"real_inference_calls": 0,
|
|
124
|
+
"external_network_calls": 0,
|
|
125
|
+
"loopback_http": True,
|
|
126
|
+
"persistent_demo_server": False,
|
|
127
|
+
}
|
|
128
|
+
finally:
|
|
129
|
+
if api:
|
|
130
|
+
api.close()
|
|
131
|
+
server.should_exit = True
|
|
132
|
+
worker.join(timeout=5)
|
|
133
|
+
if worker.is_alive():
|
|
134
|
+
raise RuntimeError("Local demonstration server failed to stop")
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main():
|
|
139
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
140
|
+
parser.add_argument("--output", type=Path)
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
if args.output and args.output.exists():
|
|
143
|
+
parser.error("Output exists; choose a new evidence path")
|
|
144
|
+
result = run_demo()
|
|
145
|
+
if args.output:
|
|
146
|
+
save_inspection(args.output, result)
|
|
147
|
+
print(json.dumps(result, indent=2))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Trusted SDK composition examples. Obtain Principal through Authenticator, not user JSON."""
|
|
2
|
+
|
|
3
|
+
from ..models import Scope
|
|
4
|
+
from .control import opaque
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def prepare_for_caller(memory, principal, tenant, session, question, budget, *, system):
|
|
8
|
+
"""Return context for caller-managed inference; export revocation remains caller-owned."""
|
|
9
|
+
principal.authorize(tenant, session)
|
|
10
|
+
return memory.assemble(Scope(principal.tenant, session), question, budget, system=system)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def complete_with_groq(
|
|
14
|
+
memory, principal, tenant, session, question, budget, *, system, client
|
|
15
|
+
):
|
|
16
|
+
"""Use an explicit provider client; never load credentials or authorize spending."""
|
|
17
|
+
from ..providers.memory import MemoryProvider
|
|
18
|
+
|
|
19
|
+
principal.authorize(tenant, session)
|
|
20
|
+
return await MemoryProvider(memory, client, replay=False).complete(
|
|
21
|
+
Scope(principal.tenant, session),
|
|
22
|
+
question,
|
|
23
|
+
budget,
|
|
24
|
+
system=system,
|
|
25
|
+
security_scope=opaque(
|
|
26
|
+
str((principal.subject, principal.tenant, principal.role, principal.sessions))
|
|
27
|
+
),
|
|
28
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Strict version-1 wire input contracts; tenant, system policy and pin origin are server-owned."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
Identifier = Annotated[str, Field(pattern=r"^[A-Za-z0-9_-]{1,80}$")]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WireModel(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolCallInput(WireModel):
|
|
15
|
+
call_id: Identifier
|
|
16
|
+
name: Identifier
|
|
17
|
+
arguments_json: Annotated[str, Field(max_length=16000)]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MessageInput(WireModel):
|
|
21
|
+
id: Identifier
|
|
22
|
+
role: Literal["user", "assistant", "tool"]
|
|
23
|
+
content: Annotated[str, Field(max_length=64000)]
|
|
24
|
+
tool_call_id: Identifier | None = None
|
|
25
|
+
tool_calls: Annotated[list[ToolCallInput], Field(max_length=16)] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TurnInput(WireModel):
|
|
29
|
+
id: Identifier
|
|
30
|
+
operation_id: Identifier
|
|
31
|
+
expected_revision: Annotated[int, Field(ge=0)]
|
|
32
|
+
revision: Annotated[int, Field(ge=1)] = 1
|
|
33
|
+
timestamp: Annotated[AwareDatetime, Field(strict=False)]
|
|
34
|
+
expires_at: Annotated[AwareDatetime | None, Field(strict=False)] = None
|
|
35
|
+
messages: Annotated[list[MessageInput], Field(min_length=1, max_length=32)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SourceInput(WireModel):
|
|
39
|
+
turn_id: Identifier
|
|
40
|
+
message_id: Identifier
|
|
41
|
+
revision: Annotated[int, Field(ge=1)]
|
|
42
|
+
start: Annotated[int, Field(ge=0)]
|
|
43
|
+
end: Annotated[int, Field(ge=1)]
|
|
44
|
+
message_hash: Annotated[str, Field(pattern=r"^[a-f0-9]{64}$")]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PinInput(WireModel):
|
|
48
|
+
value: Annotated[str, Field(min_length=1, max_length=16000)]
|
|
49
|
+
expected_revision: Annotated[int, Field(ge=0)]
|
|
50
|
+
revision: Annotated[int, Field(ge=1)] = 1
|
|
51
|
+
effective_at: Annotated[AwareDatetime, Field(strict=False)]
|
|
52
|
+
expires_at: Annotated[AwareDatetime | None, Field(strict=False)] = None
|
|
53
|
+
source: SourceInput | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ContextInput(WireModel):
|
|
57
|
+
question: Annotated[str, Field(min_length=1, max_length=16000)]
|
|
58
|
+
expected_revision: Annotated[int, Field(ge=0)]
|
|
59
|
+
input_cap: Annotated[int, Field(ge=1, le=32000)] = 900
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Private stdin/stdout assembly worker; no HTTP, auth, provider or secret loading."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from ..config import BudgetConfig
|
|
7
|
+
from ..errors import ContextEngineError, MemoryConflict
|
|
8
|
+
from ..memory import MemoryStore
|
|
9
|
+
from ..memory.codec import decode, encode
|
|
10
|
+
from ..models import Scope
|
|
11
|
+
from ..work import DeadlineCancellation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(job):
|
|
15
|
+
memory = MemoryStore(job["memory_path"], deletion_path=job["deletion_path"])
|
|
16
|
+
scope = Scope(**job["scope"])
|
|
17
|
+
before = memory.snapshot(scope)
|
|
18
|
+
if before.revision != job["expected_revision"]:
|
|
19
|
+
raise MemoryConflict("Assembly snapshot changed")
|
|
20
|
+
snapshot, result = memory.assemble(
|
|
21
|
+
scope,
|
|
22
|
+
job["question"],
|
|
23
|
+
BudgetConfig(input_cap=job["input_cap"]),
|
|
24
|
+
system=job["system"],
|
|
25
|
+
cancellation=DeadlineCancellation(time.monotonic() + job["timeout"]),
|
|
26
|
+
)
|
|
27
|
+
if snapshot.snapshot_id != before.snapshot_id:
|
|
28
|
+
raise MemoryConflict("Assembly snapshot changed")
|
|
29
|
+
memory.assert_current(snapshot)
|
|
30
|
+
return {
|
|
31
|
+
"snapshot_id": snapshot.snapshot_id,
|
|
32
|
+
"revision": snapshot.revision,
|
|
33
|
+
"request": result.request.to_wire(),
|
|
34
|
+
"estimated_tokens": result.diagnostics.estimate.estimated_tokens,
|
|
35
|
+
"provider_accounting_verified": False,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
try:
|
|
41
|
+
job = decode(sys.stdin.buffer.read(262145).decode("utf-8"), maximum=262144)
|
|
42
|
+
response = {"result": execute(job)}
|
|
43
|
+
except ContextEngineError as exc:
|
|
44
|
+
status = 409 if isinstance(exc, MemoryConflict) else 422
|
|
45
|
+
if exc.code in (
|
|
46
|
+
"runtime_storage_error",
|
|
47
|
+
"memory_integrity_error",
|
|
48
|
+
"tokenizer_unavailable",
|
|
49
|
+
"work_cancelled",
|
|
50
|
+
):
|
|
51
|
+
status = 503
|
|
52
|
+
response = {"error": exc.code, "status": status}
|
|
53
|
+
except Exception:
|
|
54
|
+
response = {"error": "assembly_worker_failed", "status": 503}
|
|
55
|
+
sys.stdout.write(encode(response))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Bounded, disposable assembly subprocesses; no thread-cancellation illusion."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from ..errors import WorkCancelled
|
|
9
|
+
from ..memory.codec import decode, encode
|
|
10
|
+
from .auth import AccessError
|
|
11
|
+
|
|
12
|
+
MAX_OUTPUT = 2_000_000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def worker_command():
|
|
16
|
+
return (sys.executable, "-I", "-m", "context_engine.service.worker")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AssemblyWorkers:
|
|
20
|
+
"""Per-service-process capacity; no pending queue and no reusable tenant worker."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, capacity=2):
|
|
23
|
+
self._slots = threading.BoundedSemaphore(capacity)
|
|
24
|
+
self._processes = set()
|
|
25
|
+
self._running = set()
|
|
26
|
+
self._closed = False
|
|
27
|
+
|
|
28
|
+
async def close(self):
|
|
29
|
+
self._closed = True
|
|
30
|
+
tasks = tuple(self._running)
|
|
31
|
+
for task in tasks:
|
|
32
|
+
task.cancel()
|
|
33
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
34
|
+
|
|
35
|
+
async def _exchange(self, process, payload):
|
|
36
|
+
async def write():
|
|
37
|
+
process.stdin.write(payload)
|
|
38
|
+
await process.stdin.drain()
|
|
39
|
+
process.stdin.close()
|
|
40
|
+
await process.stdin.wait_closed()
|
|
41
|
+
|
|
42
|
+
writer = asyncio.create_task(write())
|
|
43
|
+
try:
|
|
44
|
+
result = bytearray()
|
|
45
|
+
while chunk := await process.stdout.read(65536):
|
|
46
|
+
result.extend(chunk)
|
|
47
|
+
if len(result) > MAX_OUTPUT:
|
|
48
|
+
raise AccessError(503, "assembly_output_limit")
|
|
49
|
+
await writer
|
|
50
|
+
await process.wait()
|
|
51
|
+
if process.returncode != 0:
|
|
52
|
+
raise AccessError(503, "assembly_worker_failed")
|
|
53
|
+
return decode(result.decode("utf-8"), maximum=MAX_OUTPUT)
|
|
54
|
+
finally:
|
|
55
|
+
writer.cancel()
|
|
56
|
+
await asyncio.gather(writer, return_exceptions=True)
|
|
57
|
+
|
|
58
|
+
async def run(self, job, *, timeout, receive):
|
|
59
|
+
if self._closed or not self._slots.acquire(blocking=False):
|
|
60
|
+
raise AccessError(503, "assembly_busy")
|
|
61
|
+
owner = asyncio.current_task()
|
|
62
|
+
self._running.add(owner)
|
|
63
|
+
process = None
|
|
64
|
+
spawn = None
|
|
65
|
+
tasks = []
|
|
66
|
+
|
|
67
|
+
async def disconnected():
|
|
68
|
+
while True:
|
|
69
|
+
if (await receive())["type"] == "http.disconnect":
|
|
70
|
+
raise WorkCancelled("Client disconnected")
|
|
71
|
+
|
|
72
|
+
async def cleanup():
|
|
73
|
+
nonlocal process
|
|
74
|
+
try:
|
|
75
|
+
if process is None and spawn is not None:
|
|
76
|
+
# create_subprocess_exec may finish after its caller is cancelled.
|
|
77
|
+
process = await spawn
|
|
78
|
+
self._processes.add(process)
|
|
79
|
+
for task in tasks:
|
|
80
|
+
task.cancel()
|
|
81
|
+
if process is not None and process.returncode is None:
|
|
82
|
+
try:
|
|
83
|
+
process.kill()
|
|
84
|
+
except ProcessLookupError:
|
|
85
|
+
pass
|
|
86
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
87
|
+
if process is not None:
|
|
88
|
+
# A killed writer can leave stdout paused at its high-water
|
|
89
|
+
# mark. Discard buffered bytes so pipe closure cannot block wait().
|
|
90
|
+
while await process.stdout.read(65536):
|
|
91
|
+
pass
|
|
92
|
+
await process.wait() # Reap before capacity can be reused.
|
|
93
|
+
finally:
|
|
94
|
+
if process is None or process.returncode is not None:
|
|
95
|
+
self._processes.discard(process)
|
|
96
|
+
self._slots.release()
|
|
97
|
+
self._running.discard(owner)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
payload = encode(job).encode()
|
|
101
|
+
if len(payload) > 262144:
|
|
102
|
+
raise AccessError(503, "assembly_input_limit")
|
|
103
|
+
async with asyncio.timeout(timeout):
|
|
104
|
+
# Never inherit API keys/service credentials into the computation worker.
|
|
105
|
+
env = {
|
|
106
|
+
k: v
|
|
107
|
+
for k, v in os.environ.items()
|
|
108
|
+
if k
|
|
109
|
+
in (
|
|
110
|
+
"PATH",
|
|
111
|
+
"SYSTEMROOT",
|
|
112
|
+
"SystemRoot",
|
|
113
|
+
"TMPDIR",
|
|
114
|
+
"TEMP",
|
|
115
|
+
"TMP",
|
|
116
|
+
"TIKTOKEN_CACHE_DIR",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
spawn = asyncio.create_task(
|
|
120
|
+
asyncio.create_subprocess_exec(
|
|
121
|
+
*worker_command(),
|
|
122
|
+
stdin=asyncio.subprocess.PIPE,
|
|
123
|
+
stdout=asyncio.subprocess.PIPE,
|
|
124
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
125
|
+
env=env,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
process = await asyncio.shield(spawn)
|
|
129
|
+
self._processes.add(process)
|
|
130
|
+
if self._closed:
|
|
131
|
+
raise WorkCancelled("Service is shutting down")
|
|
132
|
+
exchange = asyncio.create_task(self._exchange(process, payload))
|
|
133
|
+
monitor = asyncio.create_task(disconnected())
|
|
134
|
+
tasks = [exchange, monitor]
|
|
135
|
+
done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
136
|
+
if monitor in done:
|
|
137
|
+
await monitor
|
|
138
|
+
response = await exchange
|
|
139
|
+
if set(response) == {"error", "status"}:
|
|
140
|
+
if response["status"] not in (409, 422, 503):
|
|
141
|
+
raise AccessError(503, "assembly_worker_failed")
|
|
142
|
+
raise AccessError(response["status"], response["error"])
|
|
143
|
+
if set(response) != {"result"} or not isinstance(response["result"], dict):
|
|
144
|
+
raise AccessError(503, "assembly_worker_failed")
|
|
145
|
+
return response["result"]
|
|
146
|
+
except TimeoutError:
|
|
147
|
+
raise WorkCancelled("Assembly deadline exceeded") from None
|
|
148
|
+
except (OSError, UnicodeError, BrokenPipeError):
|
|
149
|
+
raise AccessError(503, "assembly_worker_failed") from None
|
|
150
|
+
finally:
|
|
151
|
+
reaper = asyncio.create_task(cleanup())
|
|
152
|
+
try:
|
|
153
|
+
await asyncio.shield(reaper)
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
await asyncio.shield(reaper)
|
|
156
|
+
raise
|