Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,18 @@ kills the worker. The broker seals every terminal turn with its usage, request
count, declared model and limit reason, and writes one usage row (keyed by
`turn`) plus per-request rows for completed and failed turns alike; a replayed
turn is never metered twice. `resolveOrganizationGrokBrokerProjection` exposes
a slot's full declared shape, and `noopolis.daimon.grok-slot-preflight.v1`
receipts bind a slot's denied-path canaries to that projection's digest.
a slot's full declared shape, and `noopolis.daimon.grok-slot-preflight.v2`
receipts bind a slot's denied-path canaries to that projection's digest and to
one recycle (the caller's nonce and the slot's increasing generation).

Evaluators (Paideia judges and the optimizer, organization uid only) borrow the
same credential through inference grants: `request_inference_grant` over the
control socket returns a ten-minute token for one declared model and effort,
which the evaluator's Grok CLI presents to the provider proxy through
`env_key` in a config rendered by `renderGrokInferenceClientConfig`. Grant
requests must carry no tools, are metered like a turn, and are written only to
the broker's separate `inferenceLedgerPath` (`kind: "inference"` rows), never
to a subject usage ledger or the wake fuse.

AGY uses OS-native secure storage through one private D-Bus and Secret Service
realm. Enroll it once with:
Expand Down
32 changes: 31 additions & 1 deletion src/contracts/runtimeContractManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,38 @@ export const GROK_ENGINE_BROKER = {
missingUsageEstimate: { inputBytesPerToken: 2, outputTokens: 4_096 }
},
wakeLimitEnvironment: { timeoutMs: "DAIMON_ENGINE_WAKE_TIMEOUT_MS", maxTokens: "DAIMON_ENGINE_WAKE_TOKEN_CEILING" },
// Evaluator inference grants (P2c). Judges and the optimizer (organization uid
// only, over the control socket) borrow the broker's Grok credential through
// the provider proxy; they never hold it, and their spend never reaches the
// subject usage ledger or wake fuse.
inferenceGrants: {
requestKinds: ["request_inference_grant", "release_inference_grant"],
purposes: ["judge", "optimizer"],
tokenPrefix: "inference_",
ttlMs: 600_000,
limits: { maxRequests: 64, maxTokens: 2_000_000 },
maxLiveGrants: 8,
maxInFlightRequestsPerGrant: 1,
// Top-level request members Grok 1.0.34 sends for a Paideia judge/optimizer call
// (live stub capture); `tools` and `tool_choice` are refused outright.
bodyMembers: ["messages", "model", "reasoning_effort", "response_format", "stream", "stream_options"],
messageRoles: ["system", "user", "assistant"],
failureCodes: ["auth_stale", "grant_limit", "invalid_request", "unavailable"],
ledgerVersion: "noopolis.daimon.inference-usage.v1",
ledgerDedupeKey: ["grant", "request"],
client: {
modelId: "daimon-inference-grok",
envKey: "DAIMON_INFERENCE_GRANT",
// sha256 of `renderGrokInferenceClientConfig` for the production proxy base URL and this env key.
configSha256: {
"grok-4.6": { low: "79314d039f787e4ebfec7dacf57adc969086b948f564dec008f0ed6367e6062f", medium: "6f538de0547c0c4e6a3f04ae08595ceadadabb06b75f6b6ee4c428744bb95cd8", high: "5652656effa82f0c4f09cf8226b16e6140332a5a358b571194bb5563312367ac" },
"grok-4.5": { low: "a07f7436f1268bb399ec233c65d3b3d8fb99a11a1f175f8da1ca133c9367bc74", medium: "1f4c0d4dad1f3b09419b5739db6423a09e0049abc091594c64123a75dd53dfb9", high: "ffbc33728b821e9854fbc7c93601e599225da421ecfd6ebf10d314afcc28d6f2" },
"grok-build": { low: "ca15c6a562a008227d39c51d3a3a83715663089b3784e8b46debb1fb67b3c4a1", medium: "01783fb6beadcf6f8486fff0836820ad43fab5662b812a907fe9cfdb83e9804d", high: "98d16f2b7d12f4eb540d625c853e51d227933e204923e43e8b9b4176f10aca2c" }
}
}
},
projectionVersion: "noopolis.daimon.grok-broker-projection.v1",
slotPreflightVersion: "noopolis.daimon.grok-slot-preflight.v1",
slotPreflightVersion: "noopolis.daimon.grok-slot-preflight.v2",
artifacts: {
sourceSha256: "36f60689f0a8af0e3108f5f53d78ed52b7d4b6f934c75b6184606dfa82bc741e",
x64Sha256: "36dc76b134eb59cf5a6720b6f94228eb279108e20ea3343fa6efd9ffcb60a4d3",
Expand Down
47 changes: 46 additions & 1 deletion src/runtime/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,46 @@ model (`grok-4.6-build` → `grok-4.6`), otherwise the turn fails as rejected an
is still metered. Control protocol v2 is refused-v1 on the wire because both
ends ship in this package.

Evaluator inference grants (`grokInferenceGrants.ts`) let Paideia judges and
the DSPy optimizer — uid 2000, the trusted evaluator side — spend the broker's
Grok credential without holding it. `request_inference_grant {model,
reasoningEffort, purpose: judge|optimizer}` is an additive control protocol v2
verb (`engineBrokerInferenceProtocol.ts`); only the organization uid reaches it,
because the native relay admits only that `SO_PEERCRED` uid on `control.sock`
(the TS backend sees only the relay). The answer is a token
(`inference_` + 32 random bytes), the proxy base URL, an expiry (TTL ten
minutes) and the manifest limits; `release_inference_grant` frees one of the
eight live-grant slots early. Grants are their own kind: their own map keyed by
a random grant id, never the turn capability or turn meter maps, and the proxy
routes a bearer by its prefix to exactly one of the two lookups. A grant has no
worker isolation guard but the same spend gate as a turn (one request in
flight, request ceiling, between-requests token ceiling, estimate on missing
usage), so one grant is one sequential lane — parallel judges each hold one.
`grokInferenceProxyRequest.ts` accepts exactly what Grok 1.0.34 sends for the
Paideia judge argv (live stub capture): `stream: true` with
`stream_options.include_usage`, the declared `model`/`reasoning_effort`, plain
`{role, content}` messages, optional `response_format` json_schema, and **no
`tools` or `tool_choice` member at all** — the CLI's per-call `session_title`
request carries both and is refused locally. Every settled request appends one
`kind: "inference"` row (`purpose`, `grant`, `request`, model, usage,
`usage_source`) to `service.json` v2's optional `inferenceLedgerPath`, which
may never be a subject ledger; readers dedupe on `(grant, request)`
(`dedupeInferenceUsageRows`), and `wakeFuse.ts` skips inference rows. Without
that path every grant request is refused `unavailable`. Grants share the
subject's credential authority, so a stale realm fails both (accepted shared
fate): the grant request is refused `auth_stale`, and a proxied grant request
that meets a stale realm gets HTTP 401 `{"error":"auth_stale"}`, which the CLI
surfaces immediately as `Internal error: "Unauthorized (401) from …:
auth_stale …"`. `grokInferenceClientConfig.ts` renders the evaluator's private
`GROK_HOME` `config.toml` (pinned per model/effort in the manifest): the grant
token through `env_key = "DAIMON_INFERENCE_GRANT"`, the worker's lean settings,
no MCP, and `max_retries = 0` — with the default, Grok retries a refused (503)
request with backoff past 45 s instead of failing in ~0.35 s. Its init frame
reports `apiKeySource: "user"`, `tools: []`, `mcp_servers: []`, and the CLI
must be run with `--model daimon-inference-grok`. The inference ledger
directory must be provisioned setgid to the organization group (e.g.
`2100:2000 2750`) for uid 2000 to read rows the broker creates `0640`.

`grokBrokerProjection.ts` is the public, I/O-free projection of one brokered
Grok agent's slot (`noopolis.daimon.grok-broker-projection.v1`): Daimon's own
deny collectors plus the caller's evaluator paths, profile/config/prompt
Expand All @@ -87,9 +127,14 @@ must supply canonical non-symlink paths (its fixed tmpfs and workspace roots)
and verify that during provisioning. The projection also carries the seccomp
profile digest and the `bubblewrap` sandbox runtime a receipt must match. `grokSlotPreflightReceipt.ts` is the
zod schema a root slot supervisor's receipt must satisfy
(`noopolis.daimon.grok-slot-preflight.v1`, fixtures under
(`noopolis.daimon.grok-slot-preflight.v2`, fixtures under
`fixtures/grok-slot-preflight/`); `verifyGrokSlotPreflightReceipt` binds it to
the projection digest and requires a denied canary for exactly every deny path.
The projection digest does not change across recycles, so the receipt also
carries freshness: a supervisor-owned per-slot `generation` (strictly
increasing) and the caller's recycle `nonce` (32 random bytes, hex). The
verifier requires `{expectedNonce, minGeneration}` and refuses another nonce, a
lower generation, and any v1 receipt.

`grokBrokerWorkerConfig.ts` is the only source of worker `config.toml` bytes;
the manifest pins the sha256 of every model/effort combination and the broker
Expand Down
30 changes: 29 additions & 1 deletion src/runtime/engineBrokerControlClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { createHash, randomUUID } from "node:crypto";
import { createConnection } from "node:net";
import { ENGINE_BROKER_VERSION, encodeEngineBrokerFrame,EngineBrokerFrameDecoder,parseEngineBrokerResponse } from "./engineBrokerProtocol.js";
import type { EngineBrokerInferenceFailureCode, EngineBrokerInferenceRequest, EngineBrokerInferenceResponse } from "./engineBrokerInferenceProtocol.js";
import type { EngineBrokerTurnLimitOverrides } from "./engineBrokerTurnAccounting.js";
import type { GrokBrokerModel, GrokBrokerReasoningEffort } from "./grokBrokerModelPolicy.js";
import type { GrokInferencePurpose } from "./inferenceUsageLedger.js";

export type EngineBrokerInferenceGrant = Omit<Extract<EngineBrokerInferenceResponse, { kind: "inference_grant" }>, "version" | "kind" | "requestId">;
/** A refused grant request; `code` is closed (`auth_stale` is the stale shared realm, `grant_limit` the live-grant cap). */
export class EngineBrokerInferenceGrantRefused extends Error {
constructor(readonly code: EngineBrokerInferenceFailureCode) { super(`engine broker inference grant refused (${code})`); }
}

/**
* What the organization runtime asks of a brokered turn beyond the prompt:
Expand All @@ -13,9 +22,28 @@ export interface EngineBrokerTurnClient { turn(agentId:string,wakeId:string,prom
export class EngineBrokerControlClient implements EngineBrokerTurnClient {
constructor(private readonly socketPath="/run/daimon-engine-broker/control.sock"){}
async ready():Promise<void>{const requestId=randomUUID(),socket=createConnection({path:this.socketPath}),decoder=new EngineBrokerFrameDecoder();await new Promise<void>((resolve,reject)=>{let settled=false;const fail=()=>{if(settled)return;settled=true;socket.destroy();reject(new Error("engine broker unavailable"));};socket.once("error",fail);socket.once("close",fail);socket.once("connect",()=>socket.write(encodeEngineBrokerFrame({version:ENGINE_BROKER_VERSION,kind:"health",requestId})));socket.on("data",(chunk)=>{try{for(const value of decoder.push(chunk)){const response=parseEngineBrokerResponse(value);if(response.kind!=="ready"||response.requestId!==requestId||settled)throw new Error();settled=true;socket.destroy();resolve();}}catch{fail();}});});}
/**
* Evaluator side (organization uid only; the native relay enforces it):
* borrow the broker credential for one sequential lane of judge or optimizer
* requests. Refusals reject with {@link EngineBrokerInferenceGrantRefused};
* a broker that cannot answer rejects with `engine broker unavailable`.
*/
async requestInferenceGrant(request:Readonly<{model:GrokBrokerModel;reasoningEffort:GrokBrokerReasoningEffort;purpose:GrokInferencePurpose}>):Promise<EngineBrokerInferenceGrant>{
const response=await this.exchange({version:ENGINE_BROKER_VERSION,kind:"request_inference_grant",requestId:randomUUID(),model:request.model,reasoningEffort:request.reasoningEffort,purpose:request.purpose});
if(response.kind==="inference_grant_refused")throw new EngineBrokerInferenceGrantRefused(response.code);
if(response.kind!=="inference_grant"||response.model!==request.model||response.reasoningEffort!==request.reasoningEffort||response.purpose!==request.purpose)throw new Error("engine broker unavailable");
const {version:_version,kind:_kind,requestId:_requestId,...grant}=response;return grant;
}
async releaseInferenceGrant(grantId:string):Promise<boolean>{
const response=await this.exchange({version:ENGINE_BROKER_VERSION,kind:"release_inference_grant",requestId:randomUUID(),grantId});
if(response.kind==="inference_grant_refused")throw new EngineBrokerInferenceGrantRefused(response.code);
if(response.kind!=="inference_grant_released"||response.grantId!==grantId)throw new Error("engine broker unavailable");
return response.released;
}
private exchange(request:EngineBrokerInferenceRequest):Promise<EngineBrokerInferenceResponse>{const socket=createConnection({path:this.socketPath}),decoder=new EngineBrokerFrameDecoder();return new Promise((resolve,reject)=>{let settled=false;const fail=()=>{if(settled)return;settled=true;socket.destroy();reject(new Error("engine broker unavailable"));};socket.once("error",fail);socket.once("close",fail);socket.once("connect",()=>socket.write(encodeEngineBrokerFrame(request)));socket.on("data",(chunk)=>{try{for(const value of decoder.push(chunk)){const response=parseEngineBrokerResponse(value);if(settled||response.requestId!==request.requestId||(response.kind!=="inference_grant"&&response.kind!=="inference_grant_released"&&response.kind!=="inference_grant_refused"))throw new Error();settled=true;socket.destroy();resolve(response);}}catch{fail();}});});}
async turn(agentId:string,wakeId:string,prompt:string,mcpEndpoint:string,signal?:AbortSignal,options:EngineBrokerTurnOptions={}):Promise<string>{
const turnId=createHash("sha256").update(`${agentId}\0${wakeId}`).digest("hex"),requestId=randomUUID();const request={version:ENGINE_BROKER_VERSION,kind:"start_turn",requestId,turnId,agentId,wakeId,prompt,mcpEndpoint,...(options.limits===undefined?{}:{limits:options.limits})} as const;const socket=createConnection({path:this.socketPath});const decoder=new EngineBrokerFrameDecoder();
return new Promise<string>((resolve,reject)=>{let accepted=false,settled=false;const fail=()=>{if(settled)return;settled=true;cleanup();reject(new Error("engine broker unavailable"));};const cleanup=()=>{signal?.removeEventListener("abort",abort);socket.destroy();};const abort=()=>fail();signal?.addEventListener("abort",abort,{once:true});if(signal?.aborted)return abort();socket.once("connect",()=>socket.write(encodeEngineBrokerFrame(request)));socket.on("data",(chunk)=>{try{for(const value of decoder.push(chunk)){const response=parseEngineBrokerResponse(value);if(response.kind==="ready"||response.requestId!==requestId||response.turnId!==turnId)throw new Error();if(response.kind==="accepted"){if(accepted)throw new Error();accepted=true;continue;}if(!accepted||settled)throw new Error();settled=true;cleanup();
return new Promise<string>((resolve,reject)=>{let accepted=false,settled=false;const fail=()=>{if(settled)return;settled=true;cleanup();reject(new Error("engine broker unavailable"));};const cleanup=()=>{signal?.removeEventListener("abort",abort);socket.destroy();};const abort=()=>fail();signal?.addEventListener("abort",abort,{once:true});if(signal?.aborted)return abort();socket.once("connect",()=>socket.write(encodeEngineBrokerFrame(request)));socket.on("data",(chunk)=>{try{for(const value of decoder.push(chunk)){const response=parseEngineBrokerResponse(value);if((response.kind!=="accepted"&&response.kind!=="completed"&&response.kind!=="failed")||response.requestId!==requestId||response.turnId!==turnId)throw new Error();if(response.kind==="accepted"){if(accepted)throw new Error();accepted=true;continue;}if(!accepted||settled)throw new Error();settled=true;cleanup();
if(options.model!==undefined&&response.model!==options.model){reject(new Error(`engine broker turn used model ${response.model}, not the declared ${options.model}`));return;}
if(response.kind==="completed")resolve(response.text);else reject(new Error(`engine broker turn failed (${response.code}${response.limitReason==="none"?"":`; limit=${response.limitReason}`}${response.diagnostic ? `; ${response.diagnostic.stage}/${response.diagnostic.failureClass}; exit=${response.diagnostic.exitCode}; signal=${response.diagnostic.termSignal}` : ""})`));}}catch{fail();}});socket.once("error",fail);socket.once("close",()=>{if(!settled)fail();});});
}
Expand Down
Loading
Loading