@crvouga/mockingbird-service-hermes 0.1.0
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/API_EVIDENCE.md +418 -0
- package/CHANGELOG.md +5 -0
- package/DISCOVERY.md +54 -0
- package/README.md +290 -0
- package/SUPPORT.md +16 -0
- package/dist/chunk-E3W77NV6.js +821 -0
- package/dist/chunk-E3W77NV6.js.map +7 -0
- package/dist/chunk-RLLHIXKE.js +3618 -0
- package/dist/chunk-RLLHIXKE.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1742 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/evidence/fingerprint-python.json +156 -0
- package/evidence/pinned-oracle.json +616 -0
- package/openapi.yaml +342 -0
- package/oracle/README.md +99 -0
- package/oracle/compare.mjs +274 -0
- package/oracle/comparison.mjs +16 -0
- package/oracle/comparison.test.mjs +41 -0
- package/oracle/fetch.py +29 -0
- package/oracle/requirements.lock +11 -0
- package/oracle/server.py +227 -0
- package/oracle/source-lock.json +52 -0
- package/oracle/sources.py +71 -0
- package/oracle/test_sources.py +50 -0
- package/package.json +117 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
4
|
+
import { resolve } from "node:path"
|
|
5
|
+
import { createInterface } from "node:readline"
|
|
6
|
+
import { createServer } from "../dist/server.js"
|
|
7
|
+
import { normalizeObservation } from "./comparison.mjs"
|
|
8
|
+
|
|
9
|
+
if (!process.argv.includes("--run") || process.env.MOCKINGBIRD_HERMES_ORACLE_APPROVED !== "1") {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"Opt-in oracle requires --run and explicit approved disposable-process/database execution",
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
const root = resolve(import.meta.dirname, "../../../..")
|
|
15
|
+
const python = process.env.HERMES_ORACLE_PYTHON
|
|
16
|
+
if (!python)
|
|
17
|
+
throw new Error("Set HERMES_ORACLE_PYTHON to the approved local virtualenv interpreter")
|
|
18
|
+
const source = resolve(root, ".mockingbird/hermes-evidence/v2026.8.31")
|
|
19
|
+
const scratch = resolve(root, ".mockingbird/hermes-oracle", crypto.randomUUID())
|
|
20
|
+
await mkdir(scratch, { recursive: true })
|
|
21
|
+
const lock = JSON.parse(await readFile(new URL("source-lock.json", import.meta.url), "utf8"))
|
|
22
|
+
const mock = await createServer()
|
|
23
|
+
let currentTime = 1700000000
|
|
24
|
+
let upstream
|
|
25
|
+
const epochs = []
|
|
26
|
+
const comparisons = []
|
|
27
|
+
const ids = { real: new Map(), mock: new Map() }
|
|
28
|
+
|
|
29
|
+
const launch = () =>
|
|
30
|
+
new Promise((resolveLaunch, reject) => {
|
|
31
|
+
const child = spawn(
|
|
32
|
+
python,
|
|
33
|
+
[
|
|
34
|
+
resolve(import.meta.dirname, "server.py"),
|
|
35
|
+
"--sources",
|
|
36
|
+
source,
|
|
37
|
+
"--state",
|
|
38
|
+
resolve(scratch, "runs.db"),
|
|
39
|
+
"--time",
|
|
40
|
+
String(currentTime),
|
|
41
|
+
"--allow-disposable-state",
|
|
42
|
+
],
|
|
43
|
+
{
|
|
44
|
+
cwd: root,
|
|
45
|
+
env: { PATH: process.env.PATH, PYTHONDONTWRITEBYTECODE: "1", HERMES_HOME: scratch },
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
const lines = createInterface({ input: child.stdout })
|
|
50
|
+
let stderr = ""
|
|
51
|
+
child.stderr.on("data", (chunk) => {
|
|
52
|
+
stderr += chunk.toString()
|
|
53
|
+
})
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
child.kill("SIGTERM")
|
|
56
|
+
reject(new Error(`Oracle startup deadline: ${stderr}`))
|
|
57
|
+
}, 10000)
|
|
58
|
+
child.once("error", (error) => {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
reject(error)
|
|
61
|
+
})
|
|
62
|
+
child.once("exit", (code) => {
|
|
63
|
+
clearTimeout(timer)
|
|
64
|
+
reject(new Error(`Oracle exited before ready (${code}): ${stderr}`))
|
|
65
|
+
})
|
|
66
|
+
lines.once("line", (line) => {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
try {
|
|
69
|
+
const ready = JSON.parse(line)
|
|
70
|
+
epochs.push({ ...ready, url: undefined })
|
|
71
|
+
resolveLaunch({ child, url: ready.url, stderr: () => stderr })
|
|
72
|
+
} catch (error) {
|
|
73
|
+
child.kill("SIGTERM")
|
|
74
|
+
reject(error)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
const raw = async (url, method, path, body, key = "") => {
|
|
79
|
+
const response = await fetch(`${url}${path}`, {
|
|
80
|
+
method,
|
|
81
|
+
headers: { "content-type": "application/json", "Idempotency-Key": key },
|
|
82
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
83
|
+
signal: AbortSignal.timeout(5000),
|
|
84
|
+
})
|
|
85
|
+
return {
|
|
86
|
+
status: response.status,
|
|
87
|
+
replayed: response.headers.get("Idempotency-Replayed"),
|
|
88
|
+
body: await response.json(),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const checkedControl = async (url, path, body) => {
|
|
92
|
+
const response = await raw(url, "POST", path, body)
|
|
93
|
+
assert.equal(response.status, 200, `control ${path}: ${JSON.stringify(response.body)}`)
|
|
94
|
+
return response
|
|
95
|
+
}
|
|
96
|
+
const control = (body) => checkedControl(upstream.url, "/__oracle/control", body)
|
|
97
|
+
const admin = (path, body) => checkedControl(mock.url, `/__admin/${path}`, body)
|
|
98
|
+
const equal = (label, real, mocked) => {
|
|
99
|
+
const expected = normalizeObservation(real, ids.real)
|
|
100
|
+
const actual = normalizeObservation(mocked, ids.mock)
|
|
101
|
+
assert.deepEqual(actual, expected, label)
|
|
102
|
+
comparisons.push({ label, status: real.status, observation: expected.body })
|
|
103
|
+
}
|
|
104
|
+
const poll = async (pair, label) => {
|
|
105
|
+
equal(
|
|
106
|
+
label,
|
|
107
|
+
await raw(upstream.url, "GET", `/v1/runs/${pair.real}`),
|
|
108
|
+
await raw(mock.url, "GET", `/v1/runs/${pair.mock}`),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
const waitRunning = async (id) => {
|
|
112
|
+
const deadline = performance.now() + 3000
|
|
113
|
+
while (true) {
|
|
114
|
+
const response = await raw(upstream.url, "GET", `/v1/runs/${id}`)
|
|
115
|
+
if (response.body.status === "running") return
|
|
116
|
+
if (performance.now() > deadline || ["failed", "cancelled"].includes(response.body.status))
|
|
117
|
+
throw new Error(`Upstream executor did not reach running: ${JSON.stringify(response)}`)
|
|
118
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const submit = async (name, key = name) => {
|
|
122
|
+
const body = { input: "synthetic oracle input" }
|
|
123
|
+
const real = await raw(upstream.url, "POST", "/v1/runs", body, key)
|
|
124
|
+
const mocked = await raw(mock.url, "POST", "/v1/runs", body, key)
|
|
125
|
+
assert.equal(real.status, 202)
|
|
126
|
+
assert.equal(mocked.status, 202)
|
|
127
|
+
const pair = { real: real.body.run_id, mock: mocked.body.run_id, key, body }
|
|
128
|
+
ids.real.set(pair.real, name)
|
|
129
|
+
ids.mock.set(pair.mock, name)
|
|
130
|
+
equal(`${name}:admission`, real, mocked)
|
|
131
|
+
await waitRunning(pair.real)
|
|
132
|
+
await admin(`hermes/runs/${pair.mock}/observe`, { status: "running" })
|
|
133
|
+
await poll(pair, `${name}:running`)
|
|
134
|
+
return pair
|
|
135
|
+
}
|
|
136
|
+
const replay = async (pair, label, body = pair.body) =>
|
|
137
|
+
equal(
|
|
138
|
+
label,
|
|
139
|
+
await raw(upstream.url, "POST", "/v1/runs", body, pair.key),
|
|
140
|
+
await raw(mock.url, "POST", "/v1/runs", body, pair.key),
|
|
141
|
+
)
|
|
142
|
+
const stop = async (pair) =>
|
|
143
|
+
equal(
|
|
144
|
+
`${pair.key}:stop`,
|
|
145
|
+
await raw(upstream.url, "POST", `/v1/runs/${pair.real}/stop`, {}),
|
|
146
|
+
await raw(mock.url, "POST", `/v1/runs/${pair.mock}/stop`, {}),
|
|
147
|
+
)
|
|
148
|
+
const settle = async (pair, status) => {
|
|
149
|
+
const result =
|
|
150
|
+
status === "cancelled"
|
|
151
|
+
? { interrupted: true }
|
|
152
|
+
: status === "failed"
|
|
153
|
+
? { failed: true, error: "synthetic failure" }
|
|
154
|
+
: { final_response: "synthetic output" }
|
|
155
|
+
await control({ settle: pair.real, result })
|
|
156
|
+
await admin(`hermes/runs/${pair.mock}/observe`, {
|
|
157
|
+
status,
|
|
158
|
+
last_event: `run.${status}`,
|
|
159
|
+
...(status === "completed"
|
|
160
|
+
? {
|
|
161
|
+
output: "synthetic output",
|
|
162
|
+
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
...(status === "failed" ? { error: "synthetic failure" } : {}),
|
|
166
|
+
})
|
|
167
|
+
await poll(pair, `${pair.key}:${status}`)
|
|
168
|
+
}
|
|
169
|
+
const setTime = async (time) => {
|
|
170
|
+
currentTime = time
|
|
171
|
+
await control({ time })
|
|
172
|
+
await admin("clock", { set: time * 1000, freeze: true })
|
|
173
|
+
}
|
|
174
|
+
const crash = async () => {
|
|
175
|
+
const old = upstream
|
|
176
|
+
upstream = undefined
|
|
177
|
+
if (old.child.exitCode !== null || old.child.signalCode !== null)
|
|
178
|
+
throw new Error(`Oracle exited unexpectedly: ${old.stderr()}`)
|
|
179
|
+
let timer
|
|
180
|
+
const exited = new Promise((resolveExit, reject) => {
|
|
181
|
+
old.child.once("exit", (code) => {
|
|
182
|
+
clearTimeout(timer)
|
|
183
|
+
if (code === 0) resolveExit()
|
|
184
|
+
else reject(new Error(`Oracle process exited with ${code}`))
|
|
185
|
+
})
|
|
186
|
+
timer = setTimeout(() => {
|
|
187
|
+
old.child.kill("SIGTERM")
|
|
188
|
+
reject(new Error("Oracle crash endpoint did not exit the owned process"))
|
|
189
|
+
}, 6000)
|
|
190
|
+
})
|
|
191
|
+
// Observe rejection immediately while awaiting the expected lost connection.
|
|
192
|
+
const outcome = exited.then(
|
|
193
|
+
() => null,
|
|
194
|
+
(error) => error,
|
|
195
|
+
)
|
|
196
|
+
await fetch(`${old.url}/__oracle/crash`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
signal: AbortSignal.timeout(5000),
|
|
199
|
+
}).catch(() => {})
|
|
200
|
+
const error = await outcome
|
|
201
|
+
if (error) throw error
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
upstream = await launch()
|
|
205
|
+
await admin("clock", { set: currentTime * 1000, freeze: true })
|
|
206
|
+
const cancelled = await submit("cancelled")
|
|
207
|
+
await replay(cancelled, "active replay")
|
|
208
|
+
await replay(cancelled, "changed body conflicts", { input: "different synthetic input" })
|
|
209
|
+
await stop(cancelled)
|
|
210
|
+
await settle(cancelled, "cancelled")
|
|
211
|
+
await replay(cancelled, "terminal replay")
|
|
212
|
+
const completed = await submit("completion-race")
|
|
213
|
+
await stop(completed)
|
|
214
|
+
await settle(completed, "completed")
|
|
215
|
+
const failed = await submit("failure")
|
|
216
|
+
await settle(failed, "failed")
|
|
217
|
+
const cached = await submit("cache-boundary", "")
|
|
218
|
+
await settle(cached, "completed")
|
|
219
|
+
await setTime(1700000000 + 3600)
|
|
220
|
+
await control({ sweep: true })
|
|
221
|
+
await admin("hermes/sweep", {})
|
|
222
|
+
await poll(cached, "cache TTL equality retains keyless terminal observation")
|
|
223
|
+
await setTime(1700000000 + 3600.001)
|
|
224
|
+
await control({ sweep: true })
|
|
225
|
+
await admin("hermes/sweep", {})
|
|
226
|
+
await poll(cached, "cache TTL strictly greater expires keyless terminal observation")
|
|
227
|
+
const unfinished = await submit("unfinished")
|
|
228
|
+
const keyless = await submit("keyless", "")
|
|
229
|
+
await crash()
|
|
230
|
+
upstream = await launch()
|
|
231
|
+
assert.notEqual(epochs[0].pid, epochs[1].pid, "restart must create a fresh process")
|
|
232
|
+
await admin("hermes/restart", { owner: "stale" })
|
|
233
|
+
await poll(completed, "terminal survives real process restart")
|
|
234
|
+
await poll(unfinished, "unfinished run interrupted after real process restart")
|
|
235
|
+
await poll(keyless, "keyless run lost after real process restart")
|
|
236
|
+
await replay(unfinished, "interrupted reservation replay")
|
|
237
|
+
await setTime(1700000000 + 86400)
|
|
238
|
+
await replay(completed, "durable TTL equality retains reservation")
|
|
239
|
+
await setTime(1700000000 + 86400.001)
|
|
240
|
+
await poll(completed, "GET alone does not prune durable history")
|
|
241
|
+
const renewed = await submit("renewed", completed.key)
|
|
242
|
+
assert.notEqual(renewed.real, completed.real)
|
|
243
|
+
assert.notEqual(renewed.mock, completed.mock)
|
|
244
|
+
await control({ sweep: true })
|
|
245
|
+
await admin("hermes/sweep", {})
|
|
246
|
+
await poll(completed, "pruned and swept result returns ordinary404")
|
|
247
|
+
await settle(renewed, "completed")
|
|
248
|
+
const report = {
|
|
249
|
+
release: lock.release,
|
|
250
|
+
commit: lock.commit,
|
|
251
|
+
sources: lock.sources,
|
|
252
|
+
epochs,
|
|
253
|
+
comparisons,
|
|
254
|
+
limitations: [
|
|
255
|
+
"scripted executor; no inference",
|
|
256
|
+
"ordinary default test listener; no credential enforcement claim",
|
|
257
|
+
"routing/session/approval/process-tool integrations substituted; no hosted rooms",
|
|
258
|
+
"controlled clock and explicit sweep; no background timing claim",
|
|
259
|
+
],
|
|
260
|
+
}
|
|
261
|
+
await writeFile(resolve(scratch, "report.json"), `${JSON.stringify(report, null, 2)}\n`)
|
|
262
|
+
console.log(
|
|
263
|
+
JSON.stringify({ comparisons: comparisons.length, report: resolve(scratch, "report.json") }),
|
|
264
|
+
)
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (upstream) console.error(upstream.stderr())
|
|
267
|
+
throw error
|
|
268
|
+
} finally {
|
|
269
|
+
try {
|
|
270
|
+
if (upstream) await crash()
|
|
271
|
+
} finally {
|
|
272
|
+
await mock.close()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Map only public identity fields and the exact pinned missing-run error template. */
|
|
2
|
+
export const normalizeObservation = (response, ids) => {
|
|
3
|
+
const body = structuredClone(response.body)
|
|
4
|
+
for (const field of ["run_id", "session_id"]) {
|
|
5
|
+
if (typeof body[field] === "string" && ids.has(body[field])) body[field] = ids.get(body[field])
|
|
6
|
+
}
|
|
7
|
+
if (response.status === 404 && body.error?.code === "run_not_found") {
|
|
8
|
+
for (const [id, name] of ids) {
|
|
9
|
+
if (body.error.message === `Run not found: ${id}`) {
|
|
10
|
+
body.error.message = `Run not found: ${name}`
|
|
11
|
+
break
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { ...response, body }
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { test } from "node:test"
|
|
3
|
+
import { normalizeObservation } from "./comparison.mjs"
|
|
4
|
+
|
|
5
|
+
const realIds = new Map([["real-id", "run"]])
|
|
6
|
+
const mockIds = new Map([["mock-id", "run"]])
|
|
7
|
+
const same = (real, mock) =>
|
|
8
|
+
assert.deepEqual(normalizeObservation(real, realIds), normalizeObservation(mock, mockIds))
|
|
9
|
+
|
|
10
|
+
test("identity fields map without mutating the response", () => {
|
|
11
|
+
const real = { status: 200, body: { run_id: "real-id", session_id: "real-id" } }
|
|
12
|
+
same(real, { status: 200, body: { run_id: "mock-id", session_id: "mock-id" } })
|
|
13
|
+
assert.equal(real.body.run_id, "real-id")
|
|
14
|
+
})
|
|
15
|
+
test("result and arbitrary error strings retain generated IDs", () => {
|
|
16
|
+
for (const field of ["output", "error"]) {
|
|
17
|
+
assert.throws(
|
|
18
|
+
() =>
|
|
19
|
+
same(
|
|
20
|
+
{ status: 200, body: { [field]: "result real-id" } },
|
|
21
|
+
{ status: 200, body: { [field]: "result mock-id" } },
|
|
22
|
+
),
|
|
23
|
+
assert.AssertionError,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
test("missing-run normalization requires the exact error template", () => {
|
|
28
|
+
const missing = (message) => ({
|
|
29
|
+
status: 404,
|
|
30
|
+
body: { error: { code: "run_not_found", message } },
|
|
31
|
+
})
|
|
32
|
+
same(missing("Run not found: real-id"), missing("Run not found: mock-id"))
|
|
33
|
+
assert.throws(
|
|
34
|
+
() => same(missing("Run not found: real-id"), missing("Different: mock-id")),
|
|
35
|
+
assert.AssertionError,
|
|
36
|
+
)
|
|
37
|
+
assert.throws(
|
|
38
|
+
() => same(missing("Run not found: real-id"), missing("Run not found: mock-id suffix")),
|
|
39
|
+
assert.AssertionError,
|
|
40
|
+
)
|
|
41
|
+
})
|
package/oracle/fetch.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Explicitly fetch hash-locked source files; never install or execute upstream."""
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.request import urlopen
|
|
6
|
+
from sources import LOCK, verify
|
|
7
|
+
|
|
8
|
+
parser = argparse.ArgumentParser()
|
|
9
|
+
parser.add_argument("--fetch", action="store_true")
|
|
10
|
+
args = parser.parse_args()
|
|
11
|
+
if not args.fetch:
|
|
12
|
+
parser.error("pass --fetch to download the pinned source files")
|
|
13
|
+
root = Path(__file__).resolve().parents[4]
|
|
14
|
+
destination = root / ".mockingbird/hermes-evidence/v2026.8.31"
|
|
15
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
for name, entry in LOCK["sources"].items():
|
|
17
|
+
path = destination / name
|
|
18
|
+
if path.exists():
|
|
19
|
+
data = path.read_bytes()
|
|
20
|
+
else:
|
|
21
|
+
url = f"https://raw.githubusercontent.com/NousResearch/hermes-agent/{LOCK['commit']}/{entry['path']}"
|
|
22
|
+
with urlopen(url, timeout=30) as response:
|
|
23
|
+
data = response.read()
|
|
24
|
+
if hashlib.sha256(data).hexdigest() != entry["sha256"]:
|
|
25
|
+
raise ValueError(f"Pinned source hash mismatch: {name}")
|
|
26
|
+
if not path.exists():
|
|
27
|
+
path.write_bytes(data)
|
|
28
|
+
verify(destination)
|
|
29
|
+
print(f"Verified {len(LOCK['sources'])} files at {LOCK['commit']}")
|
package/oracle/server.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Opt-in loopback oracle: pinned run/store code, scripted non-inference executor."""
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import contextvars
|
|
6
|
+
import hmac
|
|
7
|
+
import itertools
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sqlite3
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import types
|
|
19
|
+
import typing
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from sources import LOCK, definitions, load_module, verify
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def package(name):
|
|
26
|
+
module = types.ModuleType(name)
|
|
27
|
+
module.__path__ = []
|
|
28
|
+
sys.modules[name] = module
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
parser = argparse.ArgumentParser()
|
|
34
|
+
parser.add_argument("--sources", required=True)
|
|
35
|
+
parser.add_argument("--state", required=True)
|
|
36
|
+
parser.add_argument("--time", type=float, required=True)
|
|
37
|
+
parser.add_argument("--allow-disposable-state", action="store_true")
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
if not args.allow_disposable_state:
|
|
40
|
+
parser.error("explicit --allow-disposable-state is required")
|
|
41
|
+
root = Path(__file__).resolve().parents[4]
|
|
42
|
+
state = Path(args.state).resolve()
|
|
43
|
+
state.relative_to(root / ".mockingbird" / "hermes-oracle")
|
|
44
|
+
if not state.parent.is_dir():
|
|
45
|
+
parser.error("create a fresh oracle state directory before launch")
|
|
46
|
+
source = Path(args.sources).resolve()
|
|
47
|
+
verify(source)
|
|
48
|
+
if not (3, 11) <= sys.version_info[:2] < (3, 14):
|
|
49
|
+
parser.error("pinned Hermes requires Python >=3.11,<3.14")
|
|
50
|
+
import aiohttp
|
|
51
|
+
from aiohttp import web
|
|
52
|
+
if aiohttp.__version__ != LOCK["aiohttp"]:
|
|
53
|
+
parser.error("aiohttp version differs from source-lock.json")
|
|
54
|
+
|
|
55
|
+
provenance = {}
|
|
56
|
+
base = {**{key: value for key, value in vars(typing).items() if not key.startswith("__")}, "asyncio": asyncio, "contextlib": contextlib,
|
|
57
|
+
"ContextVar": contextvars.ContextVar, "contextmanager": contextlib.contextmanager,
|
|
58
|
+
"hmac": hmac, "itertools": itertools, "json": json, "logging": logging,
|
|
59
|
+
"os": os, "re": re, "sqlite3": sqlite3, "subprocess": subprocess,
|
|
60
|
+
"sys": sys, "threading": threading, "time": time, "Path": Path,
|
|
61
|
+
"logger": logging.getLogger("oracle"), "web": web}
|
|
62
|
+
def selected(filename, names, extra=None):
|
|
63
|
+
namespace = {**base, **(extra or {})}
|
|
64
|
+
provenance[filename] = definitions(source / filename, names, namespace)
|
|
65
|
+
return namespace
|
|
66
|
+
for name in ["agent", "gateway", "tools", "hermes_cli"]:
|
|
67
|
+
package(name)
|
|
68
|
+
safety = types.ModuleType("agent.file_safety")
|
|
69
|
+
safety.__dict__.update(selected("file_safety.py", ["_BLOCKED_PROJECT_ENV_BASENAMES"]))
|
|
70
|
+
sys.modules[safety.__name__] = safety
|
|
71
|
+
redactor = load_module(source / "agent_redact.py", "agent.redact")
|
|
72
|
+
interrupt = load_module(source / "interrupt_compat.py", "agent.interrupt_compat")
|
|
73
|
+
sqlite_runtime = load_module(source / "sqlite_runtime.py", "hermes_cli.sqlite_runtime")
|
|
74
|
+
sys.modules[sqlite_runtime.__name__] = sqlite_runtime
|
|
75
|
+
state_module = types.ModuleType("hermes_state")
|
|
76
|
+
state_module.__dict__.update(selected("hermes_state.py", ["apply_wal_with_fallback"], {
|
|
77
|
+
"_is_sqlite_wal_reset_vulnerable": sqlite_runtime.is_sqlite_wal_reset_vulnerable,
|
|
78
|
+
}))
|
|
79
|
+
sys.modules["hermes_state"] = state_module
|
|
80
|
+
status = types.ModuleType("gateway.status")
|
|
81
|
+
status.__dict__.update(selected("gateway_status.py", ["_pid_exists", "get_process_start_time"]))
|
|
82
|
+
sys.modules["gateway.status"] = status
|
|
83
|
+
# Only these execution integration hooks are substituted. They never dispatch
|
|
84
|
+
# tools, persist sessions, spawn child tools or register real approval work.
|
|
85
|
+
approval = types.ModuleType("tools.approval")
|
|
86
|
+
for name in ["register_gateway_notify", "unregister_gateway_notify", "reset_current_session_key", "set_current_session_key"]:
|
|
87
|
+
setattr(approval, name, lambda *a, **k: None)
|
|
88
|
+
sys.modules[approval.__name__] = approval
|
|
89
|
+
session = types.ModuleType("gateway.session_context")
|
|
90
|
+
session.clear_session_vars = lambda *a: None
|
|
91
|
+
sys.modules[session.__name__] = session
|
|
92
|
+
clock = [args.time]
|
|
93
|
+
controlled_time = types.SimpleNamespace(time=lambda: clock[0])
|
|
94
|
+
store_module = load_module(source / "api_server_run_idempotency.py", "oracle_store")
|
|
95
|
+
store_module.time = controlled_time
|
|
96
|
+
runs = load_module(source / "api_server_runs.py", "oracle_runs")
|
|
97
|
+
runs.time = controlled_time
|
|
98
|
+
api = types.SimpleNamespace(**selected("api_server.py", [
|
|
99
|
+
"_openai_error", "_redact_api_error_text", "_request_agent_overrides",
|
|
100
|
+
"_approval_event_choices", "_expected_api_key", "_check_auth", "_parse_session_key_header",
|
|
101
|
+
], {"redact_sensitive_text": redactor.redact_sensitive_text}))
|
|
102
|
+
api._api_request_profile = contextvars.ContextVar("oracle_profile", default="default")
|
|
103
|
+
api._api_request_browser_control_principal = contextvars.ContextVar("oracle_principal", default=None)
|
|
104
|
+
api._api_request_browser_control_transport_family = contextvars.ContextVar("oracle_transport", default=None)
|
|
105
|
+
api._ProviderAuthResolutionError = type("UnexercisedProviderAuthError", (Exception,), {})
|
|
106
|
+
api._publish_turn_process_ownership = lambda *a: None
|
|
107
|
+
api._clear_turn_process_ownership = lambda *a: None
|
|
108
|
+
api._reap_disconnected_agent_processes = lambda *a, **k: None
|
|
109
|
+
api.request_hard_interrupt = interrupt.request_hard_interrupt
|
|
110
|
+
# Preserve the selected functions' original global context variable binding.
|
|
111
|
+
for name in ["_expected_api_key", "_check_auth"]:
|
|
112
|
+
getattr(api, name).__globals__["_api_request_profile"] = api._api_request_profile
|
|
113
|
+
room = selected("room_grants.py", ["_room_grant_token"])
|
|
114
|
+
|
|
115
|
+
class ScriptedAgent:
|
|
116
|
+
session_prompt_tokens = 0
|
|
117
|
+
session_completion_tokens = 0
|
|
118
|
+
session_total_tokens = 0
|
|
119
|
+
def __init__(self):
|
|
120
|
+
self.release = threading.Event()
|
|
121
|
+
self.result = {"final_response": "synthetic output"}
|
|
122
|
+
def hard_interrupt(self, message=None):
|
|
123
|
+
# Hold acknowledgement until the fixture selects cancellation or a
|
|
124
|
+
# completion-winning race; the upstream stop handler owns stopping.
|
|
125
|
+
self.interrupted = True
|
|
126
|
+
def run_conversation(self, **kwargs):
|
|
127
|
+
if not self.release.wait(30):
|
|
128
|
+
raise TimeoutError("oracle executor was not explicitly settled")
|
|
129
|
+
return self.result
|
|
130
|
+
|
|
131
|
+
class Adapter:
|
|
132
|
+
_model_name = "hermes-agent"
|
|
133
|
+
_api_key = ""
|
|
134
|
+
_MAX_SESSION_HEADER_LEN = 256
|
|
135
|
+
_RUN_STATUS_TTL = 3600
|
|
136
|
+
_RUN_STREAM_TTL = 300
|
|
137
|
+
_room_grant_token = staticmethod(room["_room_grant_token"])
|
|
138
|
+
_expected_api_key = api._expected_api_key
|
|
139
|
+
_check_auth = api._check_auth
|
|
140
|
+
_parse_session_key_header = api._parse_session_key_header
|
|
141
|
+
def __init__(self):
|
|
142
|
+
self._background_tasks = set()
|
|
143
|
+
self._stopping_run_ids = set()
|
|
144
|
+
self._active_run_agents = {}
|
|
145
|
+
self._active_run_tasks = {}
|
|
146
|
+
self._response_store = {}
|
|
147
|
+
runs._initialize_run_state(self, store_factory=lambda: store_module.RunIdempotencyStore(str(state)))
|
|
148
|
+
def _make_run_event_callback(self, run_id, loop):
|
|
149
|
+
return runs._make_run_event_callback(self, run_id, loop, _api_server=api)
|
|
150
|
+
def _run_idempotency_scope(self, request):
|
|
151
|
+
return runs._run_idempotency_scope(self, request, _api_server=api)
|
|
152
|
+
def _check_run_auth(self, request, *, permission):
|
|
153
|
+
return runs._check_run_auth(self, request, permission=permission, _api_server=api)
|
|
154
|
+
async def _normalize_room_dispatch(self, request, body):
|
|
155
|
+
if self._room_grant_token(request) or "hosted_room_dispatch" in body or "_room_execution_policy" in body:
|
|
156
|
+
raise ValueError("hosted rooms are outside this oracle")
|
|
157
|
+
return body, None
|
|
158
|
+
def _resolve_route(self, model):
|
|
159
|
+
return None
|
|
160
|
+
def _request_route_conflict_error(self, **kwargs):
|
|
161
|
+
return None
|
|
162
|
+
def _concurrency_limited_response(self):
|
|
163
|
+
return None
|
|
164
|
+
async def _conversation_history_for_session(self, session):
|
|
165
|
+
return []
|
|
166
|
+
def _profile_scope(self, profile):
|
|
167
|
+
return contextlib.nullcontext()
|
|
168
|
+
def _bind_api_server_session(self, **kwargs):
|
|
169
|
+
return []
|
|
170
|
+
def _activate_admitted_request(self):
|
|
171
|
+
pass
|
|
172
|
+
def _create_agent(self, **kwargs):
|
|
173
|
+
return ScriptedAgent()
|
|
174
|
+
for name in ["_set_run_status", "_durable_run_status", "_request_owns_run"]:
|
|
175
|
+
setattr(Adapter, name, getattr(runs, name))
|
|
176
|
+
adapter = Adapter()
|
|
177
|
+
|
|
178
|
+
async def submit(request):
|
|
179
|
+
error = adapter._check_auth(request)
|
|
180
|
+
if error is not None:
|
|
181
|
+
return error
|
|
182
|
+
return await runs._handle_runs(adapter, request, _api_server=api)
|
|
183
|
+
async def poll(request):
|
|
184
|
+
return await runs._handle_get_run(adapter, request, _api_server=api)
|
|
185
|
+
async def stop(request):
|
|
186
|
+
return await runs._handle_stop_run(adapter, request, _api_server=api)
|
|
187
|
+
async def control(request):
|
|
188
|
+
body = await request.json()
|
|
189
|
+
if "time" in body:
|
|
190
|
+
clock[0] = float(body["time"])
|
|
191
|
+
if body.get("sweep"):
|
|
192
|
+
runs._sweep_orphaned_runs_once(adapter, clock[0])
|
|
193
|
+
if "settle" in body:
|
|
194
|
+
run_id = body["settle"]
|
|
195
|
+
agent = adapter._active_run_agents[run_id]
|
|
196
|
+
agent.result = body["result"]
|
|
197
|
+
agent.release.set()
|
|
198
|
+
await asyncio.wait_for(asyncio.shield(adapter._active_run_tasks[run_id]), 5)
|
|
199
|
+
return web.json_response({"ok": True})
|
|
200
|
+
async def crash(request):
|
|
201
|
+
# Deliberately omit executor cleanup and SQLite close: next invocation
|
|
202
|
+
# opens the same disposable database in a genuinely new process.
|
|
203
|
+
os._exit(0)
|
|
204
|
+
async def serve():
|
|
205
|
+
app = web.Application()
|
|
206
|
+
app.router.add_post("/v1/runs", submit)
|
|
207
|
+
app.router.add_get("/v1/runs/{run_id}", poll)
|
|
208
|
+
app.router.add_post("/v1/runs/{run_id}/stop", stop)
|
|
209
|
+
app.router.add_post("/__oracle/control", control)
|
|
210
|
+
app.router.add_post("/__oracle/crash", crash)
|
|
211
|
+
runner = web.AppRunner(app, access_log=None)
|
|
212
|
+
await runner.setup()
|
|
213
|
+
site = web.TCPSite(runner, "127.0.0.1", 0)
|
|
214
|
+
await site.start()
|
|
215
|
+
port = site._server.sockets[0].getsockname()[1]
|
|
216
|
+
print(json.dumps({"url": f"http://127.0.0.1:{port}", "commit": LOCK["commit"],
|
|
217
|
+
"python": sys.version.split()[0], "aiohttp": aiohttp.__version__,
|
|
218
|
+
"sqlite": sqlite3.sqlite_version, "pid": os.getpid(),
|
|
219
|
+
"journal_mode": adapter._run_idempotency_store._conn.execute("PRAGMA journal_mode").fetchone()[0],
|
|
220
|
+
"dependencies": {name: importlib.metadata.version(name) for name in ["aiohttp", "aiohappyeyeballs", "aiosignal", "attrs", "frozenlist", "idna", "multidict", "propcache", "typing-extensions", "yarl"]},
|
|
221
|
+
"definitions": provenance}), flush=True)
|
|
222
|
+
await asyncio.Event().wait()
|
|
223
|
+
asyncio.run(serve())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release": "v2026.8.31",
|
|
3
|
+
"commit": "29112bef099274229cadff79cdff7bf7b99c4b77",
|
|
4
|
+
"python": ">=3.11,<3.14",
|
|
5
|
+
"aiohttp": "3.14.3",
|
|
6
|
+
"sources": {
|
|
7
|
+
"api_server_runs.py": {
|
|
8
|
+
"path": "gateway/platforms/api_server_runs.py",
|
|
9
|
+
"sha256": "048ae843592d701ff47437bd8edd47cd64ca6c0fdf88a71045bdb6fb337fbc63"
|
|
10
|
+
},
|
|
11
|
+
"api_server_run_idempotency.py": {
|
|
12
|
+
"path": "gateway/platforms/api_server_run_idempotency.py",
|
|
13
|
+
"sha256": "746904b3b6ed45d8359655b2e87c1f101394b13f6253e718ac9a83acd46174f2"
|
|
14
|
+
},
|
|
15
|
+
"api_server.py": {
|
|
16
|
+
"path": "gateway/platforms/api_server.py",
|
|
17
|
+
"sha256": "6aec6687d47c81b3567e930882a499c5c5c041ecbe40e1d08f93038212401962"
|
|
18
|
+
},
|
|
19
|
+
"hermes_state.py": {
|
|
20
|
+
"path": "hermes_state.py",
|
|
21
|
+
"sha256": "696fd2d37d66a599228f5b1be5d5b76423b744149c83a2fbd59bf3f45a5f3891"
|
|
22
|
+
},
|
|
23
|
+
"sqlite_runtime.py": {
|
|
24
|
+
"path": "hermes_cli/sqlite_runtime.py",
|
|
25
|
+
"sha256": "682ae7f6a783673b7687377a37f210f5b1f86f54d227baed95df279bdd561310"
|
|
26
|
+
},
|
|
27
|
+
"agent_redact.py": {
|
|
28
|
+
"path": "agent/redact.py",
|
|
29
|
+
"sha256": "db98c5b9cf5727abbb471b5009d19d3f8230863869cde207d53c5e5d8d4831f0"
|
|
30
|
+
},
|
|
31
|
+
"file_safety.py": {
|
|
32
|
+
"path": "agent/file_safety.py",
|
|
33
|
+
"sha256": "f2cb782cda3f091acba507e3af6a31e7326770dfc6b75657cd5adb1006521db9"
|
|
34
|
+
},
|
|
35
|
+
"interrupt_compat.py": {
|
|
36
|
+
"path": "agent/interrupt_compat.py",
|
|
37
|
+
"sha256": "f10ecfe97b6be6d150c3e41d5d377a9ca845e74d454240f2d6506f753dc23441"
|
|
38
|
+
},
|
|
39
|
+
"room_grants.py": {
|
|
40
|
+
"path": "gateway/platforms/api_server_room_grants.py",
|
|
41
|
+
"sha256": "d6e51eae92043ed8ab116e4f555db3c100829c93b61d516e31fc239ff90da204"
|
|
42
|
+
},
|
|
43
|
+
"gateway_status.py": {
|
|
44
|
+
"path": "gateway/status.py",
|
|
45
|
+
"sha256": "c7f2615117643f536f4d678a185dc1f9e6e407daea79e85f351613efac0ecefd"
|
|
46
|
+
},
|
|
47
|
+
"pyproject.toml": {
|
|
48
|
+
"path": "pyproject.toml",
|
|
49
|
+
"sha256": "c70c8b52f6cc08a4e65f0fc1713c26814fd4f19811bc7e01de645009b2a76600"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|