@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/dist/tui.js +224 -174
- package/package.json +2 -2
- package/src/agents.ts +9 -0
- package/src/continue.ts +2 -2
- package/src/panels.tsx +19 -0
- package/src/policy.ts +46 -33
- package/src/research.ts +105 -41
- package/src/rpc-def.ts +19 -31
- package/src/rpc.ts +26 -77
- package/src/runtime.ts +44 -37
- package/src/server.ts +69 -7
- package/src/session.ts +4 -2
- package/src/tools.ts +16 -1
- package/src/tui.tsx +32 -18
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)
|
|
4
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
3
|
-
//
|
|
4
|
-
|
|
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
|
|
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}
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
2
|
-
// TUI
|
|
3
|
-
//
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
}
|