@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 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 { ok: false, locked: true, retryAfterSec: gate.retryAfterSec ?? 0 };
30
- const title = `Approve ${input.stage} as ${input.approvedBy ?? userInfo().username}`;
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 { ok: false, locked: true, retryAfterSec: check.retryAfterSec ?? 0 };
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.0",
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.3.0",
47
+ "@heretek-ai/es-core": "2.5.0",
48
48
  "@opencode/plugin": "2.0.24"
49
49
  },
50
50
  "devDependencies": {
@@ -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 { ok: false, locked: true, retryAfterSec: gate.retryAfterSec ?? 0 }
88
+ if (!gate.allowed) return locked(gate.retryAfterSec ?? 0)
82
89
 
83
- const title = `Approve ${input.stage} as ${input.approvedBy ?? userInfo().username}`
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 { ok: false, locked: true, retryAfterSec: check.retryAfterSec ?? 0 }
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
- const state = await runtime.factory.read()
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
- const state = await runtime.factory.read()
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)