Files
nucleic/Sources/fake-grok/FakeGrok.swift
T

147 lines
6.8 KiB
Swift

import Foundation
import NucleicCore
/// Tier-2 contract-test stub (OBSERVABILITY B.4), the Grok analog of `fake-claude`: speaks the
/// real Grok Build wire protocols off a recorded fixture — `streaming-json` on stdout, and for
/// approval directives it discovers the per-session approval bridge exactly as Grok's generated
/// PreToolUse hook would (from the `hooks.json` the adapter wrote) and performs the actual HTTP
/// `POST /grok-hook`, asserting on the reply. Runs through the production ProcessHost + adapter
/// with no `grok` installed and no cost.
///
/// Fixture lines are either verbatim native streaming-json (echoed to stdout) or directives:
/// {"__fake__":"approval","tool_name":…,"input":…,"tool_call_id":…,
/// "expect":"allow"|"deny"} — full hook round-trip, assert behavior
/// {"__fake__":"sleep","ms":50} — pacing
///
/// Exit codes: 0 ok · 4 approval-reply mismatch · 5 setup/handshake failure.
@main
struct FakeGrok {
static func main() async {
guard let fixturePath = ProcessInfo.processInfo.environment["FAKE_GROK_FIXTURE"] else {
die(5, "FAKE_GROK_FIXTURE not set")
}
guard let fixture = try? String(contentsOfFile: fixturePath, encoding: .utf8) else {
die(5, "cannot read fixture \(fixturePath)")
}
for rawLine in fixture.split(separator: "\n", omittingEmptySubsequences: true) {
let line = String(rawLine)
guard let parsed = try? JSONValue(parsing: line),
let directive = parsed["__fake__"]?.stringValue
else {
emit(line)
continue
}
switch directive {
case "approval":
await performApproval(parsed)
case "sleep":
let ms = parsed["ms"]?.intValue ?? 10
try? await Task.sleep(for: .milliseconds(ms))
default:
die(5, "unknown directive \(directive)")
}
}
exit(0)
}
static func emit(_ line: String) {
FileHandle.standardOutput.write(Data((line + "\n").utf8))
}
static func die(_ code: Int32, _ message: String) -> Never {
FileHandle.standardError.write(Data(("fake-grok: " + message + "\n").utf8))
exit(code)
}
// MARK: - PreToolUse hook round-trip (GROK_ADAPTER §3, Tier A)
/// Discover the approval bridge the way Grok's real hook would — from the project-local
/// `.grok/hooks.json` the adapter generated in the worktree (the diagnostic
/// `NUCLEIC_GROK_HOOKS_CONFIG` env points straight at it) → the hook script → its
/// `SERVER_URL` / `AUTH` literals — then POST the gated call to `/grok-hook` and assert the
/// reply behavior.
static func performApproval(_ directive: JSONValue) async {
guard let (url, authorization) = discoverHookBridge() else {
die(5, "could not discover the grok hook bridge from the generated config")
}
var body: [String: JSONValue] = [
"tool_name": directive["tool_name"] ?? .string("unknown"),
"input": directive["input"] ?? .object([:]),
]
if let toolCallID = directive["tool_call_id"] {
body["tool_call_id"] = toolCallID
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(authorization, forHTTPHeaderField: "Authorization")
request.httpBody = try? JSONValue.object(body).encodedData()
request.timeoutInterval = 60
guard let (data, response) = try? await URLSession.shared.data(for: request),
let http = response as? HTTPURLResponse, http.statusCode == 200,
let reply = try? JSONValue(parsing: data),
let behavior = reply["behavior"]?.stringValue
else {
die(4, "POST /grok-hook did not return a usable {behavior,…} reply")
}
// The confirmed contract: allow must echo updatedInput; deny must carry a message.
if behavior == "allow", reply["updatedInput"] == nil {
die(4, "allow reply missing updatedInput")
}
if behavior == "deny", reply["message"]?.stringValue == nil {
die(4, "deny reply missing message")
}
if let expected = directive["expect"]?.stringValue, behavior != expected {
die(4, "expected behavior \(expected), got \(behavior)")
}
}
/// Read the generated `.grok/hooks.json` → hook script → `(server URL, Authorization header)`.
/// The configured `command` is the `pre_tool.sh` wrapper; the `SERVER_URL` / `AUTH` literals
/// live in the sibling `pre_tool.py`, so read that (falling back to the command file itself).
private static func discoverHookBridge() -> (URL, String)? {
guard let configPath = hooksConfigPath(),
let configData = try? Data(contentsOf: URL(fileURLWithPath: configPath)),
let config = try? JSONValue(parsing: configData),
let commandPath = config["hooks"]?["PreToolUse"]?[0]?["hooks"]?[0]?["command"]?.stringValue
else { return nil }
let pyPath = URL(fileURLWithPath: commandPath)
.deletingLastPathComponent().appendingPathComponent("pre_tool.py").path
let script = (try? String(contentsOfFile: pyPath, encoding: .utf8))
?? (try? String(contentsOfFile: commandPath, encoding: .utf8))
guard let script,
let serverURL = literal(named: "SERVER_URL", in: script),
let auth = literal(named: "AUTH", in: script),
let url = URL(string: serverURL)
else { return nil }
return (url, auth)
}
/// `hooks.json` path: the diagnostic `NUCLEIC_GROK_HOOKS_CONFIG` env the adapter sets, else
/// the project-local `.grok/hooks.json` in the cwd (the worktree) — exactly where the real
/// Grok PreToolUse hook is discovered.
private static func hooksConfigPath() -> String? {
if let env = ProcessInfo.processInfo.environment["NUCLEIC_GROK_HOOKS_CONFIG"] { return env }
let cwd = FileManager.default.currentDirectoryPath
let local = cwd + "/.grok/hooks.json"
return FileManager.default.fileExists(atPath: local) ? local : nil
}
/// Parse a `NAME = "<json string>"` line out of the generated Python hook script.
private static func literal(named name: String, in script: String) -> String? {
for line in script.split(separator: "\n") {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard trimmed.hasPrefix("\(name) = ") else { continue }
let rhs = String(trimmed.dropFirst("\(name) = ".count))
return (try? JSONValue(parsing: Data(rhs.utf8)))?.stringValue
}
return nil
}
}