@xpufx/paseo-twofado 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/client/approvals.tsx +1525 -0
  4. package/client/error-boundary.tsx +28 -0
  5. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  6. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  7. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  11. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  12. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  13. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  14. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  15. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  16. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  17. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  18. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  19. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  20. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  21. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  22. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  23. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  24. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  25. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  26. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  27. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  28. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  29. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  30. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  31. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  32. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  33. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  34. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  35. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  36. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  37. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  38. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  39. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  40. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  41. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  42. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  43. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  44. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  45. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  46. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  47. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  48. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  49. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  50. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  51. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  52. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  53. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  54. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  55. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  56. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  57. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  58. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  59. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  60. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  61. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  62. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  63. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  64. package/package.json +36 -0
  65. package/paseo-plugin.json +6 -0
  66. package/server/twofado.ts +536 -0
  67. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  68. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  69. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  70. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  71. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  72. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  73. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  74. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  75. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  76. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  77. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  78. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  79. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  80. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  81. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  82. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  83. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  84. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  85. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  86. package/shared/approval.ts +254 -0
  87. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  88. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  89. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  90. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  91. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  92. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  93. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  94. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  95. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  96. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  97. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
@@ -0,0 +1,536 @@
1
+ import net from "node:net";
2
+ import os from "node:os";
3
+ import type { RpcInput, RpcOutput } from "paseo-plugin-helper/shared";
4
+ import { createPluginLogger, guardRpcHandler } from "paseo-plugin-helper/server";
5
+ import {
6
+ approvalAck,
7
+ approvalSettings,
8
+ approvalStatus,
9
+ approvalTelegramInfo,
10
+ approvalTelegramSetConfig,
11
+ daemonHealth,
12
+ pendingList,
13
+ policyAddRule,
14
+ recentList,
15
+ verdict,
16
+ } from "../shared/approval";
17
+
18
+ const log = createPluginLogger("twofado", { banner: false });
19
+
20
+ const DEFAULT_SOCKET = "/tmp/2fado.sock";
21
+
22
+ function socketCandidates(configured?: string): string[] {
23
+ const candidates: string[] = [];
24
+ const push = (sock: string | undefined) => {
25
+ const trimmed = sock?.trim();
26
+ if (trimmed !== undefined && trimmed.length > 0 && !candidates.includes(trimmed)) {
27
+ candidates.push(trimmed);
28
+ }
29
+ };
30
+ push(configured);
31
+ push(process.env.TWOFADO_SOCKET);
32
+ push(process.env.FADO_SOCKET);
33
+ push(DEFAULT_SOCKET);
34
+ return candidates;
35
+ }
36
+
37
+ interface DaemonPendingItem {
38
+ id: string;
39
+ argv: string[];
40
+ uid: number;
41
+ cwd: string;
42
+ expires_in: number;
43
+ step?: string;
44
+ confirm_of?: string;
45
+ kind?: string;
46
+ link?: string;
47
+ summary?: string;
48
+ acked?: boolean;
49
+ ack_by?: string;
50
+ auth_url?: string;
51
+ preview?: {
52
+ resolved_binary?: string;
53
+ target_cwd?: string;
54
+ affected_count?: number;
55
+ sample_paths?: string[];
56
+ risk_level?: "low" | "medium" | "high" | "critical";
57
+ risk_reason?: string;
58
+ };
59
+ }
60
+
61
+ interface DaemonPendingList {
62
+ items: DaemonPendingItem[];
63
+ }
64
+
65
+ interface DaemonRecentItem {
66
+ id: string;
67
+ argv: string[];
68
+ cwd: string;
69
+ decision: string;
70
+ by: string;
71
+ exit: number;
72
+ output: string;
73
+ step?: string;
74
+ confirm_of?: string;
75
+ kind?: string;
76
+ link?: string;
77
+ summary?: string;
78
+ acked?: boolean;
79
+ ack_by?: string;
80
+ auth_url?: string;
81
+ }
82
+
83
+ interface DaemonRecentList {
84
+ items: DaemonRecentItem[];
85
+ }
86
+
87
+ interface DaemonStatusResponse {
88
+ id: string;
89
+ status: string;
90
+ argv?: string[];
91
+ cwd?: string;
92
+ uid?: number;
93
+ expires_in?: number;
94
+ decision?: string;
95
+ by?: string;
96
+ exit: number;
97
+ output?: string;
98
+ step?: string;
99
+ confirm_of?: string;
100
+ kind?: string;
101
+ link?: string;
102
+ summary?: string;
103
+ acked?: boolean;
104
+ ack_by?: string;
105
+ ack_at?: number;
106
+ auth_url?: string;
107
+ }
108
+
109
+ function callDaemonOn(sock: string, message: unknown, timeoutMs: number): Promise<unknown> {
110
+ return new Promise((resolve, reject) => {
111
+ let buffer = "";
112
+ let settled = false;
113
+ const socket = net.createConnection(sock);
114
+ const fail = (err: Error) => {
115
+ if (settled) return;
116
+ settled = true;
117
+ clearTimeout(timer);
118
+ socket.destroy();
119
+ reject(err);
120
+ };
121
+ const timer = setTimeout(() => fail(new Error(`2fadod timeout (${sock})`)), timeoutMs);
122
+ socket.setEncoding("utf8");
123
+ socket.on("connect", () => {
124
+ socket.write(JSON.stringify(message) + "\n");
125
+ });
126
+ socket.on("data", (data: string) => {
127
+ buffer += data;
128
+ const nl = buffer.indexOf("\n");
129
+ if (nl >= 0) {
130
+ const line = buffer.slice(0, nl);
131
+ if (settled) return;
132
+ settled = true;
133
+ clearTimeout(timer);
134
+ socket.destroy();
135
+ try {
136
+ resolve(JSON.parse(line));
137
+ } catch (err) {
138
+ reject(err instanceof Error ? err : new Error(String(err)));
139
+ }
140
+ }
141
+ });
142
+ socket.on("error", (err) => fail(err instanceof Error ? err : new Error(String(err))));
143
+ socket.on("close", () => fail(new Error(`2fadod closed connection (${sock})`)));
144
+ });
145
+ }
146
+
147
+ async function callDaemon(
148
+ message: unknown,
149
+ configured?: string,
150
+ timeoutMs = 2000,
151
+ ): Promise<unknown> {
152
+ let lastError: unknown = new Error("no 2fadod socket candidates");
153
+ for (const sock of socketCandidates(configured)) {
154
+ try {
155
+ return await callDaemonOn(sock, message, timeoutMs);
156
+ } catch (err) {
157
+ lastError = err;
158
+ }
159
+ }
160
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
161
+ }
162
+
163
+ async function listPendingInner(
164
+ input?: RpcInput<typeof pendingList>,
165
+ ): Promise<RpcOutput<typeof pendingList>> {
166
+ try {
167
+ const raw = (await callDaemon({ list: {} }, input?.socketPath)) as DaemonPendingList;
168
+ const host = os.hostname();
169
+ const items = Array.isArray(raw.items) ? raw.items : [];
170
+ return {
171
+ items: items.map((item) => ({
172
+ id: item.id,
173
+ argv: Array.isArray(item.argv) ? item.argv : [],
174
+ host,
175
+ caller: String(item.uid),
176
+ cwd: typeof item.cwd === "string" ? item.cwd : "",
177
+ expiresIn: typeof item.expires_in === "number" ? item.expires_in : 0,
178
+ step: item.step === "confirm" ? ("confirm" as const) : ("initial" as const),
179
+ confirmOf: item.confirm_of,
180
+ kind: item.kind,
181
+ link: item.link,
182
+ summary: item.summary,
183
+ acked: item.acked,
184
+ ackBy: item.ack_by,
185
+ authUrl: item.auth_url,
186
+ preview: item.preview
187
+ ? {
188
+ resolvedBinary: item.preview.resolved_binary,
189
+ targetCwd: item.preview.target_cwd,
190
+ affectedCount: item.preview.affected_count,
191
+ samplePaths: item.preview.sample_paths,
192
+ riskLevel: item.preview.risk_level,
193
+ riskReason: item.preview.risk_reason,
194
+ }
195
+ : undefined,
196
+ })),
197
+ };
198
+ } catch (err) {
199
+ log.warn("pending list failed", { error: err });
200
+ return { items: [] };
201
+ }
202
+ }
203
+
204
+ async function submitVerdictInner(
205
+ input?: RpcInput<typeof verdict>,
206
+ ): Promise<RpcOutput<typeof verdict>> {
207
+ if (input === undefined) return { recorded: false };
208
+ try {
209
+ const raw = (await callDaemon(
210
+ {
211
+ verdict: { id: input.id, decision: input.decision, by: "paseo" },
212
+ },
213
+ input.socketPath,
214
+ )) as { recorded: boolean };
215
+ return { recorded: raw.recorded === true };
216
+ } catch (err) {
217
+ log.warn("verdict submit failed", { id: input.id, error: err });
218
+ return { recorded: false };
219
+ }
220
+ }
221
+
222
+ export const listPending = guardRpcHandler(listPendingInner, {
223
+ timeoutMs: 5000,
224
+ maxInflight: 4,
225
+ onTimeout: (info) => log.warn("list timed out", info),
226
+ onSaturated: (info) => log.warn("list saturated, shedding load", info),
227
+ });
228
+
229
+ async function listRecentInner(
230
+ input?: RpcInput<typeof recentList>,
231
+ ): Promise<RpcOutput<typeof recentList>> {
232
+ const limit = input?.limit ?? 10;
233
+ try {
234
+ const raw = (await callDaemon({ recent: { limit } }, input?.socketPath)) as DaemonRecentList;
235
+ const items = Array.isArray(raw.items) ? raw.items : [];
236
+ return {
237
+ items: items.map((item) => ({
238
+ id: item.id,
239
+ argv: Array.isArray(item.argv) ? item.argv : [],
240
+ cwd: typeof item.cwd === "string" ? item.cwd : "",
241
+ decision: typeof item.decision === "string" ? item.decision : "",
242
+ by: typeof item.by === "string" ? item.by : "",
243
+ exit: typeof item.exit === "number" ? item.exit : -1,
244
+ output: typeof item.output === "string" ? item.output : "",
245
+ step: item.step === "confirm" ? ("confirm" as const) : ("initial" as const),
246
+ confirmOf: item.confirm_of,
247
+ kind: item.kind,
248
+ link: item.link,
249
+ summary: item.summary,
250
+ acked: item.acked,
251
+ ackBy: item.ack_by,
252
+ authUrl: item.auth_url,
253
+ })),
254
+ };
255
+ } catch (err) {
256
+ log.warn("recent list failed", { error: err });
257
+ return { items: [] };
258
+ }
259
+ }
260
+
261
+ export const listRecent = guardRpcHandler(listRecentInner, {
262
+ timeoutMs: 5000,
263
+ maxInflight: 4,
264
+ onTimeout: (info) => log.warn("recent timed out", info),
265
+ onSaturated: (info) => log.warn("recent saturated, shedding load", info),
266
+ });
267
+
268
+ export const submitVerdict = guardRpcHandler(submitVerdictInner, {
269
+ timeoutMs: 5000,
270
+ maxInflight: 4,
271
+ onTimeout: (info) => log.warn("verdict timed out", info),
272
+ onSaturated: (info) => log.warn("verdict saturated, shedding load", info),
273
+ });
274
+
275
+ async function submitAckInner(
276
+ input?: RpcInput<typeof approvalAck>,
277
+ ): Promise<RpcOutput<typeof approvalAck>> {
278
+ if (input === undefined || !input.id) return { acked: false };
279
+ try {
280
+ const raw = (await callDaemon(
281
+ {
282
+ ack: { id: input.id, by: "paseo" },
283
+ },
284
+ input.socketPath,
285
+ )) as { acked: boolean };
286
+ return { acked: raw.acked === true };
287
+ } catch (err) {
288
+ log.warn("ack submit failed", { id: input.id, error: err });
289
+ return { acked: false };
290
+ }
291
+ }
292
+
293
+ export const submitAck = guardRpcHandler(submitAckInner, {
294
+ timeoutMs: 5000,
295
+ maxInflight: 4,
296
+ onTimeout: (info) => log.warn("ack timed out", info),
297
+ onSaturated: (info) => log.warn("ack saturated, shedding load", info),
298
+ });
299
+
300
+ async function statusInner(
301
+ input?: RpcInput<typeof approvalStatus>,
302
+ ): Promise<RpcOutput<typeof approvalStatus>> {
303
+ if (!input?.id) {
304
+ return { id: "", status: "not_found", exit: -1 };
305
+ }
306
+ try {
307
+ const raw = (await callDaemon(
308
+ { status: { id: input.id } },
309
+ input.socketPath,
310
+ )) as DaemonStatusResponse;
311
+ const validStatuses = [
312
+ "not_found",
313
+ "pending",
314
+ "confirming",
315
+ "running",
316
+ "completed",
317
+ "denied",
318
+ "timeout",
319
+ "client_aborted",
320
+ "confirmation_timeout",
321
+ "acked",
322
+ ] as const;
323
+ const rawStatus = raw?.status as (typeof validStatuses)[number];
324
+ const status = validStatuses.includes(rawStatus) ? rawStatus : "not_found";
325
+ return {
326
+ id: raw?.id ?? input.id,
327
+ status,
328
+ decision: raw?.decision,
329
+ by: raw?.by,
330
+ exit: typeof raw?.exit === "number" ? raw.exit : -1,
331
+ output: typeof raw?.output === "string" ? raw.output : "",
332
+ argv: Array.isArray(raw?.argv) ? raw.argv : undefined,
333
+ cwd: raw?.cwd,
334
+ expiresIn: raw?.expires_in,
335
+ step: raw?.step,
336
+ confirmOf: raw?.confirm_of,
337
+ kind: raw?.kind,
338
+ link: raw?.link,
339
+ summary: raw?.summary,
340
+ acked: raw?.acked,
341
+ ackBy: raw?.ack_by,
342
+ ackAt: raw?.ack_at,
343
+ authUrl: raw?.auth_url,
344
+ };
345
+ } catch (err) {
346
+ log.warn("status fetch failed", { id: input.id, error: err });
347
+ return { id: input.id, status: "not_found", exit: -1 };
348
+ }
349
+ }
350
+
351
+ export const getStatus = guardRpcHandler(statusInner, {
352
+ timeoutMs: 5000,
353
+ maxInflight: 4,
354
+ onTimeout: (info) => log.warn("status timed out", info),
355
+ onSaturated: (info) => log.warn("status saturated, shedding load", info),
356
+ });
357
+
358
+ interface DaemonTelegramInfoResponse {
359
+ configured?: boolean;
360
+ bot_username?: string;
361
+ chat_id?: string;
362
+ approvers?: string[];
363
+ status?: "connected" | "disconnected" | "unconfigured" | "error";
364
+ error?: string;
365
+ notification_target?: string;
366
+ }
367
+
368
+ function daemonNotificationTarget(raw: string | undefined): "telegram" | "paseo" | "both" {
369
+ return raw === "telegram" || raw === "paseo" || raw === "both" ? raw : "both";
370
+ }
371
+
372
+ async function telegramInfoInner(
373
+ input?: RpcInput<typeof approvalTelegramInfo>,
374
+ ): Promise<RpcOutput<typeof approvalTelegramInfo>> {
375
+ try {
376
+ const raw = (await callDaemon(
377
+ { telegram_info: {} },
378
+ input?.socketPath,
379
+ 8000,
380
+ )) as DaemonTelegramInfoResponse;
381
+ const validStatuses = ["connected", "disconnected", "unconfigured", "error"] as const;
382
+ const rawStatus = raw?.status as (typeof validStatuses)[number];
383
+ const status = validStatuses.includes(rawStatus)
384
+ ? rawStatus
385
+ : raw?.configured
386
+ ? "connected"
387
+ : "unconfigured";
388
+ return {
389
+ configured: Boolean(raw?.configured),
390
+ botUsername: raw?.bot_username,
391
+ chatId: raw?.chat_id,
392
+ approvers: Array.isArray(raw?.approvers) ? raw.approvers : [],
393
+ status,
394
+ error: raw?.error,
395
+ notificationTarget: daemonNotificationTarget(raw?.notification_target),
396
+ };
397
+ } catch (err) {
398
+ log.warn("telegram info fetch failed", { error: err });
399
+ return {
400
+ configured: false,
401
+ status: "unconfigured",
402
+ approvers: [],
403
+ error: err instanceof Error ? err.message : String(err),
404
+ notificationTarget: "both",
405
+ };
406
+ }
407
+ }
408
+
409
+ export const getTelegramInfo = guardRpcHandler(telegramInfoInner, {
410
+ timeoutMs: 5000,
411
+ maxInflight: 4,
412
+ onTimeout: (info) => log.warn("telegram info timed out", info),
413
+ onSaturated: (info) => log.warn("telegram info saturated, shedding load", info),
414
+ });
415
+
416
+ interface DaemonTelegramConfigResponse {
417
+ success?: boolean;
418
+ bot_username?: string;
419
+ error?: string;
420
+ }
421
+
422
+ async function telegramSetConfigInner(
423
+ input?: RpcInput<typeof approvalTelegramSetConfig>,
424
+ ): Promise<RpcOutput<typeof approvalTelegramSetConfig>> {
425
+ if (!input) return { success: false, error: "missing input" };
426
+ try {
427
+ const raw = (await callDaemon(
428
+ {
429
+ telegram_set_config: {
430
+ bot_token: input.botToken,
431
+ chat_id: input.chatId,
432
+ approvers: input.approvers,
433
+ notification_target: input.notificationTarget,
434
+ },
435
+ },
436
+ input.socketPath,
437
+ )) as DaemonTelegramConfigResponse;
438
+ return {
439
+ success: Boolean(raw?.success),
440
+ botUsername: raw?.bot_username,
441
+ error: raw?.error,
442
+ };
443
+ } catch (err) {
444
+ log.warn("telegram set config failed", { error: err });
445
+ return {
446
+ success: false,
447
+ error: err instanceof Error ? err.message : String(err),
448
+ };
449
+ }
450
+ }
451
+
452
+ export const setTelegramConfig = guardRpcHandler(telegramSetConfigInner, {
453
+ timeoutMs: 5000,
454
+ maxInflight: 4,
455
+ onTimeout: (info) => log.warn("telegram set config timed out", info),
456
+ onSaturated: (info) => log.warn("telegram set config saturated, shedding load", info),
457
+ });
458
+
459
+ interface DaemonVersionResponse {
460
+ version?: string;
461
+ git_commit?: string;
462
+ build_time?: string;
463
+ binary_sha256?: string;
464
+ pid?: number;
465
+ }
466
+
467
+ async function healthInner(
468
+ input?: RpcInput<typeof daemonHealth>,
469
+ ): Promise<RpcOutput<typeof daemonHealth>> {
470
+ try {
471
+ const raw = (await callDaemon({ version: {} }, input?.socketPath, 1500)) as DaemonVersionResponse;
472
+ return {
473
+ reachable: true,
474
+ version: typeof raw?.version === "string" ? raw.version : undefined,
475
+ pid: typeof raw?.pid === "number" ? raw.pid : undefined,
476
+ };
477
+ } catch (err) {
478
+ log.warn("health probe failed", { error: err });
479
+ return { reachable: false };
480
+ }
481
+ }
482
+
483
+ export const getHealth = guardRpcHandler(healthInner, {
484
+ timeoutMs: 5000,
485
+ maxInflight: 4,
486
+ onTimeout: (info) => log.warn("health timed out", info),
487
+ onSaturated: (info) => log.warn("health saturated, shedding load", info),
488
+ });
489
+
490
+ export class TwofadoClient {
491
+ constructor(private socketPath?: string) {}
492
+
493
+ async policyAddRule(
494
+ params: RpcInput<typeof policyAddRule>,
495
+ ): Promise<RpcOutput<typeof policyAddRule>> {
496
+ return policyAddRuleInner({ ...params, socketPath: params?.socketPath ?? this.socketPath });
497
+ }
498
+ }
499
+
500
+ export const FadoClient = TwofadoClient;
501
+
502
+ interface DaemonPolicyAddRuleResponse {
503
+ success?: boolean;
504
+ error?: string;
505
+ rules_count?: number;
506
+ }
507
+
508
+ async function policyAddRuleInner(
509
+ input?: RpcInput<typeof policyAddRule>,
510
+ ): Promise<RpcOutput<typeof policyAddRule>> {
511
+ if (!input || input.pattern.length === 0) return { success: false, error: "empty pattern" };
512
+ try {
513
+ const raw = (await callDaemon(
514
+ {
515
+ policy_add_rule: {
516
+ target: input.target,
517
+ match_type: input.match_type,
518
+ pattern: input.pattern,
519
+ },
520
+ },
521
+ input.socketPath,
522
+ )) as DaemonPolicyAddRuleResponse;
523
+ if (raw?.success === true) return { success: true, rules_count: raw.rules_count };
524
+ return { success: false, error: raw?.error ?? "daemon rejected rule" };
525
+ } catch (err) {
526
+ log.warn("policy add rule failed", { error: err });
527
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
528
+ }
529
+ }
530
+
531
+ export const addPolicyRule = guardRpcHandler(policyAddRuleInner, {
532
+ timeoutMs: 5000,
533
+ maxInflight: 4,
534
+ onTimeout: (info) => log.warn("policy add rule timed out", info),
535
+ onSaturated: (info) => log.warn("policy add rule saturated, shedding load", info),
536
+ });
@@ -0,0 +1,85 @@
1
+ import { safeExec } from "./process";
2
+
3
+ export interface AgentIdentity {
4
+ id?: string;
5
+ name?: string;
6
+ model?: string;
7
+ provider?: string;
8
+ repo?: string;
9
+ branch?: string;
10
+ envelopeText?: string;
11
+ }
12
+
13
+ export interface AgentIdentityOptions {
14
+ timeoutMs?: number;
15
+ envelopeCommand?: string;
16
+ }
17
+
18
+ const ENV_KEYS = [
19
+ "AGENT_ID",
20
+ "AGENT_NAME",
21
+ "AGENT_MODEL",
22
+ "AGENT_PROVIDER",
23
+ "PASEO_AGENT_ID",
24
+ ] as const;
25
+
26
+ function readEnvIdentity(env: NodeJS.ProcessEnv = process.env): AgentIdentity | null {
27
+ const id = env.PASEO_AGENT_ID || env.AGENT_ID || undefined;
28
+ const name = env.AGENT_NAME || undefined;
29
+ const model = env.AGENT_MODEL || undefined;
30
+ const provider = env.AGENT_PROVIDER || undefined;
31
+ if (!id && !name && !model && !provider) return null;
32
+ return { id, name, model, provider };
33
+ }
34
+
35
+ function normalizeEnvelopeJson(raw: unknown): AgentIdentity | null {
36
+ if (!raw || typeof raw !== "object") return null;
37
+ const o = raw as Record<string, unknown>;
38
+ const str = (v: unknown): string | undefined =>
39
+ typeof v === "string" && v.length > 0 ? v : undefined;
40
+ const identity: AgentIdentity = {
41
+ id: str(o.id) ?? str(o.agentId),
42
+ name: str(o.name) ?? str(o.agentName),
43
+ model: str(o.model),
44
+ provider: str(o.provider),
45
+ repo: str(o.repo) ?? str(o.workspace),
46
+ branch: str(o.branch),
47
+ envelopeText: str(o.envelopeText) ?? str(o.envelope),
48
+ };
49
+ if (!identity.id && !identity.name && !identity.model && !identity.provider && !identity.repo && !identity.branch) return null;
50
+ return identity;
51
+ }
52
+
53
+ export async function getAgentIdentity(
54
+ options: AgentIdentityOptions = {},
55
+ ): Promise<AgentIdentity | null> {
56
+ const timeoutMs = options.timeoutMs ?? 5000;
57
+ const command = options.envelopeCommand ?? "xpufx-tool envelope --format json";
58
+ const envIdentity = readEnvIdentity();
59
+ void ENV_KEYS;
60
+ try {
61
+ const result = await safeExec(command, { timeoutMs });
62
+ if (result.code !== 0) return envIdentity;
63
+ const text = result.stdout.trim();
64
+ if (!text) return envIdentity;
65
+ try {
66
+ const parsed: unknown = JSON.parse(text);
67
+ const cliIdentity = normalizeEnvelopeJson(parsed);
68
+ if (!cliIdentity) return envIdentity;
69
+ if (!envIdentity) return cliIdentity;
70
+ return {
71
+ id: envIdentity.id ?? cliIdentity.id,
72
+ name: envIdentity.name ?? cliIdentity.name,
73
+ model: envIdentity.model ?? cliIdentity.model,
74
+ provider: envIdentity.provider ?? cliIdentity.provider,
75
+ repo: cliIdentity.repo,
76
+ branch: cliIdentity.branch,
77
+ envelopeText: cliIdentity.envelopeText,
78
+ };
79
+ } catch {
80
+ return envIdentity;
81
+ }
82
+ } catch {
83
+ return envIdentity;
84
+ }
85
+ }