@heretek-ai/epistemic-swarm 1.0.5 → 1.1.2

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/src/panels.tsx CHANGED
@@ -68,6 +68,13 @@ const FactoryPanel = (props: PanelProps) => {
68
68
  {data().spend.ceilingUSD ? ` / $${data().spend.ceilingUSD}` : ""}
69
69
  {data().spend.estimated ? " (estimated)" : ""}
70
70
  </text>
71
+ <Show when={data().tree}>
72
+ <text>
73
+ Design tree (round {data().tree.round}): {data().tree.settled} settled · {data().tree.open} open ·{" "}
74
+ {data().tree.deferred} deferred
75
+ {data().tree.facts ? ` (${data().tree.facts} for research)` : ""}
76
+ </text>
77
+ </Show>
71
78
  <Show when={data().halt}>
72
79
  <text>HALTED: {data().halt}</text>
73
80
  </Show>
@@ -77,6 +84,18 @@ const FactoryPanel = (props: PanelProps) => {
77
84
  <Show when={data().pending.length}>
78
85
  <text>Waiting on human: {data().pending.join(", ")}</text>
79
86
  </Show>
87
+ <Show when={data().audits?.length}>
88
+ <text>Audits:</text>
89
+ <For each={data().audits}>
90
+ {(audit: any) => (
91
+ <text>
92
+ {" "}
93
+ {audit.id} · {audit.target} · {audit.status} r{audit.round} · t:{audit.thesis} a:{audit.antithesis}
94
+ {audit.tiebreak ? ` x:${audit.tiebreak}` : ""}
95
+ </text>
96
+ )}
97
+ </For>
98
+ </Show>
80
99
  <text>Phases:</text>
81
100
  <For each={data().phases}>
82
101
  {(phase: any) => (
package/src/policy.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // Mechanical enforcement in the host: every write, edit, patch and shell call
2
2
  // from any agent passes through core's policy (canonical paths, control files,
3
- // seat scopes); factory seats additionally stop on STOP or a halted run, run
4
- // read-only shells under bubblewrap, and get fast gate feedback after edits.
3
+ // seat scopes), and every shell command runs inside the bubblewrap sandbox its
4
+ // decision names (trust/sandbox.ts). Factory seats additionally stop on STOP
5
+ // or a halted run and get fast gate feedback after edits.
5
6
  import path from "node:path"
6
7
  import {
8
+ agentSpec,
7
9
  bwrapAvailable,
8
10
  checkStopFile,
9
11
  evaluateRead,
@@ -12,13 +14,13 @@ import {
12
14
  formatDiagnostics,
13
15
  formatReport,
14
16
  loadGatesConfig,
15
- readonlyWrap,
16
17
  runGates,
18
+ sandboxArgv,
17
19
  seatOf,
18
20
  shellQuote,
19
- treeFingerprint,
20
21
  } from "@heretek-ai/es-core"
21
22
  import { Error as ToolError } from "@opencode/plugin/promise/tool"
23
+ import { HOST_WEB_TOOLS } from "./agents.ts"
22
24
  import type { Runtime } from "./runtime.ts"
23
25
 
24
26
  const WRITE_TOOLS = new Set(["write", "edit"])
@@ -48,8 +50,6 @@ const appendContent = (content: unknown, text: string) =>
48
50
  : text
49
51
 
50
52
  export function createPolicyHooks(runtime: Runtime) {
51
- const fingerprints = new Map<string, { dir: string; print: string }>()
52
-
53
53
  const before = async (event: { tool: string; agent: string; id: string; input: unknown }) => {
54
54
  const seat = seatOf(event.agent)
55
55
  const input = (event.input ?? {}) as Record<string, any>
@@ -58,11 +58,17 @@ export function createPolicyHooks(runtime: Runtime) {
58
58
  if (stop.stopped)
59
59
  deny(`The factory is stopped (.factory/STOP: ${stop.reason}). A human must remove it and resume.`)
60
60
  }
61
- if (seat && !event.tool.startsWith("es_")) {
61
+ // A halted run (spend ceiling, runtime cap, drift, STOP) stops every seat
62
+ // tool except es_status, so no seat keeps spending through its own es_* tools.
63
+ if (seat && event.tool !== "es_status") {
62
64
  const state = await runtime.factory.read().catch(() => undefined)
63
65
  if (state?.stage === "HALTED")
64
66
  deny(`The factory is halted: ${state.halt?.reason}. Stop and report; a human must resume.`)
65
67
  }
68
+ if (agentSpec(event.agent)?.web === "cached" && (HOST_WEB_TOOLS as readonly string[]).includes(event.tool))
69
+ deny(
70
+ `Seat "${event.agent}" gets web facts only through es_research_search and es_research_fetch (cached and citable); ${event.tool} is not available to it.`,
71
+ )
66
72
  const context = await runtime.policy()
67
73
  if (WRITE_TOOLS.has(event.tool) && typeof input.path === "string") {
68
74
  const decision = evaluateWrite(context, event.agent, input.path)
@@ -80,14 +86,37 @@ export function createPolicyHooks(runtime: Runtime) {
80
86
  if (event.tool === "shell" && typeof input.command === "string") {
81
87
  const decision = evaluateShell(context, event.agent, input.command, { sandboxAvailable: bwrapAvailable() })
82
88
  if (decision.effect === "deny") deny(decision.reason)
83
- const cwd =
84
- typeof input.workdir === "string"
85
- ? path.resolve(runtime.root, input.workdir)
86
- : (context.worktree ?? runtime.root)
87
- if (decision.effect === "allow" && decision.mode === "readonly-sandbox") {
88
- event.input = { ...input, command: readonlyWrap(["sh", "-c", input.command], cwd).map(shellQuote).join(" ") }
89
- } else if (decision.effect === "allow" && decision.mode === "readonly-checked") {
90
- fingerprints.set(event.id, { dir: cwd, print: await treeFingerprint(cwd) })
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The last execute.before hook: re-evaluates the final shell command (a
94
+ * PreToolUse hook may have rewritten it) and wraps it in its sandbox, so no
95
+ * later rewrite can run unsandboxed and hook matchers see the plain command.
96
+ */
97
+ const sandbox = async (event: { tool: string; agent: string; id: string; input: unknown }) => {
98
+ const input = (event.input ?? {}) as Record<string, any>
99
+ if (event.tool === "shell" && typeof input.command === "string") {
100
+ const context = await runtime.policy()
101
+ const decision = evaluateShell(context, event.agent, input.command, { sandboxAvailable: bwrapAvailable() })
102
+ if (decision.effect === "deny") deny(decision.reason)
103
+ if (decision.effect === "allow" && decision.mode === "sandbox") {
104
+ const cwd =
105
+ typeof input.workdir === "string"
106
+ ? path.resolve(runtime.root, input.workdir)
107
+ : (context.worktree ?? runtime.root)
108
+ const argv = sandboxArgv(
109
+ {
110
+ kind: decision.kind,
111
+ root: runtime.root,
112
+ cwd,
113
+ stateDir: runtime.stateDir,
114
+ offline: decision.offline,
115
+ ...(decision.kind === "programmer" && context.worktree ? { writable: context.worktree } : {}),
116
+ },
117
+ ["sh", "-c", input.command],
118
+ )
119
+ event.input = { ...input, command: argv.map(shellQuote).join(" ") }
91
120
  }
92
121
  }
93
122
  }
@@ -101,23 +130,6 @@ export function createPolicyHooks(runtime: Runtime) {
101
130
  result?: any
102
131
  }) => {
103
132
  const seat = seatOf(event.agent)
104
- const checked = fingerprints.get(event.id)
105
- if (checked) {
106
- fingerprints.delete(event.id)
107
- if ((await treeFingerprint(checked.dir)) !== checked.print) {
108
- await runtime.factory
109
- .halt(`agent:${event.agent}`, `read-only seat ${event.agent} changed files via the shell`)
110
- .catch(() => undefined)
111
- if (event.status === "completed")
112
- event.result = {
113
- ...event.result,
114
- content: appendContent(
115
- event.result?.content,
116
- "VIOLATION: this read-only seat modified the checkout. The factory has been halted.",
117
- ),
118
- }
119
- }
120
- }
121
133
  if (event.status !== "completed") return
122
134
  const input = (event.input ?? {}) as Record<string, any>
123
135
  const files =
@@ -157,6 +169,7 @@ export function createPolicyHooks(runtime: Runtime) {
157
169
  commands: config.commands.filter((check) => check.kind === "format" || check.kind === "lint"),
158
170
  },
159
171
  stateDir: runtime.stateDir,
172
+ sandbox: "required",
160
173
  })
161
174
  if (report.findings.length || !report.passed)
162
175
  event.result = {
@@ -217,5 +230,5 @@ export function createPolicyHooks(runtime: Runtime) {
217
230
  for (const key of Object.keys(event.env)) if (/^OPENCODE_(SERVER_)?PASSWORD$/.test(key)) delete event.env[key]
218
231
  }
219
232
 
220
- return { before, after, evaluate, shellEnv }
233
+ return { before, sandbox, after, evaluate, shellEnv }
221
234
  }
package/src/research.ts CHANGED
@@ -1,63 +1,127 @@
1
1
  // Research integration: the configured search provider becomes the host's
2
- // websearch (results cached content-addressed, so they can be cited), and the
3
- // host's own webfetch results are cached too.
4
- import { researchCache, selectProvider } from "@heretek-ai/es-core"
2
+ // websearch, and host web results are cached content-addressed so they can be
3
+ // cited. Caching is for Epistemic Swarm seats in a project that already has a
4
+ // .factory/ directory: the user's own agents get plain results, and no
5
+ // project gains a .factory/ because someone searched the web.
6
+ import { stat } from "node:fs/promises"
7
+ import { factoryLayout, researchCache, seatOf, selectProvider } from "@heretek-ai/es-core"
5
8
  import type { Runtime } from "./runtime.ts"
6
9
 
10
+ interface SearchHit {
11
+ readonly url: string
12
+ readonly title?: string
13
+ readonly text: string
14
+ }
15
+
16
+ /** Results the provider returned, waiting for the tool hook (which knows the agent) to cache them. */
17
+ export class PendingSearches {
18
+ private readonly entries: Array<{ query: string; provider: string; hits: readonly SearchHit[]; at: number }> = []
19
+ private static readonly TTL_MS = 5 * 60_000
20
+
21
+ add(query: string, provider: string, hits: readonly SearchHit[]) {
22
+ this.prune()
23
+ this.entries.push({ query, provider, hits, at: Date.now() })
24
+ }
25
+
26
+ /** The oldest unclaimed results for this query (each search is claimed once). */
27
+ take(query: string) {
28
+ this.prune()
29
+ const index = this.entries.findIndex((entry) => entry.query === query)
30
+ return index < 0 ? undefined : this.entries.splice(index, 1)[0]
31
+ }
32
+
33
+ private prune() {
34
+ const cutoff = Date.now() - PendingSearches.TTL_MS
35
+ while (this.entries.length && this.entries[0]!.at < cutoff) this.entries.shift()
36
+ }
37
+ }
38
+
39
+ /** Host web results are cached only for an ES seat, in a project that already has .factory/. */
40
+ async function cacheable(runtime: Runtime, agent: string | undefined): Promise<boolean> {
41
+ if (!seatOf(agent)) return false
42
+ return stat(factoryLayout(runtime.root).dir).then(
43
+ (info) => info.isDirectory(),
44
+ () => false,
45
+ )
46
+ }
47
+
7
48
  export function registerWebsearch(
8
49
  editor: { add(definition: any): void; default: { get(): unknown; set(id: string): void } },
9
50
  runtime: Runtime,
51
+ pending: PendingSearches,
10
52
  ) {
11
- const provider = selectProvider(runtime.options.searchProvider)
53
+ const searxng = runtime.config.research?.searxngUrl
54
+ const env = searxng ? { ...process.env, SEARXNG_URL: searxng } : process.env
55
+ const provider = selectProvider(runtime.options.searchProvider, env)
12
56
  if (!provider) return
13
- const cache = researchCache(runtime.root)
14
57
  editor.add({
15
58
  id: "epistemic-swarm",
16
- name: `Epistemic Swarm (${provider.id}, cached)`,
59
+ name: `Epistemic Swarm (${provider.id})`,
17
60
  execute: async (input: { query: string; limit?: number }, context: { signal: AbortSignal }) => {
18
- const results = await provider.search(input.query, { limit: input.limit ?? 8, signal: context.signal })
19
- const out = []
20
- for (const result of results) {
21
- const cached = await cache.put({
22
- url: result.url,
23
- title: result.title,
24
- text: result.content ?? result.snippet,
25
- provider: provider.id,
26
- query: input.query,
27
- })
28
- out.push({
29
- url: result.url,
30
- title: result.title,
31
- content: `[cached sha256:${cached.meta.sha256}]\n${result.content ?? result.snippet}`,
32
- time: {},
33
- })
34
- }
35
- return out
61
+ const results = await provider.search(input.query, { limit: input.limit ?? 8, signal: context.signal, env })
62
+ const hits = results.map((result) => ({
63
+ url: result.url,
64
+ title: result.title,
65
+ text: result.content ?? result.snippet,
66
+ }))
67
+ pending.add(input.query, provider.id, hits)
68
+ return hits.map((hit) => ({ url: hit.url, title: hit.title, content: hit.text, time: {} }))
36
69
  },
37
70
  })
38
71
  // Additive: become the default only when nothing else is selected.
39
72
  if (editor.default.get() === undefined) editor.default.set("epistemic-swarm")
40
73
  }
41
74
 
42
- /** Cache host webfetch results so they can be cited as [VERIFIED: sha256:… "quote"]. */
43
- export function createWebfetchCache(runtime: Runtime) {
44
- const cache = researchCache(runtime.root)
45
- return async (event: { tool: string; status: string; input: unknown; result?: any }) => {
75
+ const contentText = (content: unknown): string =>
76
+ typeof content === "string"
77
+ ? content
78
+ : Array.isArray(content)
79
+ ? content.map((part: any) => part?.text ?? "").join("\n")
80
+ : ""
81
+
82
+ const prepend = (result: any, note: string) => {
83
+ const content = result?.content
84
+ return {
85
+ ...result,
86
+ content: typeof content === "string" ? `${note}\n${content}` : [{ type: "text", text: note }, ...(content ?? [])],
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Cache a seat's host webfetch and websearch results so they can be cited as
92
+ * [VERIFIED: sha256:… "quote"]. Other agents' results pass through untouched.
93
+ */
94
+ export function createWebCache(runtime: Runtime, pending: PendingSearches) {
95
+ const cache = researchCache(runtime.root, runtime.stateDir)
96
+ return async (event: { tool: string; status: string; agent?: string; input: unknown; result?: any }) => {
97
+ if (event.tool === "websearch") {
98
+ const query = (event.input as { query?: string } | undefined)?.query
99
+ const search = typeof query === "string" ? pending.take(query) : undefined
100
+ if (!search || event.status !== "completed" || !(await cacheable(runtime, event.agent))) return
101
+ const lines: string[] = []
102
+ for (const hit of search.hits) {
103
+ if (!hit.text?.trim()) continue
104
+ const cached = await cache.put({
105
+ url: hit.url,
106
+ title: hit.title,
107
+ text: hit.text,
108
+ provider: search.provider,
109
+ query: search.query,
110
+ })
111
+ lines.push(`[cached sha256:${cached.meta.sha256}] ${hit.url}`)
112
+ }
113
+ if (lines.length)
114
+ event.result = prepend(event.result, `${lines.join("\n")}\n(cite with [VERIFIED: sha256:<hash> "exact words"])`)
115
+ return
116
+ }
46
117
  if (event.tool !== "webfetch" || event.status !== "completed") return
47
118
  const url = (event.input as { url?: string } | undefined)?.url
48
- const content = event.result?.content
49
- const text =
50
- typeof content === "string"
51
- ? content
52
- : Array.isArray(content)
53
- ? content.map((part: any) => part?.text ?? "").join("\n")
54
- : ""
55
- if (!url || text.trim().length < 40) return
119
+ const text = contentText(event.result?.content)
120
+ if (!url || text.trim().length < 40 || !(await cacheable(runtime, event.agent))) return
56
121
  const cached = await cache.put({ url, text, provider: "webfetch" })
57
- const note = `[cached as sha256:${cached.meta.sha256}; cite with [VERIFIED: sha256:${cached.meta.sha256} "exact words"]]`
58
- event.result = {
59
- ...event.result,
60
- content: typeof content === "string" ? `${note}\n${content}` : [{ type: "text", text: note }, ...(content ?? [])],
61
- }
122
+ event.result = prepend(
123
+ event.result,
124
+ `[cached as sha256:${cached.meta.sha256}; cite with [VERIFIED: sha256:${cached.meta.sha256} "exact words"]]`,
125
+ )
62
126
  }
63
127
  }
package/src/rpc-def.ts CHANGED
@@ -1,6 +1,7 @@
1
- // RPC contract between the server plugin and the TUI. These methods are the
2
- // TUI half of the human-only channel: no agent tool reaches them, and the TUI
3
- // only calls them after a human confirms a dialog showing what is approved.
1
+ // RPC contract between the server plugin and the TUI. Previews are shown in
2
+ // the TUI; human-only mutations (approve, trust, resume) happen at a terminal
3
+ // with the passphrase (`es approve`, `es trust`, `es factory resume`), never
4
+ // through an agent tool or RPC. Only lspInstall still mutates via RPC.
4
5
  import { Rpc } from "@opencode/plugin/rpc"
5
6
 
6
7
  const obj = (properties: Record<string, unknown>, required: string[] = []) => ({
@@ -26,25 +27,16 @@ export const EsRpc = Rpc.define({
26
27
  input: obj({}),
27
28
  output: obj({ summary: str, stage: str, pending: strArray }, ["summary", "stage", "pending"]),
28
29
  },
30
+ /** The effective config (JSON), the files it came from, and load warnings to show once. */
31
+ configState: {
32
+ input: obj({}),
33
+ output: obj({ config: str, sources: strArray, warnings: strArray }, ["config", "sources", "warnings"]),
34
+ },
29
35
  previewApproval: {
30
36
  input: obj({ stage: { type: "string", enum: ["frontier", "spec"] } }, ["stage"]),
31
37
  output: Preview,
32
38
  },
33
- approve: {
34
- input: obj({ stage: { type: "string", enum: ["frontier", "spec"] }, user: str, token: str }, [
35
- "stage",
36
- "user",
37
- "token",
38
- ]),
39
- output: obj({ message: str }, ["message"]),
40
- errors: { refused: obj({ reason: str }, ["reason"]) },
41
- },
42
39
  previewTrust: { input: obj({}), output: Preview },
43
- trust: {
44
- input: obj({ user: str, token: str }, ["user", "token"]),
45
- output: obj({ message: str }, ["message"]),
46
- errors: { refused: obj({ reason: str }, ["reason"]) },
47
- },
48
40
  previewResume: { input: obj({}), output: Preview },
49
41
  previewLspInstall: { input: obj({ id: str }, ["id"]), output: Preview },
50
42
  lspInstall: {
@@ -52,20 +44,6 @@ export const EsRpc = Rpc.define({
52
44
  output: obj({ message: str }, ["message"]),
53
45
  errors: { refused: obj({ reason: str }, ["reason"]) },
54
46
  },
55
- resume: {
56
- input: obj(
57
- {
58
- user: str,
59
- token: str,
60
- acceptControlDrift: { type: "boolean" },
61
- raiseCeilingUSD: { type: "number" },
62
- extendRuntime: { type: "boolean" },
63
- },
64
- ["user", "token"],
65
- ),
66
- output: obj({ message: str }, ["message"]),
67
- errors: { refused: obj({ reason: str }, ["reason"]) },
68
- },
69
47
  factoryState: {
70
48
  input: obj({}),
71
49
  output: obj(
@@ -81,6 +59,16 @@ export const EsRpc = Rpc.define({
81
59
  ]),
82
60
  phases: { type: "array" },
83
61
  pending: strArray,
62
+ audits: { type: "array" },
63
+ tree: obj(
64
+ Object.fromEntries(
65
+ ["round", "total", "settled", "open", "deferred", "facts", "frontier"].map((key) => [
66
+ key,
67
+ { type: "number" },
68
+ ]),
69
+ ),
70
+ ["round", "total", "settled", "open", "deferred", "facts", "frontier"],
71
+ ),
84
72
  },
85
73
  ["stage", "spend", "phases", "pending"],
86
74
  ),
package/src/rpc.ts CHANGED
@@ -1,29 +1,29 @@
1
- // Server side of the human-only channel. Each mutating method needs a token
2
- // from a preview made in the last two minutes over the same subject, so the
3
- // human confirms exactly what the dialog showed (no swap between preview and
4
- // confirm).
1
+ // Server side of the TUI previews. Human-only mutations (approve, trust,
2
+ // resume) happen at a terminal with the passphrase-sealed key, never via RPC:
3
+ // the TUI previews, then points at `es approve`, `es trust`, `es factory
4
+ // resume`. Only lspInstall still mutates via a preview token.
5
5
  import { randomBytes } from "node:crypto"
6
6
  import {
7
7
  type ApprovalStage,
8
+ allAudits,
8
9
  approvalSubject,
9
10
  capabilityLoss,
10
11
  commandSetHash,
11
- factoryLayout,
12
+ describeTarget,
12
13
  factorySummary,
13
14
  type HookEngine,
14
15
  hashJson,
15
16
  installServer,
16
17
  isTrusted,
17
18
  loadGatesConfig,
19
+ readFrontier,
18
20
  readIdeas,
19
21
  readPlan,
20
22
  readResult,
21
23
  readScores,
22
- recordApproval,
23
24
  recordConsent,
24
- trustProject,
25
+ treeCounts,
25
26
  verifyControl,
26
- writeJson,
27
27
  } from "@heretek-ai/es-core"
28
28
  import type { Runtime } from "./runtime.ts"
29
29
  import { pendingApprovals } from "./tools.ts"
@@ -36,7 +36,7 @@ interface Ticket {
36
36
 
37
37
  const TTL_MS = 120_000
38
38
 
39
- export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>, hooks?: HookEngine) {
39
+ export function createRpcHandlers(runtime: Runtime, _notify: () => Promise<void>, hooks?: HookEngine) {
40
40
  const tickets = new Map<string, Ticket>()
41
41
  const issue = (kind: string, subject: unknown) => {
42
42
  const token = randomBytes(12).toString("hex")
@@ -75,14 +75,21 @@ export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>,
75
75
  status: async () => {
76
76
  const state = await runtime.factory.read()
77
77
  return {
78
- summary: factorySummary(state),
78
+ summary: await runtime.factory.summary(state),
79
79
  stage: state?.stage ?? "NONE",
80
80
  pending: (await pendingApprovals(runtime.root)).map((item) => item.stage),
81
81
  }
82
82
  },
83
+ configState: async () => ({
84
+ config: JSON.stringify(runtime.config),
85
+ sources: [...runtime.configSources],
86
+ warnings: [...runtime.warnings],
87
+ }),
83
88
  factoryState: async () => {
84
89
  const state = await runtime.factory.read()
90
+ const frontier = await readFrontier(runtime.root).catch(() => undefined)
85
91
  return {
92
+ ...(frontier ? { tree: treeCounts(frontier) } : {}),
86
93
  stage: state?.stage ?? "NONE",
87
94
  ...(state?.runId ? { runId: state.runId } : {}),
88
95
  ...(state?.halt ? { halt: state.halt.reason } : {}),
@@ -101,6 +108,15 @@ export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>,
101
108
  qa: Object.fromEntries(Object.entries(phase.qa).filter(([, verdict]) => verdict !== undefined)),
102
109
  })),
103
110
  pending: (await pendingApprovals(runtime.root)).map((item) => item.stage),
111
+ audits: (state ? allAudits(state) : []).map((audit) => ({
112
+ id: audit.id,
113
+ target: describeTarget(audit.target),
114
+ status: audit.status,
115
+ round: audit.round,
116
+ thesis: audit.thesis?.verdict ?? "-",
117
+ antithesis: audit.antithesis?.verdict ?? "-",
118
+ ...(audit.tiebreak ? { tiebreak: audit.tiebreak.verdict } : {}),
119
+ })),
104
120
  }
105
121
  },
106
122
  lspState: async () => {
@@ -189,31 +205,6 @@ export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>,
189
205
  }
190
206
  }
191
207
  },
192
- approve: async (input: unknown, context: any) => {
193
- const { stage, user, token } = input as { stage: ApprovalStage; user: string; token: string }
194
- try {
195
- redeem(token, `approve:${stage}`, (await approvalSubject(runtime.root, stage)).subject)
196
- const record = await recordApproval(runtime.root, {
197
- stage,
198
- channel: "tui",
199
- approvedBy: user,
200
- stateDir: runtime.stateDir,
201
- })
202
- await writeJson(
203
- factoryLayout(runtime.root).pending,
204
- (await pendingApprovals(runtime.root)).filter((item) => item.stage !== stage),
205
- )
206
- if (stage === "frontier") {
207
- const state = await runtime.factory.read()
208
- if (!state) await runtime.factory.begin(`human:${user}`)
209
- if ((await runtime.factory.read())?.stage === "GRILL") await runtime.factory.beginResearch(`human:${user}`)
210
- }
211
- await notify()
212
- return { message: `Approved ${stage} as ${record.approvedBy}.` }
213
- } catch (error) {
214
- return refused(context, error)
215
- }
216
- },
217
208
  previewTrust: async () => {
218
209
  const subject = await trustSubject()
219
210
  const trusted =
@@ -227,27 +218,6 @@ export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>,
227
218
  token: issue("trust", subject.key),
228
219
  }
229
220
  },
230
- trust: async (input: unknown, context: any) => {
231
- const { user, token } = input as { user: string; token: string }
232
- try {
233
- const subject = await trustSubject()
234
- redeem(token, "trust", subject.key)
235
- await trustProject(runtime.root, subject.gates.hash, subject.gates.lines, {
236
- approvedBy: user,
237
- stateDir: runtime.stateDir,
238
- })
239
- if (subject.hooks.hash)
240
- await trustProject(runtime.root, subject.hooks.hash, subject.hooks.lines, {
241
- approvedBy: user,
242
- stateDir: runtime.stateDir,
243
- kind: "hooks",
244
- })
245
- await hooks?.refresh()
246
- return { message: `Trusted ${subject.lines.length} command/hook line(s).` }
247
- } catch (error) {
248
- return refused(context, error)
249
- }
250
- },
251
221
  previewLspInstall: async (input: unknown) => {
252
222
  const { id } = input as { id: string }
253
223
  const server = (await runtime.lsp.settings()).servers.find((item) => item.id === id)
@@ -306,26 +276,5 @@ export function createRpcHandlers(runtime: Runtime, notify: () => Promise<void>,
306
276
  token: issue("resume", state.halt),
307
277
  }
308
278
  },
309
- resume: async (input: unknown, context: any) => {
310
- const { user, token, acceptControlDrift, raiseCeilingUSD, extendRuntime } = input as {
311
- user: string
312
- token: string
313
- acceptControlDrift?: boolean
314
- raiseCeilingUSD?: number
315
- extendRuntime?: boolean
316
- }
317
- try {
318
- redeem(token, "resume", (await runtime.factory.read())?.halt)
319
- const state = await runtime.factory.resume(user, {
320
- ...(acceptControlDrift ? { acceptControlDrift } : {}),
321
- ...(raiseCeilingUSD ? { raiseCeilingUSD } : {}),
322
- ...(extendRuntime ? { extendRuntime } : {}),
323
- })
324
- await notify()
325
- return { message: `Resumed at ${state.stage}.` }
326
- } catch (error) {
327
- return refused(context, error)
328
- }
329
- },
330
279
  }
331
280
  }