SLOPSHOPPER

qe-account-cell

Immutable Quest Engineering account-cell tool broker

newguardprompttoolprocessagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · qe-account-cell
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Read(/work/app/src/auth.ts) ⎿ Denied by qe-account-cell: QE account-cell Mod is not ready. ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Quest Engineering

Quest Engineering is a control plane for orchestrating persistent AI-agent work. This repository contains the authoritative Elixir control plane foundation and its first dependency-free semantic tactic compiler.

Current architecture

Quest Engineering Client
        ↓
Quest Engineering Control Plane (this repository)
        ↓
Quest Engineering Worker (separate Bun/TypeScript component)
        ↓
HarnessRegistry (semantic AgentHarness adapters)
        ├─ provider-neutral execution handles (headless is valid)
        └─ optional InteractiveAgentHarness → TerminalSessionBackend (Herdr)

The control plane hosts Worker Protocol v18 and workers/bun is the filesystem authority. SBX is the accepted per-Run execution boundary for the additive Pi/Antigravity/Claude profile; each harness executes in the Run-owned environment with private Git and lineage-scoped native state. Claude Agent SDK is a headless, streamed-process harness with no Herdr dependency; see docs/claude-agent-sdk.md. Phase 5 acceptance and its independent teardown/evidence-hygiene dimensions are recorded in docs/phase-5-closure.md. Live session identity, local Herdr attachment, and operational HumanAttention are documented in docs/live-execution-sessions.md. Product Workspaces are logical and path-free; long-lived Workers discover explicitly authorized source repositories and provision one durable isolated Git worktree per Run before model execution. Workspace bindings reconcile independently when a Worker connects: a binding to a Product Workspace that no longer exists is retired without taking the Worker offline or deleting retained worktrees, dispatch state, or harness-native sessions. After successful Runtime completion, the Worker safely finalizes and pushes Git changes while Phoenix creates and centrally reconciles one same-repository GitHub Pull Request. Only an exact human-merged PR completes the Quest; publishing retry, explicit Run Again, and safe worktree cleanup remain separate operations. The Svelte/Pixi/Tauri client presents this truthful lifecycle as a dense Mini Medieval fantasy management town with deterministic Members, work orders, delivery markers, and accessible DOM management windows.

Planning Classes, immutable Markdown execution artifacts, exact-subject verdicts, and independent plan/code remediation gates are documented in docs/spec-driven-development.md.

Phase-A provider-neutral accounts and durable account-owned, Run-independent execution environments are documented in docs/provider-accounts.md. Their physical availability is explicitly separate from authentication and inference readiness.

Umbrella applications

  • quest_engineering_core — dependency-free Elixir boundary containing reusable Product Tactic authoring and pure expansion into semantic Step/Sequence/Parallel/Until tactics, deterministic compilation, and a pure runtime.
  • quest_engineering_server — Phoenix/Ecto infrastructure boundary with Product Tactic persistence, immutable resolved launch snapshots, transactional runtime persistence, and the generation-fenced WebSocket Worker Protocol v18.

Dependencies point inward only:

quest_engineering_server
        ↓
quest_engineering_core

quest_engineering_core must never depend on the server, Phoenix, Ecto, PostgreSQL, HTTP, process/filesystem APIs, Herdr, Pi, or Bun.

Requirements

Versions are declared in .tool-versions:

  • Erlang/OTP 29.0.5
  • Elixir 1.20.3
  • PostgreSQL 18.3 (provided by Docker Compose)

Install Erlang and Elixir with your preferred version manager. With asdf:

asdf install

Local setup

Start PostgreSQL:

docker compose up -d postgres

Install dependencies, create the development database, and run migrations:

mix deps.get
mix ecto.create
mix ecto.migrate
# Or perform all three steps at once:
mix setup

Additive migrations create Product definitions, logical Workspaces, durable binding attempts, Worker source bindings, Run workspace assignments, one-per-Run Deliveries, Quest completion metadata, runtime persistence, dispatches, and reconciliation records. See apps/quest_engineering_server/README.md and workers/bun/README.md for v4 provisioning, fencing, delivery, and restart recovery.

Run the umbrella tests. The test alias creates and migrates the test database automatically:

mix test

Prove the core compiles and tests independently:

cd apps/quest_engineering_core
mix test
cd ../..

Run the API server:

mix phx.server

The health endpoint is available at <http://localhost:4000/api/v1/health>. Workers connect to /worker/websocket with the configured static Worker token.

Run deterministic code-quality checks (format, warnings-as-errors compilation, unused dependency check, ExUnit, and Credo):

mix check

Run Dialyzer separately because initial PLT construction is comparatively slow:

mix dialyzer

Generate test coverage with ExCoveralls:

mix coveralls.html
Source 1 files
hooks/register.ts 528 lines
1import type {
2	EngineInterface,
3	Next,
4	PluginRegisterInput,
5	Register,
6	TurnStepInput,
7} from "claude-code";
8
9const PROTOCOL = "qe_claude_account_cell_v1";
10const BRIDGE = "/opt/qe/claude/qe-account-cell-bridge";
11const MAX_BRIDGE_OUTPUT = 65_536;
12const MAX_OPERATION_INPUT = 1_048_576;
13
14const PREFIX = "mcp__qe-account-cell__";
15const TOOLS = {
16	read: `${PREFIX}qe_workspace_read`,
17	list: `${PREFIX}qe_workspace_list`,
18	write: `${PREFIX}qe_workspace_write`,
19	edit: `${PREFIX}qe_workspace_edit`,
20	search: `${PREFIX}qe_workspace_search`,
21	shell: `${PREFIX}qe_run_shell`,
22	complete: `${PREFIX}qe_complete_step`,
23	attention: `${PREFIX}qe_request_human_assistance_and_wait`,
24} as const;
25const ALLOWED_TOOLS = new Set<string>(Object.values(TOOLS));
26
27type JsonObject = Record<string, unknown>;
28
29function object(value: unknown, label: string): JsonObject {
30	if (typeof value !== "object" || value === null || Array.isArray(value))
31		throw new Error(`${label} must be an object.`);
32	return value as JsonObject;
33}
34
35function boundedString(value: unknown, label: string, maximum: number): string {
36	if (typeof value !== "string" || value.length === 0 || value.length > maximum)
37		throw new Error(`${label} is invalid.`);
38	return value;
39}
40
41function optionalInteger(
42	value: unknown,
43	label: string,
44	minimum: number,
45	maximum: number,
46): number | undefined {
47	if (value === undefined) return undefined;
48	if (
49		typeof value !== "number" ||
50		!Number.isSafeInteger(value) ||
51		value < minimum ||
52		value > maximum
53	)
54		throw new Error(`${label} is invalid.`);
55	return value;
56}
57
58function workspacePath(value: unknown, label: string): string {
59	const path = boundedString(value, label, 4_096);
60	if (
61		path.startsWith("/") ||
62		path.includes("\\") ||
63		path.includes("\0") ||
64		path.split("/").some((segment) => segment === "..")
65	)
66		throw new Error(`${label} must remain relative to the Run workspace.`);
67	return path;
68}
69
70function boundedJson(value: unknown, label: string, maximum: number): string {
71	const encoded = JSON.stringify(value);
72	if (encoded === undefined || encoded.length > maximum)
73		throw new Error(`${label} is invalid or too large.`);
74	return encoded;
75}
76
77function parseBridgeOutput(text: string): JsonObject {
78	if (text.length > MAX_BRIDGE_OUTPUT)
79		throw new Error("QE broker output exceeded the spike limit.");
80	return object(JSON.parse(text), "QE broker output");
81}
82
83function parseEffort(value: string): NonNullable<TurnStepInput["effort"]> {
84	switch (value) {
85		case "low":
86		case "medium":
87		case "high":
88		case "xhigh":
89		case "max":
90			return value;
91		default:
92			throw new Error("QE_CLAUDE_EFFORT is invalid.");
93	}
94}
95
96async function runBridge(
97	$: EngineInterface,
98	operation: JsonObject,
99): Promise<JsonObject> {
100	const input = boundedJson(
101		operation,
102		"QE broker operation",
103		MAX_OPERATION_INPUT,
104	);
105	const execution = await $.process.run([BRIDGE, "bound-operation"], {
106		stdin: input,
107		timeoutMs: 600_000,
108	});
109	if (
110		execution.exitCode !== 0 ||
111		execution.isStdoutTruncated ||
112		execution.isStderrTruncated
113	)
114		throw new Error(`QE broker failed with exit code ${execution.exitCode}.`);
115	return parseBridgeOutput(execution.stdout);
116}
117
118async function waitForHuman(
119	$: EngineInterface,
120	operation: JsonObject,
121): Promise<JsonObject> {
122	const input = boundedJson(
123		operation,
124		"QE HumanAttention operation",
125		MAX_OPERATION_INPUT,
126	);
127	const stream = $.process.spawn({
128		argv: [BRIDGE, "bound-human-attention-wait"],
129		input,
130	});
131	let stdout = "";
132	let stderr = "";
133	let item = await stream.next();
134	while (item.done !== true) {
135		if (item.value.stream === "stdout") stdout += item.value.text;
136		else stderr += item.value.text;
137		if (stdout.length + stderr.length > MAX_BRIDGE_OUTPUT) {
138			await stream.return({ code: null, signal: "SIGTERM" });
139			throw new Error("QE HumanAttention output exceeded the spike limit.");
140		}
141		item = await stream.next();
142	}
143	if (item.value.code !== 0 || item.value.signal !== null)
144		throw new Error("QE HumanAttention bridge did not exit successfully.");
145	return parseBridgeOutput(stdout);
146}
147
148async function checkPlugin(
149	_$: EngineInterface,
150	event: PluginRegisterInput,
151	next: Next<"plugin.register">,
152) {
153	if (event.tier === "user")
154		return {
155			refuse:
156				"QE account-cell policy permits only immutable organization and built-in Mods.",
157		} as const;
158	return next(event);
159}
160
161function brokerOperation(tool: string, event: JsonObject): JsonObject {
162	switch (tool) {
163		case TOOLS.read:
164			return {
165				type: "workspace_read",
166				path: workspacePath(event.path, "path"),
167				...(optionalInteger(event.offset, "offset", 0, 10_000_000) === undefined
168					? {}
169					: { offset: event.offset }),
170				...(optionalInteger(event.limit, "limit", 1, 100_000) === undefined
171					? {}
172					: { limit: event.limit }),
173			};
174		case TOOLS.list:
175			return {
176				type: "workspace_list",
177				path: workspacePath(event.path ?? ".", "path"),
178			};
179		case TOOLS.write:
180			return {
181				type: "workspace_write",
182				path: workspacePath(event.path, "path"),
183				content: boundedString(event.content, "content", 262_144),
184			};
185		case TOOLS.edit:
186			return {
187				type: "workspace_edit",
188				path: workspacePath(event.path, "path"),
189				oldText: boundedString(event.oldText, "oldText", 262_144),
190				newText:
191					typeof event.newText === "string" && event.newText.length <= 262_144
192						? event.newText
193						: (() => {
194								throw new Error("newText is invalid.");
195							})(),
196				expectedReplacements:
197					optionalInteger(
198						event.expectedReplacements,
199						"expectedReplacements",
200						1,
201						1_000,
202					) ?? 1,
203			};
204		case TOOLS.search:
205			return {
206				type: "workspace_search",
207				query: boundedString(event.query, "query", 4_096),
208				path: workspacePath(event.path ?? ".", "path"),
209				...(optionalInteger(event.maxResults, "maxResults", 1, 1_000) ===
210				undefined
211					? {}
212					: { maxResults: event.maxResults }),
213			};
214		case TOOLS.shell:
215			return {
216				type: "run_shell",
217				command: boundedString(event.command, "command", 32_768),
218				timeoutMs:
219					optionalInteger(event.timeoutMs, "timeoutMs", 100, 30_000) ?? 30_000,
220			};
221		case TOOLS.complete:
222			return {
223				type: "complete_step",
224				outputs: object(event.outputs, "outputs"),
225			};
226		default:
227			throw new Error("The tool is not a synchronous QE broker operation.");
228	}
229}
230
231export const register: Register = (on) => {
232	let ready = false;
233	let expectedModel = "";
234	let expectedEffort: NonNullable<TurnStepInput["effort"]> | "" = "";
235	let nativeSessionId = "";
236
237	on("plugin.register", checkPlugin).catch(async (_$, event, next) => {
238		if (event.tier === "user")
239			return {
240				refuse: "QE failed closed while checking an unapproved Mod.",
241			};
242		return next(event);
243	});
244
245	on("session.start", async ($, event, next) => {
246		ready = false;
247		const protocol = await $.env.get("QE_ACCOUNT_CELL_MOD_PROTOCOL");
248		expectedModel = (await $.env.get("QE_CLAUDE_MODEL")) ?? "";
249		expectedEffort = parseEffort((await $.env.get("QE_CLAUDE_EFFORT")) ?? "");
250		nativeSessionId = await $.session.id();
251		if (
252			protocol !== PROTOCOL ||
253			expectedModel.length === 0 ||
254			nativeSessionId.length === 0
255		)
256			throw new Error("QE account-cell Mod environment is incomplete.");
257
258		await $.tool.register({
259			name: "qe_workspace_read",
260			description:
261				"Read a UTF-8 file from the pre-bound Quest Engineering Run workspace. Paths are relative to that workspace; this tool cannot address the Claude account cell.",
262			inputSchema: {
263				type: "object",
264				properties: {
265					path: { type: "string", minLength: 1, maxLength: 4_096 },
266					offset: { type: "integer", minimum: 0, maximum: 10_000_000 },
267					limit: { type: "integer", minimum: 1, maximum: 100_000 },
268				},
269				required: ["path"],
270				additionalProperties: false,
271			},
272		});
273		await $.tool.register({
274			name: "qe_workspace_list",
275			description:
276				"List a directory in the pre-bound Quest Engineering Run workspace. The account-cell filesystem is not in this tool's namespace.",
277			inputSchema: {
278				type: "object",
279				properties: {
280					path: { type: "string", minLength: 1, maxLength: 4_096 },
281				},
282				additionalProperties: false,
283			},
284		});
285		await $.tool.register({
286			name: "qe_workspace_write",
287			description:
288				"Write a UTF-8 file in the pre-bound Quest Engineering Run workspace.",
289			inputSchema: {
290				type: "object",
291				properties: {
292					path: { type: "string", minLength: 1, maxLength: 4_096 },
293					content: { type: "string", maxLength: 262_144 },
294				},
295				required: ["path", "content"],
296				additionalProperties: false,
297			},
298		});
299		await $.tool.register({
300			name: "qe_workspace_edit",
301			description:
302				"Replace exact text in a file in the pre-bound Quest Engineering Run workspace.",
303			inputSchema: {
304				type: "object",
305				properties: {
306					path: { type: "string", minLength: 1, maxLength: 4_096 },
307					oldText: { type: "string", minLength: 1, maxLength: 262_144 },
308					newText: { type: "string", maxLength: 262_144 },
309					expectedReplacements: {
310						type: "integer",
311						minimum: 1,
312						maximum: 1_000,
313					},
314				},
315				required: ["path", "oldText", "newText"],
316				additionalProperties: false,
317			},
318		});
319		await $.tool.register({
320			name: "qe_workspace_search",
321			description:
322				"Search only the pre-bound Quest Engineering Run workspace and return bounded matches.",
323			inputSchema: {
324				type: "object",
325				properties: {
326					query: { type: "string", minLength: 1, maxLength: 4_096 },
327					path: { type: "string", minLength: 1, maxLength: 4_096 },
328					maxResults: { type: "integer", minimum: 1, maximum: 1_000 },
329				},
330				required: ["query"],
331				additionalProperties: false,
332			},
333		});
334		await $.tool.register({
335			name: "qe_run_shell",
336			description:
337				"Execute a command only inside the pre-bound credential-free Quest Engineering Run SBX. No command executes in the Claude account cell.",
338			inputSchema: {
339				type: "object",
340				properties: {
341					command: { type: "string", minLength: 1, maxLength: 32_768 },
342					timeoutMs: { type: "integer", minimum: 100, maximum: 30_000 },
343				},
344				required: ["command"],
345				additionalProperties: false,
346			},
347		});
348		await $.tool.register({
349			name: "qe_complete_step",
350			description:
351				"Submit the declared structured outputs to the pre-bound Quest Engineering completion authority.",
352			inputSchema: {
353				type: "object",
354				properties: { outputs: { type: "object" } },
355				required: ["outputs"],
356				additionalProperties: false,
357			},
358		});
359		await $.tool.register({
360			name: "qe_request_human_assistance_and_wait",
361			description:
362				"Create a durable Quest Engineering HumanAttention request and wait for its exact response without opening a local account-cell prompt.",
363			inputSchema: {
364				type: "object",
365				properties: {
366					category: {
367						type: "string",
368						enum: [
369							"needs_input",
370							"needs_permission",
371							"needs_authentication",
372							"needs_confirmation",
373							"blocked_external",
374							"interactive_prompt",
375							"unknown_interactive_block",
376						],
377					},
378					message: { type: "string", minLength: 1, maxLength: 16_384 },
379					interaction: {
380						type: "string",
381						enum: [
382							"confirmation",
383							"text",
384							"choice",
385							"multiline_response",
386							"conversational_intervention",
387						],
388					},
389					responseSchema: { type: "object" },
390				},
391				required: ["category", "message"],
392				additionalProperties: false,
393			},
394		});
395		ready = true;
396		return next(event);
397	});
398
399	on("command.run", () => ({
400		text: "QE account-cell policy refused this local command.",
401	})).catch(() => ({
402		text: "QE account-cell command policy failed closed.",
403	}));
404
405	on("agent.offer", () => ({ isOffered: false })).catch(() => ({
406		isOffered: false,
407	}));
408
409	on("agent.spawn", () => ({
410		deny: "QE account-cell policy disables Claude subagents.",
411	})).catch(() => ({
412		deny: "QE account-cell subagent policy failed closed.",
413	}));
414
415	on("tool.call", async ($, event) => {
416		if (!ready) return { deny: "QE account-cell Mod is not ready." };
417		if (event.agentId)
418			return { deny: "QE account-cell policy denies subagent tool calls." };
419		if (!ALLOWED_TOOLS.has(event.tool))
420			return {
421				deny: `QE account-cell policy did not authorize ${event.tool}.`,
422			};
423
424		const input = object(event, "tool input");
425		if (event.tool === TOOLS.attention) {
426			const category = boundedString(input.category, "category", 64);
427			const message = boundedString(input.message, "message", 16_384);
428			const interaction =
429				input.interaction === undefined
430					? undefined
431					: boundedString(input.interaction, "interaction", 64);
432			const responseSchema =
433				input.responseSchema === undefined
434					? undefined
435					: object(input.responseSchema, "responseSchema");
436			if (responseSchema !== undefined)
437				boundedJson(responseSchema, "responseSchema", 16_384);
438			const result = await waitForHuman($, {
439				type: "request_human_assistance_and_wait",
440				category,
441				message,
442				...(interaction === undefined ? {} : { interaction }),
443				...(responseSchema === undefined ? {} : { responseSchema }),
444			});
445			return { result: JSON.stringify(result) };
446		}
447
448		const result = await runBridge($, brokerOperation(event.tool, input));
449		return { result: JSON.stringify(result) };
450	}).catch((_$, event, next) => ({
451		deny: `QE account-cell tool mediation failed closed for ${event.tool}: ${next.error.kind}.`,
452	}));
453
454	on("tool.check", async (_$, event, next) => {
455		if (!ready || event.agentId || !ALLOWED_TOOLS.has(event.tool))
456			return {
457				decision: "deny",
458				reason: "QE account-cell exact tool policy denied this call.",
459			};
460		const lower = await next(event);
461		if (lower.decision === "deny") return lower;
462		if (event.ceiling === "ask" || event.ceiling === "deny")
463			return {
464				decision: event.ceiling,
465				reason: "QE preserved the managed policy ceiling.",
466			};
467		return {
468			decision: "allow",
469			reason: "QE allowed an exact pre-bound broker tool.",
470		};
471	}).catch(() => ({
472		decision: "deny",
473		reason: "QE account-cell permission mediation failed closed.",
474	}));
475
476	on("prompt.submit", async (_$, event, next) => {
477		if (!ready)
478			return {
479				drop: "QE account-cell Mod is not ready; refusing the prompt.",
480			};
481		return next({
482			...event,
483			context: [
484				...(event.context ?? []),
485				"Quest Engineering account-cell contract: every filesystem, search, and shell operation must use the QE broker tools. No local account-cell coding tool exists. Never claim completion without calling qe_complete_step with exactly the declared output keys. Use qe_request_human_assistance_and_wait for human input.",
486			],
487		});
488	}).catch(() => ({
489		drop: "QE account-cell prompt mediation failed closed.",
490	}));
491
492	on("turn.step", async function* (_$, event, next) {
493		if (
494			!ready ||
495			event.agentId !== undefined ||
496			event.model !== expectedModel ||
497			event.effort !== expectedEffort
498		)
499			return {
500				turnId: event.turnId,
501				index: event.index,
502				answer: "",
503				toolUses: [],
504				stopReason: "refusal",
505				usage: null,
506			};
507		const chunks = [];
508		const downstream = next(event);
509		let item = await downstream.next();
510		while (item.done !== true) {
511			chunks.push(item.value);
512			item = await downstream.next();
513		}
514		if ((item.value.serverToolUses?.length ?? 0) > 0)
515			return {
516				turnId: event.turnId,
517				index: event.index,
518				answer: "",
519				toolUses: [],
520				serverToolUses: item.value.serverToolUses,
521				stopReason: "refusal",
522				usage: item.value.usage,
523			};
524		for (const chunk of chunks) yield chunk;
525		return item.value;
526	});
527};
528