@heretek-ai/epistemic-swarm 2.3.0 → 2.5.0
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 +17 -3
- package/package.json +2 -2
- package/src/approve-tui.ts +22 -4
- package/src/policy.ts +23 -0
- package/src/rpc.ts +69 -2
package/dist/tui.js
CHANGED
|
@@ -22,16 +22,29 @@ var TERMINAL_FALLBACK = (stage) => `Run \`es approve ${stage}\` at a terminal wi
|
|
|
22
22
|
var APPROVAL_PREVIEW_TTL_MS = 120000;
|
|
23
23
|
async function approveInTui(input) {
|
|
24
24
|
const dir = input.stateDir ?? defaultStateDir();
|
|
25
|
+
const clock = input.now ?? Date.now;
|
|
26
|
+
const locked = (retryAfterSec) => ({
|
|
27
|
+
ok: false,
|
|
28
|
+
locked: true,
|
|
29
|
+
retryAfterSec,
|
|
30
|
+
retryAt: new Date(clock() + retryAfterSec * 1000).toISOString()
|
|
31
|
+
});
|
|
25
32
|
if (!await exists(factoryLayout(input.root).dir) || !await hasHumanKey(dir))
|
|
26
33
|
return { ok: false, fallback: true, reason: TERMINAL_FALLBACK(input.stage) };
|
|
27
34
|
const gate = await checkApprovalAttempts(dir);
|
|
28
35
|
if (!gate.allowed)
|
|
29
|
-
return
|
|
30
|
-
const
|
|
36
|
+
return locked(gate.retryAfterSec ?? 0);
|
|
37
|
+
const base = `Approve ${input.stage} as ${input.approvedBy ?? userInfo().username}`;
|
|
38
|
+
let feedback;
|
|
31
39
|
for (;; ) {
|
|
40
|
+
const title = feedback ? `${base} \u2014 ${feedback}` : base;
|
|
32
41
|
const codepoints = await input.readPassphrase(title);
|
|
33
42
|
if (codepoints === undefined)
|
|
34
43
|
return { ok: false, cancelled: true };
|
|
44
|
+
if (codepoints.length === 0) {
|
|
45
|
+
zeroCodepoints(codepoints);
|
|
46
|
+
return { ok: false, cancelled: true };
|
|
47
|
+
}
|
|
35
48
|
const passphrase = codepoints.join("");
|
|
36
49
|
zeroCodepoints(codepoints);
|
|
37
50
|
try {
|
|
@@ -59,7 +72,8 @@ async function approveInTui(input) {
|
|
|
59
72
|
if (error instanceof HumanKeyError && error.message === "wrong passphrase") {
|
|
60
73
|
const check = await noteWrongPassphrase({ dir, root: input.root, stage: input.stage, channel: "tui" });
|
|
61
74
|
if (!check.allowed)
|
|
62
|
-
return
|
|
75
|
+
return locked(check.retryAfterSec ?? 0);
|
|
76
|
+
feedback = `Wrong passphrase. ${check.remaining ?? 0} attempts left.`;
|
|
63
77
|
continue;
|
|
64
78
|
}
|
|
65
79
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"esRelease": {
|
|
5
5
|
"order": 3
|
|
6
6
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test": "bun test"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@heretek-ai/es-core": "2.
|
|
47
|
+
"@heretek-ai/es-core": "2.5.0",
|
|
48
48
|
"@opencode/plugin": "2.0.24"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
package/src/approve-tui.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ApproveTuiInput {
|
|
|
51
51
|
export type ApproveTuiOutcome =
|
|
52
52
|
| { readonly ok: true; readonly record: Approval }
|
|
53
53
|
| { readonly ok: false; readonly fallback: true; readonly reason: string }
|
|
54
|
-
| { readonly ok: false; readonly locked: true; readonly retryAfterSec: number }
|
|
54
|
+
| { readonly ok: false; readonly locked: true; readonly retryAfterSec: number; readonly retryAt: string }
|
|
55
55
|
| { readonly ok: false; readonly cancelled: true }
|
|
56
56
|
|
|
57
57
|
const TERMINAL_FALLBACK = (stage: ApprovalStage) =>
|
|
@@ -71,6 +71,13 @@ export const APPROVAL_PREVIEW_TTL_MS = 120_000
|
|
|
71
71
|
*/
|
|
72
72
|
export async function approveInTui(input: ApproveTuiInput): Promise<ApproveTuiOutcome> {
|
|
73
73
|
const dir = input.stateDir ?? defaultStateDir()
|
|
74
|
+
const clock = input.now ?? Date.now
|
|
75
|
+
const locked = (retryAfterSec: number): ApproveTuiOutcome => ({
|
|
76
|
+
ok: false,
|
|
77
|
+
locked: true,
|
|
78
|
+
retryAfterSec,
|
|
79
|
+
retryAt: new Date(clock() + retryAfterSec * 1000).toISOString(),
|
|
80
|
+
})
|
|
74
81
|
// A TUI that cannot see the project root and the sealed key (a remote
|
|
75
82
|
// attach) keeps the "approve in a terminal" alert instead of signing
|
|
76
83
|
// anywhere else.
|
|
@@ -78,12 +85,22 @@ export async function approveInTui(input: ApproveTuiInput): Promise<ApproveTuiOu
|
|
|
78
85
|
return { ok: false, fallback: true, reason: TERMINAL_FALLBACK(input.stage) }
|
|
79
86
|
|
|
80
87
|
const gate = await checkApprovalAttempts(dir)
|
|
81
|
-
if (!gate.allowed) return
|
|
88
|
+
if (!gate.allowed) return locked(gate.retryAfterSec ?? 0)
|
|
82
89
|
|
|
83
|
-
const
|
|
90
|
+
const base = `Approve ${input.stage} as ${input.approvedBy ?? userInfo().username}`
|
|
91
|
+
// #353: the reopened dialog names the failure reason and the attempts
|
|
92
|
+
// left, so a wrong passphrase is visible instead of silent.
|
|
93
|
+
let feedback: string | undefined
|
|
84
94
|
for (;;) {
|
|
95
|
+
const title = feedback ? `${base} — ${feedback}` : base
|
|
85
96
|
const codepoints = await input.readPassphrase(title)
|
|
86
97
|
if (codepoints === undefined) return { ok: false, cancelled: true }
|
|
98
|
+
// #353: an empty Enter cancels like the CLI ("empty cancels"), and
|
|
99
|
+
// records no failure.
|
|
100
|
+
if (codepoints.length === 0) {
|
|
101
|
+
zeroCodepoints(codepoints)
|
|
102
|
+
return { ok: false, cancelled: true }
|
|
103
|
+
}
|
|
87
104
|
// The string cannot be zeroed in JS (same as the CLI's echo-off input):
|
|
88
105
|
// it lives in this local only, and the array is zeroed at once.
|
|
89
106
|
const passphrase = codepoints.join("")
|
|
@@ -115,7 +132,8 @@ export async function approveInTui(input: ApproveTuiInput): Promise<ApproveTuiOu
|
|
|
115
132
|
} catch (error) {
|
|
116
133
|
if (error instanceof HumanKeyError && error.message === "wrong passphrase") {
|
|
117
134
|
const check = await noteWrongPassphrase({ dir, root: input.root, stage: input.stage, channel: "tui" })
|
|
118
|
-
if (!check.allowed) return
|
|
135
|
+
if (!check.allowed) return locked(check.retryAfterSec ?? 0)
|
|
136
|
+
feedback = `Wrong passphrase. ${check.remaining ?? 0} attempts left.`
|
|
119
137
|
continue
|
|
120
138
|
}
|
|
121
139
|
throw error
|
package/src/policy.ts
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path"
|
|
|
7
7
|
import {
|
|
8
8
|
agentSpec,
|
|
9
9
|
bwrapAvailable,
|
|
10
|
+
bwrapOverlay,
|
|
10
11
|
checkStopFile,
|
|
11
12
|
evaluateRead,
|
|
12
13
|
evaluateShell,
|
|
@@ -39,6 +40,28 @@ export function patchTargets(patchText: string): string[] {
|
|
|
39
40
|
return targets
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The seat-shell sandbox mode for `cwd` (#326): "sandboxed" when bubblewrap
|
|
45
|
+
* can build the seat sandbox including the overlay mounts the shell backstop
|
|
46
|
+
* needs, "host-fallback" when bwrap is present but the overlay is unavailable
|
|
47
|
+
* for `cwd` (e.g. a btrfs mount without userxattr — the shell then runs
|
|
48
|
+
* without the read-only backstop, the #294 fallback), "unavailable" when
|
|
49
|
+
* there is no bwrap at all (seat shells are refused outright).
|
|
50
|
+
*
|
|
51
|
+
* Naming only: enforcement is unchanged. Whether the fallback stays or the
|
|
52
|
+
* shell fails closed on such hosts is the human's product decision on #326;
|
|
53
|
+
* this names the mode so the control matrix asserts the designed behaviour
|
|
54
|
+
* for the mode the host is in, with the mode in the assertion output.
|
|
55
|
+
*/
|
|
56
|
+
export function seatShellSandboxMode(cwd: string): "sandboxed" | "host-fallback" | "unavailable" {
|
|
57
|
+
if (!bwrapAvailable()) return "unavailable"
|
|
58
|
+
try {
|
|
59
|
+
return bwrapOverlay(cwd) ? "sandboxed" : "host-fallback"
|
|
60
|
+
} catch {
|
|
61
|
+
return "host-fallback"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
const deny = (message: string): never => {
|
|
43
66
|
throw new ToolError({ message })
|
|
44
67
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -70,6 +70,37 @@ export function createRpcHandlers(
|
|
|
70
70
|
reason: error instanceof Error ? error.message : String(error),
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
// #355: an unreadable run state (missing/forged seal) is a status value,
|
|
74
|
+
// not a throw — otherwise the 5 s footer poll logs `rpc handler failed`
|
|
75
|
+
// every poll. The problem is logged once per distinct problem.
|
|
76
|
+
let lastSealProblem: string | undefined
|
|
77
|
+
const isSealProblem = (error: unknown): string | undefined => {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
79
|
+
return /Cannot verify \.factory\/runtime\/state\.json|\.factory\/runtime\/state\.json has a|is a version .* run/i.test(
|
|
80
|
+
message,
|
|
81
|
+
)
|
|
82
|
+
? message
|
|
83
|
+
: undefined
|
|
84
|
+
}
|
|
85
|
+
const noteSealProblem = (message: string) => {
|
|
86
|
+
if (lastSealProblem !== message) {
|
|
87
|
+
lastSealProblem = message
|
|
88
|
+
console.warn(`[epistemic-swarm] run state unreadable: ${message}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const clearSealProblem = () => {
|
|
92
|
+
if (lastSealProblem !== undefined) {
|
|
93
|
+
lastSealProblem = undefined
|
|
94
|
+
console.warn("[epistemic-swarm] run state readable again")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const sealFix = (message: string): string => {
|
|
98
|
+
if (message.includes("es reseal --sign")) return "es reseal --sign"
|
|
99
|
+
if (message.includes("ES_STATE_DIR")) return "ES_STATE_DIR"
|
|
100
|
+
if (message.includes("/grill")) return "/grill"
|
|
101
|
+
return "see status"
|
|
102
|
+
}
|
|
103
|
+
|
|
73
104
|
/** Everything a trust approval covers: gate commands and project hooks. */
|
|
74
105
|
const trustSubject = async () => {
|
|
75
106
|
const { config } = await loadGatesConfig(runtime.root)
|
|
@@ -87,7 +118,24 @@ export function createRpcHandlers(
|
|
|
87
118
|
|
|
88
119
|
return {
|
|
89
120
|
status: async () => {
|
|
90
|
-
|
|
121
|
+
let state: Awaited<ReturnType<Runtime["factory"]["read"]>>
|
|
122
|
+
try {
|
|
123
|
+
state = await runtime.factory.read()
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const problem = isSealProblem(error)
|
|
126
|
+
if (problem === undefined) throw error
|
|
127
|
+
noteSealProblem(problem)
|
|
128
|
+
const fix = sealFix(problem)
|
|
129
|
+
return {
|
|
130
|
+
summary: `<factory-state>State unreadable: ${problem}</factory-state>`,
|
|
131
|
+
stage: "ERROR",
|
|
132
|
+
pending: [],
|
|
133
|
+
headline: `State unreadable: ${problem}`,
|
|
134
|
+
footer: `ES · state unreadable · ${problem.slice(0, 100)} · fix: ${fix}`,
|
|
135
|
+
sealProblem: problem,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
clearSealProblem()
|
|
91
139
|
const liveness = await readLiveness(runtime.root, state)
|
|
92
140
|
const paused = live.paused?.()
|
|
93
141
|
const footer = footerLine(state, liveness, paused)
|
|
@@ -106,7 +154,26 @@ export function createRpcHandlers(
|
|
|
106
154
|
warnings: [...runtime.warnings],
|
|
107
155
|
}),
|
|
108
156
|
factoryState: async () => {
|
|
109
|
-
|
|
157
|
+
let state: Awaited<ReturnType<Runtime["factory"]["read"]>>
|
|
158
|
+
try {
|
|
159
|
+
state = await runtime.factory.read()
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const problem = isSealProblem(error)
|
|
162
|
+
if (problem === undefined) throw error
|
|
163
|
+
noteSealProblem(problem)
|
|
164
|
+
return {
|
|
165
|
+
headline: `State unreadable: ${problem}`,
|
|
166
|
+
seats: [],
|
|
167
|
+
events: [],
|
|
168
|
+
phases: [],
|
|
169
|
+
audits: [],
|
|
170
|
+
pending: [],
|
|
171
|
+
spend: { usd: 0, estimated: false },
|
|
172
|
+
stage: "ERROR",
|
|
173
|
+
sealProblem: problem,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
clearSealProblem()
|
|
110
177
|
const frontier = await readFrontier(runtime.root).catch(() => undefined)
|
|
111
178
|
const liveness = await readLiveness(runtime.root, state)
|
|
112
179
|
const research = researchLine(liveness)
|