harness-everything 0.21.0 → 0.22.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/README.md +30 -0
- package/benchmarks/results/system-one/cua-s1-forms-resident-async-cpu-2026-09-23/README.md +87 -0
- package/benchmarks/results/system-one/cua-s1-forms-resident-async-cpu-2026-09-23/paired-latency.json +107 -0
- package/benchmarks/results/system-one/cua-s1-forms-resident-async-cpu-2026-09-23/report.json +856 -0
- package/benchmarks/results/system-one/cua-s1-forms-resident-cpu-2026-09-23/README.md +74 -0
- package/benchmarks/results/system-one/cua-s1-forms-resident-cpu-2026-09-23/report.json +856 -0
- package/benchmarks/results/system-one/pre-merge-review-2026-09-23/README.md +92 -0
- package/benchmarks/results/system-one/pre-merge-review-2026-09-23/haiku-spec-conformance.md +241 -0
- package/benchmarks/results/system-one/pre-merge-review-2026-09-23/luna-adversarial.md +212 -0
- package/benchmarks/results/system-one/pre-merge-review-2026-09-23/skill-top1-probe.js +83 -0
- package/benchmarks/results/system-one/pre-merge-review-2026-09-23/skill-top1-probe.json +7700 -0
- package/ci/fixtures/system-one-resident-stub.py +28 -0
- package/ci/fixtures/system-one-worker-probe.js +15 -0
- package/ci/mechanism-packaging-bytecode.test.js +61 -0
- package/ci/mechanism-system-one-install.test.js +11 -0
- package/ci/mechanism-system-one-resident.test.js +296 -0
- package/ci/mechanism-system-one-router.test.js +25 -0
- package/ci/system-one-adapter-test.py +166 -0
- package/docs/platform-capabilities.md +8 -0
- package/docs/system-one-routing.md +91 -3
- package/evals/pilot-router-nonsoftware/graders/no-tier-language.md +6 -0
- package/evals/pilot-router-nonsoftware/graders/skill-not-fired.md +9 -0
- package/evals/pilot-router-nonsoftware/prompt.md +8 -0
- package/evals/pilot-router-tier3/graders/skill-fired.md +6 -0
- package/evals/pilot-router-tier3/prompt.md +8 -0
- package/evals/pilot-tdd-bugfix/graders/skill-fired.md +6 -0
- package/evals/pilot-tdd-bugfix/prompt.md +8 -0
- package/evals/pilot-tdd-trivia/graders/no-red-phase-claim.md +7 -0
- package/evals/pilot-tdd-trivia/graders/skill-not-fired.md +9 -0
- package/evals/pilot-tdd-trivia/prompt.md +8 -0
- package/evals/pilot-verify-discussion/graders/no-verification-claim.md +7 -0
- package/evals/pilot-verify-discussion/graders/skill-not-fired.md +9 -0
- package/evals/pilot-verify-discussion/prompt.md +8 -0
- package/evals/pilot-verify-preship/graders/skill-fired.md +6 -0
- package/evals/pilot-verify-preship/prompt.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/system-one/cua_adapter.py +265 -6
- package/harness-everything/scripts/system-one/install.js +17 -8
- package/harness-everything/scripts/system-one/provider.js +18 -3
- package/harness-everything/scripts/system-one/resident.js +200 -0
- package/harness-everything/scripts/system-one/router.js +18 -2
- package/harness-everything/scripts/tier-router.js +13 -4
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/packaging-noise.js +6 -0
- package/plugins/harness-everything/scripts/lib/skills.js +2 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/cua_adapter.py +265 -6
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/install.js +17 -8
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +18 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +200 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +18 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +13 -4
- package/sbom.cdx.json +7 -7
- package/scripts/build-openai-submission.js +2 -0
- package/scripts/evaluate-system-one.js +18 -9
- package/scripts/lib/packaging-noise.js +6 -0
- package/scripts/lib/skills.js +2 -0
- package/scripts/sync-openai-plugin.js +2 -1
|
@@ -129,5 +129,13 @@ structural floor. Canonical and packaged runtime copies use the same
|
|
|
129
129
|
contract; this establishes no new live-host capability. Python, PyTorch and a
|
|
130
130
|
compatible local checkpoint are optional external dependencies, not npm payloads;
|
|
131
131
|
the explicit `npm run system-one:install` sets them up and hooks never run it.
|
|
132
|
+
With `transport: "resident"`, an opted-in router hook may spawn one detached
|
|
133
|
+
loopback-only Python server per manifest. One retained Windows + Claude Code
|
|
134
|
+
headless session showed the hook-spawned server outliving the host; macOS/Linux
|
|
135
|
+
and Codex hosts are not live-verified, and this does not change the matrix.
|
|
136
|
+
Hooks read the mode from the host process environment: Claude Code settings
|
|
137
|
+
`env` reaches hooks and every agent-run command, while a Codex started from an
|
|
138
|
+
ordinary terminal sees it only when set at user/OS level. Keep the mode `off`
|
|
139
|
+
until a Harness-trained checkpoint passes the rollout gates.
|
|
132
140
|
Public Skills-only artifacts do not acquire lifecycle hooks from this feature.
|
|
133
141
|
See [System One routing](system-one-routing.md) for limits, rollback and evidence.
|
|
@@ -68,10 +68,13 @@ installs the pinned `cua_s1` Git URL. It downloads the `cua-ai/cua-s1-forms`
|
|
|
68
68
|
safetensors weights and JSON sidecar at Hugging Face revision
|
|
69
69
|
`f54adbf447f4ca6ec259f529ee3f2e3e09f8cc71`, verifies pinned sizes and SHA-256
|
|
70
70
|
before an atomic write (the pickle `.pt` is never fetched), and writes
|
|
71
|
-
`<dir>/manifest.json` with `timeoutMs: 10000
|
|
71
|
+
`<dir>/manifest.json` with `timeoutMs: 10000`, `transport: "resident"` and
|
|
72
|
+
`idleTimeoutMs: 1800000`. The default `<dir>` is
|
|
72
73
|
`~/.agents/harness-everything/system-one`; `--dir` takes an absolute path and
|
|
73
74
|
`--dry-run` prints the plan without changes. Acceptance requires a `pinned`
|
|
74
|
-
provenance probe and one real CPU inference
|
|
75
|
+
provenance probe and one real warm CPU inference through a resident server that
|
|
76
|
+
the installer starts and then stops; otherwise the exit code is 1. Child-process
|
|
77
|
+
output goes to stderr; stdout carries only the JSON report.
|
|
75
78
|
Torch/numpy/safetensors versions resolve at install time and are recorded by
|
|
76
79
|
the probe. The installer does not change the router mode: it prints
|
|
77
80
|
`HARNESS_SYSTEM_ONE_MODE=shadow` and the manifest path for the host
|
|
@@ -88,13 +91,93 @@ No prompts or model values are written to disk or echoed in error messages.
|
|
|
88
91
|
|
|
89
92
|
The Node transport has a hard timeout and a 1 MiB stdout/stderr bound; it kills
|
|
90
93
|
the direct child on timeout. It starts one short-lived Python process per score
|
|
91
|
-
request. This is
|
|
94
|
+
request. This is the default `oneshot` transport, not a warm server; see the
|
|
95
|
+
resident provider below.
|
|
92
96
|
The process cannot execute actions and is not given a CUA driver. Failures return
|
|
93
97
|
`{status: 'unavailable', reason}` with a bounded reason code; malformed score
|
|
94
98
|
responses are checked by Phase 0. Transport fixtures prove IPC and failure
|
|
95
99
|
handling only. Real checkpoint inference and measured CPU latency remain a
|
|
96
100
|
separate gate, with explicit unavailable evidence if dependencies are absent.
|
|
97
101
|
|
|
102
|
+
## Resident provider (Phase 4 prerequisite)
|
|
103
|
+
|
|
104
|
+
The one-shot bridge pays Python start, `import torch` and checkpoint load on every
|
|
105
|
+
call (measured ≈5 s on CPU); warm in-process inference is a few milliseconds. The
|
|
106
|
+
manifest may set `transport: "resident"` (default `"oneshot"`) and optional
|
|
107
|
+
`idleTimeoutMs` (60000–14400000, default 1800000) to keep one verified model in
|
|
108
|
+
memory. Other manifest rules are unchanged; the timeout remains one-shot only.
|
|
109
|
+
|
|
110
|
+
**Lifecycle.** A resident-mode score never blocks on model loading. If no live
|
|
111
|
+
server exists, the Node provider creates a start lock with exclusive-create,
|
|
112
|
+
spawns `cua_adapter.py --serve` detached with the configured Python (no shell),
|
|
113
|
+
and returns `unavailable/provider-starting`, so the lexical route is used for
|
|
114
|
+
that prompt. Concurrent callers seeing a lock younger than 60 s do not spawn
|
|
115
|
+
again. The server verifies artifact hashes before `load_checkpoint`, writes its
|
|
116
|
+
state file atomically, then removes the lock. A startup failure writes a bounded
|
|
117
|
+
reason into the lock and exits; callers report `provider-unavailable` without
|
|
118
|
+
respawning until the lock is 60 s old. An executable that cannot be spawned at
|
|
119
|
+
all is detected synchronously (no child PID) and recorded as `spawn-failed` the
|
|
120
|
+
same way. The server exits after `idleTimeoutMs` without requests, on
|
|
121
|
+
`shutdown`, when either checkpoint file's size or modification time changes
|
|
122
|
+
(`artifact-changed`), or as soon as its state file no longer names its PID and
|
|
123
|
+
token (replaced, corrupted or deleted), so an undiscoverable server never
|
|
124
|
+
lingers as an orphan. It deletes only a state file that still names it.
|
|
125
|
+
|
|
126
|
+
**Files.** State and lock live beside the manifest as
|
|
127
|
+
`system-one-resident-<digest>.json` / `.starting`, where `<digest>` is the first
|
|
128
|
+
16 hex digits of SHA-256 of the parsed manifest re-serialized with `JSON.stringify`
|
|
129
|
+
(file key order). Editing the manifest
|
|
130
|
+
therefore selects a new server; the old one exits when idle. The state file has
|
|
131
|
+
exactly `schemaVersion: 1`, `pid`, `port`, `token` (64 hex), and `startedAt`, and
|
|
132
|
+
is created with mode 0600; on Windows the file instead receives an explicit
|
|
133
|
+
DACL with inheritance removed and a single full-control entry for the current
|
|
134
|
+
user's SID before the token is written. The manifest directory must still be
|
|
135
|
+
writable only by that user: whoever can replace files there can redirect the
|
|
136
|
+
client, and whoever can edit the manifest chooses the executable.
|
|
137
|
+
|
|
138
|
+
**Protocol.** The server binds only `127.0.0.1` on an ephemeral port and handles
|
|
139
|
+
one request per connection: one newline-terminated JSON object of at most 2 MiB
|
|
140
|
+
with `token`, `op` (`score`, `ping`, `shutdown`) and, for `score`, a request with
|
|
141
|
+
the exact Phase 0 structure (fields, lowercase IDs, unique options, 2–256
|
|
142
|
+
options, 64-hex fingerprints). Connections are served concurrently, up to 64, each
|
|
143
|
+
with a 1 s read deadline; beyond that the reply is `busy`. Inference itself is
|
|
144
|
+
serialized, so a silent or slow client cannot delay other callers. Tokens are compared in constant time; a mismatch returns
|
|
145
|
+
`{ok: false, reason: "unauthorized"}`. `score` applies the same byte limits
|
|
146
|
+
(no truncation) and returns `{ok: true, response}` in the Phase 0 response
|
|
147
|
+
format, which Phase 0 validation still checks. Any other failure returns
|
|
148
|
+
`{ok: false, reason}` with a bounded code; no prompt, traceback, path or score is
|
|
149
|
+
echoed in errors. The server never executes actions and is not given a driver.
|
|
150
|
+
|
|
151
|
+
**Client.** The hook entry pre-scores asynchronously. When `tier-router.js` runs
|
|
152
|
+
as a script (the kernel and packaged hooks spawn it) in `shadow` or `prefer`
|
|
153
|
+
mode, it builds the fixed-catalog tier request before routing and makes one
|
|
154
|
+
direct async loopback round trip (`scoreAsync`), with no worker thread. Start,
|
|
155
|
+
lock, stale-state, deadline (1000 ms) and reply-limit (1 MiB) rules are the same
|
|
156
|
+
as the synchronous path. The router uses the pre-scored result only when its
|
|
157
|
+
request hash equals the request the router builds; otherwise it scores
|
|
158
|
+
synchronously. A pre-score error is reported exactly as a synchronous scorer
|
|
159
|
+
error would be. In `off` mode the entry neither pre-scores nor contacts a
|
|
160
|
+
provider. The synchronous `score` API remains for programmatic callers: it
|
|
161
|
+
makes the same round trip on a worker thread and waits with `Atomics.wait`.
|
|
162
|
+
Starting that worker costs 29–41 ms per call on the reference host, which is why
|
|
163
|
+
the hook avoids it. The paired measurements are in
|
|
164
|
+
[cua-s1-forms-resident-async-cpu-2026-09-23](../benchmarks/results/system-one/cua-s1-forms-resident-async-cpu-2026-09-23/README.md).
|
|
165
|
+
Both paths map results to the existing reasons: timeout → `provider-timeout` (no respawn);
|
|
166
|
+
refused connection or dead PID → stale state removed and a new start
|
|
167
|
+
(`provider-starting`); `unauthorized` → `provider-unavailable`; any other
|
|
168
|
+
server-declined request → `provider-exit`; malformed replies → `provider-json`.
|
|
169
|
+
|
|
170
|
+
**Operators.** `node harness-everything/scripts/system-one/resident.js
|
|
171
|
+
start|status|stop <absolute manifest>` controls a server explicitly; `start`
|
|
172
|
+
waits up to 60 s for readiness. The evaluation CLI waits for readiness before
|
|
173
|
+
measuring, so resident runs are recorded with `coldStart: false`. It times
|
|
174
|
+
`scoreAsync`, the path the hook uses, so `warmLatency` measures what a prompt
|
|
175
|
+
actually pays. The installer
|
|
176
|
+
writes `transport: "resident"`, verifies one resident score, and stops the server
|
|
177
|
+
it started. Repository tests use a stub scorer behind the real server/protocol
|
|
178
|
+
code; they prove the mechanism, not host survival of detached processes or model
|
|
179
|
+
quality. Those require retained evidence.
|
|
180
|
+
|
|
98
181
|
## Phase 2: tier integration and rollback
|
|
99
182
|
|
|
100
183
|
Set `HARNESS_SYSTEM_ONE_MODE=off|shadow|prefer` in the host environment. Default
|
|
@@ -126,6 +209,11 @@ selected candidate, model identity, confidence and margin; it omits prompt text,
|
|
|
126
209
|
score vectors and artifact paths. The existing router contract schema is unchanged.
|
|
127
210
|
Kernel/packaged entry points inherit these host environment variables. Prompts
|
|
128
211
|
and hook payload fields cannot set model configuration or launch executables.
|
|
212
|
+
Claude Code settings `env` is hot-reloaded and reaches hooks and every command
|
|
213
|
+
the agent runs; Codex hooks inherit the `codex` process environment, so a Codex
|
|
214
|
+
started from an ordinary terminal needs user/OS-level variables (which then apply
|
|
215
|
+
to every program the user starts). The recommended setting is `off` until a
|
|
216
|
+
`harness-routing-v1` checkpoint passes the rollout gates.
|
|
129
217
|
Repository tests establish mechanism/package evidence only; actual host loading
|
|
130
218
|
and semantic quality remain unverified until retained sessions demonstrate them.
|
|
131
219
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 15
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
tags: [pilot, positive-trigger, tier-3]
|
|
5
|
+
description: "Architectural, repo-wide request should route through the harness-everything router and land on Tier 3."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Route this coding request: split our monolith into two services (orders and billing) with an event bus between them; this touches deployment, data migration, and every team's code.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 15
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
tags: [pilot, positive-trigger]
|
|
5
|
+
description: "A bug-fix request that asks for a regression test should engage the tdd skill's RED/GREEN loop."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Fix a bug with a regression test: the discount calculation rounds wrong for prices ending in .99.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 10
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
tags: [pilot, negative-trigger]
|
|
5
|
+
description: "A one-line trivia question about TDD terminology must not start a RED/GREEN test-writing loop."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
In one sentence, what does the RED phase of test-driven development mean?
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 10
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
tags: [pilot, negative-trigger]
|
|
5
|
+
description: "A design discussion with no completed work must not launch the verification-loop skill."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Sketch two possible approaches for caching API responses and list trade-offs. Don't write any code yet.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
max_turns: 15
|
|
3
|
+
allowed_tools: [Read, Glob, Grep, Skill]
|
|
4
|
+
tags: [pilot, positive-trigger]
|
|
5
|
+
description: "A pre-completion request to prove work is done should trigger the verification-loop skill."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You said the refactor is finished. Run the full verification loop and show me the evidence before we call this done.
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
"""Optional CPU CUA-S1 bridge. Stdout is protocol-only; never execute UI actions."""
|
|
2
2
|
import hashlib
|
|
3
|
+
import hmac
|
|
3
4
|
import json
|
|
5
|
+
import os
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
import re
|
|
8
|
+
import secrets
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
6
11
|
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
def verify_artifacts(manifest):
|
|
@@ -25,18 +32,23 @@ def check_limits(request, config):
|
|
|
25
32
|
raise ValueError('input-too-long')
|
|
26
33
|
|
|
27
34
|
|
|
28
|
-
def
|
|
29
|
-
|
|
35
|
+
def load_scorer(weights, manifest):
|
|
36
|
+
"""Load the verified checkpoint once; return (score(request) -> probabilities, config)."""
|
|
30
37
|
import torch
|
|
31
38
|
from cua_s1.model import ChoiceExample, load_checkpoint
|
|
32
39
|
|
|
33
40
|
torch.set_num_threads(1)
|
|
34
41
|
torch.use_deterministic_algorithms(True)
|
|
35
42
|
model, collator, config = load_checkpoint(weights, 'cpu')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
|
|
44
|
+
def score(request):
|
|
45
|
+
example = ChoiceExample(context=request['context'], options=tuple(o['text'] for o in request['options']), label=0)
|
|
46
|
+
with torch.inference_mode():
|
|
47
|
+
return model(collator([example])).softmax(-1)[0].tolist()
|
|
48
|
+
return score, config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_response(request, manifest, probabilities):
|
|
40
52
|
return {
|
|
41
53
|
'schemaVersion': 1, 'requestHash': request['requestHash'], 'catalogHash': request['catalogHash'],
|
|
42
54
|
'model': {'id': manifest['modelId'], 'revision': manifest['revision'], 'domain': manifest['domain']},
|
|
@@ -44,6 +56,251 @@ def infer(request, manifest):
|
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
|
|
59
|
+
def infer(request, manifest):
|
|
60
|
+
weights = verify_artifacts(manifest)
|
|
61
|
+
score, config = load_scorer(weights, manifest)
|
|
62
|
+
check_limits(request, config)
|
|
63
|
+
return build_response(request, manifest, score(request))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
MAX_REQUEST_BYTES = 2 * 1024 * 1024
|
|
67
|
+
MAX_OPTIONS = 256
|
|
68
|
+
MAX_CLIENTS = 64
|
|
69
|
+
READ_DEADLINE_S = 1.0 # never longer than the Node client's 1000 ms call deadline
|
|
70
|
+
STARTUP_REASONS = ('artifact-hash', 'checkpoint-format')
|
|
71
|
+
ID_PATTERN = re.compile(r'[a-z0-9._-]{1,80}')
|
|
72
|
+
HASH_PATTERN = re.compile(r'[0-9a-f]{64}')
|
|
73
|
+
_USER_SID = []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _artifact_stamp(weights):
|
|
77
|
+
return [(p.stat().st_size, p.stat().st_mtime_ns) for p in (weights, weights.with_suffix('.json'))]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _restrict_to_current_user(path):
|
|
81
|
+
"""Windows: replace the whole DACL with one protected full-control ACE for the current user.
|
|
82
|
+
|
|
83
|
+
Editing ACEs is not enough: some hosts create files with explicit (non-inherited) SYSTEM,
|
|
84
|
+
Administrators or OWNER RIGHTS entries that survive `icacls /inheritance:r /grant:r`.
|
|
85
|
+
"""
|
|
86
|
+
if os.name != 'nt':
|
|
87
|
+
return
|
|
88
|
+
import ctypes
|
|
89
|
+
from ctypes import wintypes
|
|
90
|
+
if not _USER_SID:
|
|
91
|
+
tools = Path(os.environ.get('SystemRoot', 'C:/Windows')) / 'System32'
|
|
92
|
+
out = subprocess.run([str(tools / 'whoami.exe'), '/user', '/fo', 'csv', '/nh'],
|
|
93
|
+
capture_output=True, text=True, timeout=10, check=True).stdout
|
|
94
|
+
sid = out.strip().split(',')[-1].strip().strip('"')
|
|
95
|
+
if not re.fullmatch(r'S-1-[0-9-]+', sid):
|
|
96
|
+
raise OSError('user-sid')
|
|
97
|
+
_USER_SID.append(sid)
|
|
98
|
+
advapi32 = ctypes.WinDLL('advapi32', use_last_error=True)
|
|
99
|
+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
100
|
+
descriptor = ctypes.c_void_p()
|
|
101
|
+
if not advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
102
|
+
f'D:P(A;;FA;;;{_USER_SID[0]})', 1, ctypes.byref(descriptor), None):
|
|
103
|
+
raise OSError('dacl-build')
|
|
104
|
+
try:
|
|
105
|
+
present, defaulted, dacl = wintypes.BOOL(), wintypes.BOOL(), ctypes.c_void_p()
|
|
106
|
+
if not advapi32.GetSecurityDescriptorDacl(descriptor, ctypes.byref(present), ctypes.byref(dacl), ctypes.byref(defaulted)):
|
|
107
|
+
raise OSError('dacl-read')
|
|
108
|
+
SE_FILE_OBJECT, DACL_INFO, PROTECTED_DACL_INFO = 1, 0x4, 0x80000000
|
|
109
|
+
advapi32.SetNamedSecurityInfoW.argtypes = [wintypes.LPWSTR, ctypes.c_int, wintypes.DWORD,
|
|
110
|
+
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
|
111
|
+
if advapi32.SetNamedSecurityInfoW(str(path), SE_FILE_OBJECT, DACL_INFO | PROTECTED_DACL_INFO, None, None, dacl, None) != 0:
|
|
112
|
+
raise OSError('dacl-set')
|
|
113
|
+
finally:
|
|
114
|
+
kernel32.LocalFree(descriptor)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _write_private_json(target, value):
|
|
118
|
+
"""Atomic replace of an owner-only file (0600; explicit owner-only DACL on Windows) before the token is written."""
|
|
119
|
+
tmp = target.with_name(f'.{target.name}.{os.getpid()}.{secrets.token_hex(4)}.tmp')
|
|
120
|
+
os.close(os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600))
|
|
121
|
+
_restrict_to_current_user(tmp)
|
|
122
|
+
with open(tmp, 'w', encoding='utf-8') as handle:
|
|
123
|
+
json.dump(value, handle)
|
|
124
|
+
os.replace(tmp, target)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _read_request_line(conn):
|
|
128
|
+
buffer = bytearray()
|
|
129
|
+
while b'\n' not in buffer and len(buffer) <= MAX_REQUEST_BYTES:
|
|
130
|
+
chunk = conn.recv(65536)
|
|
131
|
+
if not chunk:
|
|
132
|
+
break
|
|
133
|
+
buffer.extend(chunk)
|
|
134
|
+
if len(buffer.split(b'\n', 1)[0]) > MAX_REQUEST_BYTES:
|
|
135
|
+
# Drain to EOF (bounded by size and the read deadline) so the reply is not lost to a reset.
|
|
136
|
+
drained = 0
|
|
137
|
+
try:
|
|
138
|
+
while drained < 8 * MAX_REQUEST_BYTES and conn.recv(65536):
|
|
139
|
+
drained += 65536
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
raise ValueError('request-limit')
|
|
143
|
+
return bytes(buffer).split(b'\n', 1)[0]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _valid_score_request(request):
|
|
147
|
+
"""Phase 0 request structure. Hashes are fingerprints, not authentication, so they are shape-checked only."""
|
|
148
|
+
if (not isinstance(request, dict)
|
|
149
|
+
or set(request) != {'schemaVersion', 'task', 'context', 'options', 'catalogHash', 'requestHash'}
|
|
150
|
+
or request['schemaVersion'] != 1 or not isinstance(request['task'], str) or not ID_PATTERN.fullmatch(request['task'])
|
|
151
|
+
or not isinstance(request['context'], str)
|
|
152
|
+
or not all(isinstance(request[k], str) and HASH_PATTERN.fullmatch(request[k]) for k in ('catalogHash', 'requestHash'))):
|
|
153
|
+
return False
|
|
154
|
+
options = request['options']
|
|
155
|
+
if not isinstance(options, list) or not 2 <= len(options) <= MAX_OPTIONS:
|
|
156
|
+
return False
|
|
157
|
+
if any(not isinstance(o, dict) or set(o) != {'id', 'text'} or not isinstance(o['id'], str)
|
|
158
|
+
or not ID_PATTERN.fullmatch(o['id']) or not isinstance(o['text'], str) for o in options):
|
|
159
|
+
return False
|
|
160
|
+
return len({o['id'] for o in options}) == len(options)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _handle(conn, token, manifest, weights, stamp, score, config):
|
|
164
|
+
"""Return (reply, stop). Errors carry bounded reason codes only: no prompt, path, or traceback."""
|
|
165
|
+
invalid = {'ok': False, 'reason': 'invalid-request'}
|
|
166
|
+
try:
|
|
167
|
+
line = _read_request_line(conn)
|
|
168
|
+
except ValueError:
|
|
169
|
+
return {'ok': False, 'reason': 'request-limit'}, False
|
|
170
|
+
except OSError:
|
|
171
|
+
return invalid, False
|
|
172
|
+
try:
|
|
173
|
+
message = json.loads(line)
|
|
174
|
+
except ValueError:
|
|
175
|
+
return invalid, False
|
|
176
|
+
if not isinstance(message, dict):
|
|
177
|
+
return invalid, False
|
|
178
|
+
supplied = message.get('token')
|
|
179
|
+
if not isinstance(supplied, str) or not hmac.compare_digest(supplied.encode('utf-8'), token.encode('utf-8')):
|
|
180
|
+
return {'ok': False, 'reason': 'unauthorized'}, False
|
|
181
|
+
op = message.get('op')
|
|
182
|
+
if op == 'ping':
|
|
183
|
+
return {'ok': True, 'pid': os.getpid(), 'model': {'id': manifest['modelId'], 'revision': manifest['revision'], 'domain': manifest['domain']}}, False
|
|
184
|
+
if op == 'shutdown':
|
|
185
|
+
return {'ok': True}, True
|
|
186
|
+
if op != 'score' or not _valid_score_request(message.get('request')):
|
|
187
|
+
return invalid, False
|
|
188
|
+
try:
|
|
189
|
+
changed = _artifact_stamp(weights) != stamp
|
|
190
|
+
except OSError:
|
|
191
|
+
changed = True
|
|
192
|
+
if changed:
|
|
193
|
+
return {'ok': False, 'reason': 'artifact-changed'}, True
|
|
194
|
+
request = message['request']
|
|
195
|
+
try:
|
|
196
|
+
check_limits(request, config)
|
|
197
|
+
except ValueError:
|
|
198
|
+
return {'ok': False, 'reason': 'input-too-long'}, False
|
|
199
|
+
try:
|
|
200
|
+
return {'ok': True, 'response': build_response(request, manifest, score(request))}, False
|
|
201
|
+
except Exception:
|
|
202
|
+
return {'ok': False, 'reason': 'score-failed'}, False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def serve(manifest, state_path, lock_path, scorer_factory=None, idle_timeout_s=None):
|
|
206
|
+
"""Resident loopback scorer: verify, load once, publish state, serve until idle/shutdown/artifact change."""
|
|
207
|
+
state_path, lock_path = Path(state_path), Path(lock_path)
|
|
208
|
+
try:
|
|
209
|
+
weights = verify_artifacts(manifest)
|
|
210
|
+
stamp = _artifact_stamp(weights)
|
|
211
|
+
score, config = (scorer_factory or load_scorer)(weights, manifest)
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
reason = str(exc) if str(exc) in STARTUP_REASONS else 'load-failed'
|
|
214
|
+
try:
|
|
215
|
+
_write_private_json(lock_path, {'failed': reason, 'at': time.time()})
|
|
216
|
+
except OSError:
|
|
217
|
+
pass
|
|
218
|
+
return 1
|
|
219
|
+
idle = idle_timeout_s if idle_timeout_s is not None else manifest.get('idleTimeoutMs', 1800000) / 1000
|
|
220
|
+
token = secrets.token_hex(32)
|
|
221
|
+
pid = os.getpid()
|
|
222
|
+
stop = threading.Event()
|
|
223
|
+
score_lock = threading.Lock() # one model, one inference at a time; connections are handled concurrently
|
|
224
|
+
slots = threading.BoundedSemaphore(MAX_CLIENTS)
|
|
225
|
+
|
|
226
|
+
def guarded_score(request):
|
|
227
|
+
with score_lock:
|
|
228
|
+
return score(request)
|
|
229
|
+
|
|
230
|
+
def send(conn, reply):
|
|
231
|
+
try:
|
|
232
|
+
conn.sendall((json.dumps(reply, allow_nan=False) + '\n').encode('utf-8'))
|
|
233
|
+
except OSError:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
def worker(conn):
|
|
237
|
+
try:
|
|
238
|
+
with conn:
|
|
239
|
+
conn.settimeout(READ_DEADLINE_S)
|
|
240
|
+
reply, halt = _handle(conn, token, manifest, weights, stamp, guarded_score, config)
|
|
241
|
+
send(conn, reply)
|
|
242
|
+
if halt:
|
|
243
|
+
stop.set()
|
|
244
|
+
finally:
|
|
245
|
+
slots.release()
|
|
246
|
+
|
|
247
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
248
|
+
try:
|
|
249
|
+
server.bind(('127.0.0.1', 0))
|
|
250
|
+
server.listen(MAX_CLIENTS)
|
|
251
|
+
server.settimeout(0.25)
|
|
252
|
+
_write_private_json(state_path, {'schemaVersion': 1, 'pid': pid, 'port': server.getsockname()[1], 'token': token, 'startedAt': time.time()})
|
|
253
|
+
try:
|
|
254
|
+
lock_path.unlink()
|
|
255
|
+
except FileNotFoundError:
|
|
256
|
+
pass
|
|
257
|
+
last = time.monotonic()
|
|
258
|
+
# A server whose state file no longer names it is undiscoverable (replaced or corrupted): exit, never orphan.
|
|
259
|
+
while not stop.is_set() and time.monotonic() - last <= idle and _owns_state(state_path, pid, token):
|
|
260
|
+
try:
|
|
261
|
+
conn, _ = server.accept()
|
|
262
|
+
except socket.timeout:
|
|
263
|
+
continue
|
|
264
|
+
last = time.monotonic()
|
|
265
|
+
if not slots.acquire(blocking=False):
|
|
266
|
+
with conn:
|
|
267
|
+
conn.settimeout(0.2)
|
|
268
|
+
try:
|
|
269
|
+
_read_request_line(conn)
|
|
270
|
+
except (OSError, ValueError):
|
|
271
|
+
pass
|
|
272
|
+
send(conn, {'ok': False, 'reason': 'busy'})
|
|
273
|
+
continue
|
|
274
|
+
threading.Thread(target=worker, args=(conn,), daemon=True).start()
|
|
275
|
+
finally:
|
|
276
|
+
server.close()
|
|
277
|
+
if _owns_state(state_path, pid, token, missing=False):
|
|
278
|
+
try:
|
|
279
|
+
state_path.unlink()
|
|
280
|
+
except OSError:
|
|
281
|
+
pass
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _owns_state(state_path, pid, token, missing=False):
|
|
286
|
+
"""True while the state file still names this server. A transient Windows sharing violation counts as owned."""
|
|
287
|
+
try:
|
|
288
|
+
data = json.loads(state_path.read_text(encoding='utf-8'))
|
|
289
|
+
except FileNotFoundError:
|
|
290
|
+
return missing
|
|
291
|
+
except OSError:
|
|
292
|
+
return True
|
|
293
|
+
except ValueError:
|
|
294
|
+
return False
|
|
295
|
+
return isinstance(data, dict) and data.get('pid') == pid and data.get('token') == token
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def serve_main(argv, scorer_factory=None):
|
|
299
|
+
if len(argv) != 3:
|
|
300
|
+
return 2
|
|
301
|
+
return serve(json.loads(argv[0]), argv[1], argv[2], scorer_factory=scorer_factory)
|
|
302
|
+
|
|
303
|
+
|
|
47
304
|
def source_revision(direct_url_text):
|
|
48
305
|
"""Return the PEP 610 VCS commit recorded at install time, or None when unprovable."""
|
|
49
306
|
try:
|
|
@@ -76,6 +333,8 @@ def main():
|
|
|
76
333
|
if sys.argv[1:] == ['--provenance']:
|
|
77
334
|
print(json.dumps(provenance(), allow_nan=False))
|
|
78
335
|
return 0
|
|
336
|
+
if sys.argv[1:2] == ['--serve']:
|
|
337
|
+
return serve_main(sys.argv[2:])
|
|
79
338
|
manifest = json.loads(sys.argv[1])
|
|
80
339
|
raw = sys.stdin.buffer.read(2 * 1024 * 1024 + 1)
|
|
81
340
|
if len(raw) > 2 * 1024 * 1024:
|