@ouro.bot/cli 0.1.0-alpha.804 → 0.1.0-alpha.805
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/changelog.json +8 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +14 -11
- package/dist/heart/daemon/cli-defaults.js +22 -4
- package/dist/heart/daemon/cli-exec.js +95 -6
- package/dist/heart/daemon/cli-help.js +6 -0
- package/dist/heart/daemon/cli-parse.js +52 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +8 -0
- package/dist/heart/daemon/daemon-entry.js +2 -1
- package/dist/heart/daemon/daemon.js +94 -47
- package/dist/heart/daemon/sense-manager.js +1 -1
- package/dist/heart/frontend-approval-runtime.js +432 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +154 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/mail-import-discovery.js +3 -0
- package/dist/heart/turn-context.js +1 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/mind/prompt.js +4 -1
- package/dist/repertoire/mcp-manager.js +82 -17
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/pipeline.js +5 -0
- package/dist/senses/shared-turn.js +369 -257
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.settleFrontendApproval = settleFrontendApproval;
|
|
37
|
+
exports.createFrontendApprovalRuntime = createFrontendApprovalRuntime;
|
|
38
|
+
const node_crypto_1 = require("node:crypto");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const approval_files_1 = require("./approval-files");
|
|
41
|
+
const approval_store_1 = require("./approval-store");
|
|
42
|
+
const tool_approval_1 = require("./tool-approval");
|
|
43
|
+
const core_1 = require("./core");
|
|
44
|
+
const identity_1 = require("./identity");
|
|
45
|
+
const turn_execution_lease_1 = require("./turn-execution-lease");
|
|
46
|
+
const context_1 = require("../mind/context");
|
|
47
|
+
const session_transaction_1 = require("../mind/session-transaction");
|
|
48
|
+
const tools_1 = require("../repertoire/tools");
|
|
49
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
50
|
+
const runtime_1 = require("../nerves/runtime");
|
|
51
|
+
const PERMISSION_OPTIONS = [
|
|
52
|
+
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
|
|
53
|
+
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
|
|
54
|
+
];
|
|
55
|
+
/* v8 ignore start -- lazy default adapter avoids widening every daemon test mock; settlement behavior is covered through the injected dependency @preserve */
|
|
56
|
+
async function defaultReleaseRuntimeMcpServers() {
|
|
57
|
+
const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
|
|
58
|
+
await manager.releaseRuntimeMcpServers();
|
|
59
|
+
}
|
|
60
|
+
/* v8 ignore stop */
|
|
61
|
+
const DEFAULT_SETTLEMENT_DEPENDENCIES = {
|
|
62
|
+
withTurnExecutionLease: turn_execution_lease_1.withTurnExecutionLease,
|
|
63
|
+
setAgentName: identity_1.setAgentName,
|
|
64
|
+
withSessionTurnLease: session_transaction_1.withSessionTurnLease,
|
|
65
|
+
readSessionTransaction: session_transaction_1.readSessionTransaction,
|
|
66
|
+
executeApprovalDecision: tool_approval_1.executeApprovalDecision,
|
|
67
|
+
resolveToolDefinition: tools_1.resolveToolDefinition,
|
|
68
|
+
approvalPolicyForInvocation: tools_1.approvalPolicyForInvocation,
|
|
69
|
+
execTool: tools_1.execTool,
|
|
70
|
+
getSharedMcpManager: mcp_manager_1.getSharedMcpManager,
|
|
71
|
+
releaseRuntimeMcpServers: defaultReleaseRuntimeMcpServers,
|
|
72
|
+
loadSession: context_1.loadSession,
|
|
73
|
+
postTurnTrim: context_1.postTurnTrim,
|
|
74
|
+
postTurnPersist: context_1.postTurnPersist,
|
|
75
|
+
resumeApprovalContinuation: core_1.resumeApprovalContinuation,
|
|
76
|
+
runAgent: core_1.runAgent,
|
|
77
|
+
randomUUID: node_crypto_1.randomUUID,
|
|
78
|
+
};
|
|
79
|
+
async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_DEPENDENCIES) {
|
|
80
|
+
const token = input.tokenStore.get(input.suspension.approvalId);
|
|
81
|
+
if (!token)
|
|
82
|
+
throw new Error(`frontend approval decision token is unavailable: ${input.suspension.approvalId}`);
|
|
83
|
+
const checkpoint = input.checkpointStore.read(input.suspension.approvalId);
|
|
84
|
+
if (!checkpoint)
|
|
85
|
+
throw new Error(`frontend approval checkpoint is unavailable: ${input.suspension.approvalId}`);
|
|
86
|
+
const record = input.approvalStore.read(input.suspension.approvalId);
|
|
87
|
+
if (!record)
|
|
88
|
+
throw new Error(`frontend approval record is unavailable: ${input.suspension.approvalId}`);
|
|
89
|
+
return dependencies.withTurnExecutionLease(async () => {
|
|
90
|
+
dependencies.setAgentName(input.request.agent);
|
|
91
|
+
try {
|
|
92
|
+
const mcpManager = await dependencies.getSharedMcpManager(input.request.runtimeMcpServers ? { runtimeServers: input.request.runtimeMcpServers } : undefined) ?? undefined;
|
|
93
|
+
return await dependencies.withSessionTurnLease(record.sessionPath, async (lease) => {
|
|
94
|
+
const currentRevision = dependencies.readSessionTransaction(record.sessionPath, lease).revision;
|
|
95
|
+
const continuationSignal = input.optionId === "cancelled" ? AbortSignal.abort() : input.signal;
|
|
96
|
+
const liveToolContext = input.approvalRequest.liveToolContext ?? {
|
|
97
|
+
signin: async () => undefined,
|
|
98
|
+
agentRoot: input.agentRoot,
|
|
99
|
+
currentSession: {
|
|
100
|
+
friendId: input.request.friendId,
|
|
101
|
+
channel: input.request.channel,
|
|
102
|
+
key: input.request.sessionKey,
|
|
103
|
+
sessionPath: record.sessionPath,
|
|
104
|
+
},
|
|
105
|
+
currentUserMessage: input.request.message,
|
|
106
|
+
};
|
|
107
|
+
const terminal = await dependencies.executeApprovalDecision({
|
|
108
|
+
approvalStore: input.approvalStore,
|
|
109
|
+
checkpointStore: input.checkpointStore,
|
|
110
|
+
decision: {
|
|
111
|
+
approvalId: record.approvalId,
|
|
112
|
+
decisionToken: token,
|
|
113
|
+
decision: input.optionId === "allow-once" ? "approve" : "deny",
|
|
114
|
+
requesterId: input.request.friendId,
|
|
115
|
+
transport: "frontend",
|
|
116
|
+
transportUserId: input.request.friendId,
|
|
117
|
+
transportChatId: input.request.sessionKey,
|
|
118
|
+
transportMessageId: record.approvalId,
|
|
119
|
+
sessionKey: input.request.sessionKey,
|
|
120
|
+
...(input.optionId === "expired" ? { decisionAt: Date.parse(record.expiresAt) } : {}),
|
|
121
|
+
},
|
|
122
|
+
ownerId: `frontend-decision-${dependencies.randomUUID()}`,
|
|
123
|
+
currentSessionRevision: currentRevision,
|
|
124
|
+
resolveTool: dependencies.resolveToolDefinition,
|
|
125
|
+
resolveApprovalPolicy: (name, args) => dependencies.approvalPolicyForInvocation(name, args, liveToolContext),
|
|
126
|
+
liveGuard: async () => ({ ok: true }),
|
|
127
|
+
liveRisk: async () => ({ ok: true }),
|
|
128
|
+
execute: (name, args) => dependencies.execTool(name, args, liveToolContext),
|
|
129
|
+
});
|
|
130
|
+
input.tokenStore.remove(record.approvalId);
|
|
131
|
+
const existing = dependencies.loadSession(record.sessionPath);
|
|
132
|
+
const existingStructuredOutputIds = new Set(existing?.structuredOutputs.map((output) => output.id) ?? []);
|
|
133
|
+
const deliveries = [];
|
|
134
|
+
let response = "";
|
|
135
|
+
const emit = input.frontendEventSink.onEvent;
|
|
136
|
+
const callbacks = {
|
|
137
|
+
settleOutputMode: "retractable_buffer",
|
|
138
|
+
onModelStart: () => emit({ type: "model_started", data: {} }),
|
|
139
|
+
onModelStreamStart: () => emit({ type: "model_stream_started", data: {} }),
|
|
140
|
+
onTextChunk: (text) => {
|
|
141
|
+
response += text;
|
|
142
|
+
emit({ type: "text_delta", data: { text } });
|
|
143
|
+
},
|
|
144
|
+
onReasoningChunk: (text) => emit({ type: "reasoning_delta", data: { text } }),
|
|
145
|
+
onToolStart: (name, args) => emit({ type: "tool_started", data: { name, args } }),
|
|
146
|
+
onToolEnd: (name, summary, success) => emit({ type: "tool_completed", data: { name, summary, success } }),
|
|
147
|
+
onError: (error, severity) => emit({ type: "error", data: { message: error.message, severity } }),
|
|
148
|
+
onClearText: () => {
|
|
149
|
+
response = "";
|
|
150
|
+
emit({ type: "text_cleared", data: {} });
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
let continuationEpoch = 0;
|
|
154
|
+
const continuationOwnerId = `frontend-continuation-${dependencies.randomUUID()}`;
|
|
155
|
+
const nestedCoordinator = {
|
|
156
|
+
propose: (request) => input.approvalCoordinatorFactory({
|
|
157
|
+
request: input.request,
|
|
158
|
+
publish: input.publish,
|
|
159
|
+
})({
|
|
160
|
+
sessionPath: record.sessionPath,
|
|
161
|
+
baseSessionRevision: dependencies.readSessionTransaction(record.sessionPath, lease).revision,
|
|
162
|
+
}).propose(request),
|
|
163
|
+
};
|
|
164
|
+
const persist = (messages, result) => {
|
|
165
|
+
const expectedRevision = dependencies.readSessionTransaction(record.sessionPath, lease).revision;
|
|
166
|
+
const prepared = dependencies.postTurnTrim(messages, result?.usage);
|
|
167
|
+
dependencies.postTurnPersist(record.sessionPath, prepared, result?.usage, existing?.state, lease, expectedRevision);
|
|
168
|
+
};
|
|
169
|
+
const continuation = await dependencies.resumeApprovalContinuation({
|
|
170
|
+
record: terminal,
|
|
171
|
+
checkpoint,
|
|
172
|
+
currentSessionRevision: dependencies.readSessionTransaction(record.sessionPath, lease).revision,
|
|
173
|
+
sessionMessages: checkpoint.preCallMessages,
|
|
174
|
+
callbacks,
|
|
175
|
+
channel: input.request.channel,
|
|
176
|
+
claimContinuation: () => {
|
|
177
|
+
const claim = input.approvalStore.claimContinuation({
|
|
178
|
+
approvalId: record.approvalId,
|
|
179
|
+
ownerId: continuationOwnerId,
|
|
180
|
+
});
|
|
181
|
+
continuationEpoch = claim.record.continuationEpoch;
|
|
182
|
+
return claim;
|
|
183
|
+
},
|
|
184
|
+
markContinuationMaterialized: () => {
|
|
185
|
+
input.approvalStore.markContinuationMaterialized({
|
|
186
|
+
approvalId: record.approvalId,
|
|
187
|
+
ownerId: continuationOwnerId,
|
|
188
|
+
epoch: continuationEpoch,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
markContinuationAttempted: () => {
|
|
192
|
+
input.approvalStore.markContinuationAttempted({
|
|
193
|
+
approvalId: record.approvalId,
|
|
194
|
+
ownerId: continuationOwnerId,
|
|
195
|
+
epoch: continuationEpoch,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
completeContinuation: () => {
|
|
199
|
+
input.approvalStore.completeContinuation({
|
|
200
|
+
approvalId: record.approvalId,
|
|
201
|
+
ownerId: continuationOwnerId,
|
|
202
|
+
epoch: continuationEpoch,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
runAgent: dependencies.runAgent,
|
|
206
|
+
runAgentOptions: {
|
|
207
|
+
mcpManager,
|
|
208
|
+
toolContext: liveToolContext,
|
|
209
|
+
approvalCoordinator: nestedCoordinator,
|
|
210
|
+
},
|
|
211
|
+
persist,
|
|
212
|
+
deliver: async (text) => {
|
|
213
|
+
response = text;
|
|
214
|
+
deliveries.push({ kind: "text", text });
|
|
215
|
+
emit({ type: "assistant_delivery", data: { kind: "text", text } });
|
|
216
|
+
},
|
|
217
|
+
signal: continuationSignal,
|
|
218
|
+
});
|
|
219
|
+
const persisted = dependencies.loadSession(record.sessionPath);
|
|
220
|
+
for (const output of persisted?.structuredOutputs ?? []) {
|
|
221
|
+
if (!existingStructuredOutputIds.has(output.id)) {
|
|
222
|
+
emit({ type: "structured_output", data: { output } });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const turnOutcome = continuation.outcome === "terminal_notice"
|
|
226
|
+
? "settled"
|
|
227
|
+
: continuation.outcome === "already_continued"
|
|
228
|
+
? "superseded"
|
|
229
|
+
: continuation.outcome;
|
|
230
|
+
if (turnOutcome === "suspended" && !continuation.suspension) {
|
|
231
|
+
throw new Error("frontend approval continuation omitted its nested suspension");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
response,
|
|
235
|
+
ponderDeferred: false,
|
|
236
|
+
deliveries,
|
|
237
|
+
deliveryFailures: [],
|
|
238
|
+
sessionPath: record.sessionPath,
|
|
239
|
+
turnOutcome,
|
|
240
|
+
...(continuation.suspension ? { suspension: continuation.suspension } : {}),
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
if (input.request.runtimeMcpServers) {
|
|
246
|
+
await dependencies.releaseRuntimeMcpServers();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function createFrontendApprovalRuntime(options) {
|
|
252
|
+
const agentRoot = options.agentRoot ?? identity_1.getAgentRoot;
|
|
253
|
+
const now = options.now ?? Date.now;
|
|
254
|
+
const approvalTimeoutMs = options.approvalTimeoutMs ?? 300_000;
|
|
255
|
+
const settleApproval = options.settleApproval ?? settleFrontendApproval;
|
|
256
|
+
if (!Number.isSafeInteger(approvalTimeoutMs) || approvalTimeoutMs < 1) {
|
|
257
|
+
throw new Error("approvalTimeoutMs must be a positive integer");
|
|
258
|
+
}
|
|
259
|
+
(0, runtime_1.emitNervesEvent)({
|
|
260
|
+
component: "heart",
|
|
261
|
+
event: "heart.frontend_approval_runtime_ready",
|
|
262
|
+
message: "frontend approval runtime ready",
|
|
263
|
+
});
|
|
264
|
+
const states = new Map();
|
|
265
|
+
const pending = new Map();
|
|
266
|
+
let closed = false;
|
|
267
|
+
function closeStatesWhenIdle() {
|
|
268
|
+
if (!closed || pending.size > 0)
|
|
269
|
+
return;
|
|
270
|
+
for (const state of states.values())
|
|
271
|
+
state.approvalStore.close();
|
|
272
|
+
states.clear();
|
|
273
|
+
}
|
|
274
|
+
function stateFor(agent) {
|
|
275
|
+
let state = states.get(agent);
|
|
276
|
+
if (state)
|
|
277
|
+
return state;
|
|
278
|
+
const stateRoot = path.join(agentRoot(agent), "state", "approvals");
|
|
279
|
+
state = {
|
|
280
|
+
approvalStore: (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite") }),
|
|
281
|
+
checkpointStore: new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json")),
|
|
282
|
+
tokenStore: new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json")),
|
|
283
|
+
};
|
|
284
|
+
states.set(agent, state);
|
|
285
|
+
return state;
|
|
286
|
+
}
|
|
287
|
+
function settlePending(item, optionId) {
|
|
288
|
+
if (item.settled)
|
|
289
|
+
return false;
|
|
290
|
+
item.settled = true;
|
|
291
|
+
item.resolve(optionId);
|
|
292
|
+
item.publish("permission_resolved", { requestId: item.requestId, optionId }, "permission_resolved");
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
const runtime = {
|
|
296
|
+
approvalCoordinatorFactory(input) {
|
|
297
|
+
return (context) => ({
|
|
298
|
+
propose: async (request) => {
|
|
299
|
+
if (closed)
|
|
300
|
+
throw new Error("frontend approval runtime is closed");
|
|
301
|
+
if (request.toolCall.type !== "function")
|
|
302
|
+
throw new Error("frontend approval requires a function tool call");
|
|
303
|
+
const state = stateFor(input.request.agent);
|
|
304
|
+
const committed = (0, tool_approval_1.commitApprovalProposal)({
|
|
305
|
+
approvalStore: state.approvalStore,
|
|
306
|
+
checkpointStore: state.checkpointStore,
|
|
307
|
+
tokenStore: state.tokenStore,
|
|
308
|
+
proposal: {
|
|
309
|
+
toolCallId: request.toolCall.id,
|
|
310
|
+
toolName: request.toolCall.function.name,
|
|
311
|
+
arguments: request.arguments,
|
|
312
|
+
schemaDigest: request.schemaDigest,
|
|
313
|
+
toolDigest: request.toolDigest,
|
|
314
|
+
policyDigest: request.policyDigest,
|
|
315
|
+
policyId: request.policyId,
|
|
316
|
+
sessionKey: input.request.sessionKey,
|
|
317
|
+
sessionPath: context.sessionPath,
|
|
318
|
+
baseSessionRevision: context.baseSessionRevision,
|
|
319
|
+
checkpointDigest: "0".repeat(64),
|
|
320
|
+
requesterId: input.request.friendId,
|
|
321
|
+
transport: "frontend",
|
|
322
|
+
transportUserId: input.request.friendId,
|
|
323
|
+
transportChatId: input.request.sessionKey,
|
|
324
|
+
expiresAt: new Date(now() + approvalTimeoutMs).toISOString(),
|
|
325
|
+
frozenAssistantMessage: request.frozenAssistantMessage,
|
|
326
|
+
},
|
|
327
|
+
preCallMessages: request.preCallMessages,
|
|
328
|
+
});
|
|
329
|
+
const decision = Promise.withResolvers();
|
|
330
|
+
state.approvalStore.bindPrompt({
|
|
331
|
+
approvalId: committed.record.approvalId,
|
|
332
|
+
transport: "frontend",
|
|
333
|
+
transportChatId: input.request.sessionKey,
|
|
334
|
+
transportMessageId: committed.record.approvalId,
|
|
335
|
+
});
|
|
336
|
+
const item = {
|
|
337
|
+
requestId: committed.record.approvalId,
|
|
338
|
+
toolCallId: request.toolCall.id,
|
|
339
|
+
title: `Approve ${request.toolCall.function.name}`,
|
|
340
|
+
request: input.request,
|
|
341
|
+
approvalRequest: request,
|
|
342
|
+
expiresAt: Date.parse(committed.record.expiresAt),
|
|
343
|
+
publish: input.publish,
|
|
344
|
+
decision: decision.promise,
|
|
345
|
+
resolve: decision.resolve,
|
|
346
|
+
settled: false,
|
|
347
|
+
};
|
|
348
|
+
pending.set(committed.record.approvalId, item);
|
|
349
|
+
input.publish("permission_requested", {
|
|
350
|
+
requestId: committed.record.approvalId,
|
|
351
|
+
toolCallId: request.toolCall.id,
|
|
352
|
+
title: `Approve ${request.toolCall.function.name}`,
|
|
353
|
+
options: PERMISSION_OPTIONS,
|
|
354
|
+
}, "permission_requested");
|
|
355
|
+
return {
|
|
356
|
+
approvalId: committed.record.approvalId,
|
|
357
|
+
checkpointDigest: committed.record.checkpointDigest,
|
|
358
|
+
suspendedSessionRevision: committed.record.suspendedSessionRevision,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
async resumeApproval(input) {
|
|
364
|
+
const item = pending.get(input.suspension.approvalId);
|
|
365
|
+
if (!item || item.request.turnId !== input.request.turnId) {
|
|
366
|
+
throw new Error(`frontend approval ${input.suspension.approvalId} does not own turn ${input.request.turnId}`);
|
|
367
|
+
}
|
|
368
|
+
const abort = () => settlePending(item, "cancelled");
|
|
369
|
+
if (input.signal.aborted)
|
|
370
|
+
abort();
|
|
371
|
+
else
|
|
372
|
+
input.signal.addEventListener("abort", abort, { once: true });
|
|
373
|
+
const timeout = setTimeout(() => settlePending(item, "expired"), Math.max(0, item.expiresAt - now()));
|
|
374
|
+
try {
|
|
375
|
+
const optionId = await item.decision;
|
|
376
|
+
const state = stateFor(input.request.agent);
|
|
377
|
+
return await settleApproval({
|
|
378
|
+
...input,
|
|
379
|
+
approvalRequest: item.approvalRequest,
|
|
380
|
+
optionId,
|
|
381
|
+
publish: item.publish,
|
|
382
|
+
agentRoot: agentRoot(input.request.agent),
|
|
383
|
+
...state,
|
|
384
|
+
approvalCoordinatorFactory: runtime.approvalCoordinatorFactory,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
clearTimeout(timeout);
|
|
389
|
+
input.signal.removeEventListener("abort", abort);
|
|
390
|
+
pending.delete(input.suspension.approvalId);
|
|
391
|
+
closeStatesWhenIdle();
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
resolvePermission(requestId, optionId) {
|
|
395
|
+
if (closed || (optionId !== "allow-once" && optionId !== "reject-once" && optionId !== "cancelled"))
|
|
396
|
+
return false;
|
|
397
|
+
const item = pending.get(requestId);
|
|
398
|
+
return item ? settlePending(item, optionId) : false;
|
|
399
|
+
},
|
|
400
|
+
cancelTurn(turnId) {
|
|
401
|
+
for (const item of pending.values()) {
|
|
402
|
+
if (item.request.turnId === turnId)
|
|
403
|
+
settlePending(item, "cancelled");
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
pendingPermissions(ref) {
|
|
407
|
+
return [...pending.values()].flatMap((item) => {
|
|
408
|
+
if (item.settled
|
|
409
|
+
|| item.request.agent !== ref.agent
|
|
410
|
+
|| item.request.friendId !== ref.friendId
|
|
411
|
+
|| item.request.sessionKey !== ref.sessionKey)
|
|
412
|
+
return [];
|
|
413
|
+
return [{
|
|
414
|
+
requestId: item.requestId,
|
|
415
|
+
turnId: item.request.turnId,
|
|
416
|
+
toolCallId: item.toolCallId,
|
|
417
|
+
title: item.title,
|
|
418
|
+
options: [...PERMISSION_OPTIONS],
|
|
419
|
+
}];
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
close() {
|
|
423
|
+
if (closed)
|
|
424
|
+
return;
|
|
425
|
+
for (const item of pending.values())
|
|
426
|
+
settlePending(item, "cancelled");
|
|
427
|
+
closed = true;
|
|
428
|
+
closeStatesWhenIdle();
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
return runtime;
|
|
432
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FrontendJournalStore = exports.FrontendJournalPayloadTooLargeError = exports.FrontendJournalCorruptError = void 0;
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const identity_1 = require("./identity");
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
const JOURNAL_VERSION = 1;
|
|
43
|
+
const DEFAULT_MAX_EVENT_BYTES = 256 * 1024;
|
|
44
|
+
const DEFAULT_REPLAY_LIMIT = 1_000;
|
|
45
|
+
const EVENT_TYPES = new Set([
|
|
46
|
+
"user_message",
|
|
47
|
+
"turn_started",
|
|
48
|
+
"assistant_delivery",
|
|
49
|
+
"tool_started",
|
|
50
|
+
"tool_completed",
|
|
51
|
+
"structured_output",
|
|
52
|
+
"permission_requested",
|
|
53
|
+
"permission_resolved",
|
|
54
|
+
"error",
|
|
55
|
+
"turn_completed",
|
|
56
|
+
"turn_failed",
|
|
57
|
+
"turn_cancelled",
|
|
58
|
+
]);
|
|
59
|
+
class FrontendJournalCorruptError extends Error {
|
|
60
|
+
constructor(journalPath) {
|
|
61
|
+
super(`frontend journal has an invalid tail: ${journalPath}`);
|
|
62
|
+
this.name = "FrontendJournalCorruptError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.FrontendJournalCorruptError = FrontendJournalCorruptError;
|
|
66
|
+
class FrontendJournalPayloadTooLargeError extends Error {
|
|
67
|
+
constructor(size, limit) {
|
|
68
|
+
super(`frontend journal event is ${size} bytes; limit is ${limit}`);
|
|
69
|
+
this.name = "FrontendJournalPayloadTooLargeError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.FrontendJournalPayloadTooLargeError = FrontendJournalPayloadTooLargeError;
|
|
73
|
+
function required(value, field) {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
77
|
+
return trimmed;
|
|
78
|
+
}
|
|
79
|
+
function opaqueSegment(value) {
|
|
80
|
+
return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex").slice(0, 32);
|
|
81
|
+
}
|
|
82
|
+
function record(value) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
84
|
+
throw new Error("journal data must be an object");
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function parseEvent(raw, ref, expectedSequence) {
|
|
88
|
+
const value = record(raw);
|
|
89
|
+
if (value.version !== JOURNAL_VERSION)
|
|
90
|
+
throw new Error("unsupported journal version");
|
|
91
|
+
if (value.sequence !== expectedSequence)
|
|
92
|
+
throw new Error("invalid journal sequence");
|
|
93
|
+
if (value.agent !== ref.agent || value.friendId !== ref.friendId || value.sessionId !== ref.sessionId) {
|
|
94
|
+
throw new Error("journal identity mismatch");
|
|
95
|
+
}
|
|
96
|
+
if (typeof value.turnId !== "string" || !value.turnId)
|
|
97
|
+
throw new Error("invalid journal turnId");
|
|
98
|
+
if (typeof value.type !== "string" || !EVENT_TYPES.has(value.type)) {
|
|
99
|
+
throw new Error("invalid journal event type");
|
|
100
|
+
}
|
|
101
|
+
if (typeof value.occurredAt !== "string" || !value.occurredAt)
|
|
102
|
+
throw new Error("invalid journal timestamp");
|
|
103
|
+
return {
|
|
104
|
+
version: JOURNAL_VERSION,
|
|
105
|
+
sequence: expectedSequence,
|
|
106
|
+
agent: ref.agent,
|
|
107
|
+
friendId: ref.friendId,
|
|
108
|
+
sessionId: ref.sessionId,
|
|
109
|
+
turnId: value.turnId,
|
|
110
|
+
type: value.type,
|
|
111
|
+
occurredAt: value.occurredAt,
|
|
112
|
+
data: record(value.data),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
class FrontendJournalStore {
|
|
116
|
+
agentRoot;
|
|
117
|
+
now;
|
|
118
|
+
maxEventBytes;
|
|
119
|
+
constructor(options = {}) {
|
|
120
|
+
this.agentRoot = options.agentRoot ?? identity_1.getAgentRoot;
|
|
121
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
122
|
+
this.maxEventBytes = options.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES;
|
|
123
|
+
if (!Number.isSafeInteger(this.maxEventBytes) || this.maxEventBytes < 1) {
|
|
124
|
+
throw new Error("maxEventBytes must be a positive integer");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
pathFor(ref) {
|
|
128
|
+
const agent = required(ref.agent, "agent");
|
|
129
|
+
const friendId = required(ref.friendId, "friendId");
|
|
130
|
+
const sessionId = required(ref.sessionId, "sessionId");
|
|
131
|
+
return path.join(this.agentRoot(agent), "state", "frontend-sessions", opaqueSegment(friendId), `${opaqueSegment(sessionId)}.jsonl`);
|
|
132
|
+
}
|
|
133
|
+
replay(ref, options = {}) {
|
|
134
|
+
const afterSequence = options.afterSequence ?? 0;
|
|
135
|
+
const limit = options.limit ?? DEFAULT_REPLAY_LIMIT;
|
|
136
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
137
|
+
throw new Error("afterSequence must be a non-negative integer");
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
140
|
+
throw new Error("limit must be a positive integer");
|
|
141
|
+
}
|
|
142
|
+
const journalPath = this.pathFor(ref);
|
|
143
|
+
if (!fs.existsSync(journalPath)) {
|
|
144
|
+
return { events: [], lastSequence: 0, hasMore: false, degraded: false, incompleteTurnIds: [] };
|
|
145
|
+
}
|
|
146
|
+
const valid = [];
|
|
147
|
+
let degraded = false;
|
|
148
|
+
const lines = fs.readFileSync(journalPath, "utf8").split("\n").filter((line) => line.length > 0);
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
try {
|
|
151
|
+
valid.push(parseEvent(JSON.parse(line), ref, valid.length + 1));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
degraded = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const matching = valid.filter((event) => event.sequence > afterSequence);
|
|
159
|
+
const incompleteTurnIds = new Set();
|
|
160
|
+
for (const event of valid) {
|
|
161
|
+
if (event.type === "turn_started") {
|
|
162
|
+
incompleteTurnIds.add(event.turnId);
|
|
163
|
+
}
|
|
164
|
+
else if (event.type === "turn_completed"
|
|
165
|
+
|| event.type === "turn_failed"
|
|
166
|
+
|| event.type === "turn_cancelled") {
|
|
167
|
+
incompleteTurnIds.delete(event.turnId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
events: matching.slice(0, limit),
|
|
172
|
+
lastSequence: valid.at(-1)?.sequence ?? 0,
|
|
173
|
+
hasMore: matching.length > limit,
|
|
174
|
+
degraded,
|
|
175
|
+
incompleteTurnIds: [...incompleteTurnIds],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
append(ref, input) {
|
|
179
|
+
const journalPath = this.pathFor(ref);
|
|
180
|
+
const replay = this.replay(ref);
|
|
181
|
+
if (replay.degraded)
|
|
182
|
+
throw new FrontendJournalCorruptError(journalPath);
|
|
183
|
+
if (!EVENT_TYPES.has(input.type))
|
|
184
|
+
throw new Error("invalid journal event type");
|
|
185
|
+
const event = {
|
|
186
|
+
version: JOURNAL_VERSION,
|
|
187
|
+
sequence: replay.lastSequence + 1,
|
|
188
|
+
agent: required(ref.agent, "agent"),
|
|
189
|
+
friendId: required(ref.friendId, "friendId"),
|
|
190
|
+
sessionId: required(ref.sessionId, "sessionId"),
|
|
191
|
+
turnId: required(input.turnId, "turnId"),
|
|
192
|
+
type: input.type,
|
|
193
|
+
occurredAt: this.now(),
|
|
194
|
+
data: record(input.data),
|
|
195
|
+
};
|
|
196
|
+
const encoded = `${JSON.stringify(event)}\n`;
|
|
197
|
+
const size = Buffer.byteLength(encoded);
|
|
198
|
+
if (size > this.maxEventBytes)
|
|
199
|
+
throw new FrontendJournalPayloadTooLargeError(size, this.maxEventBytes);
|
|
200
|
+
// ponytail: semantic event volume is low; add a side index only if replay-on-append becomes measurable.
|
|
201
|
+
const directory = path.dirname(journalPath);
|
|
202
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
203
|
+
fs.chmodSync(directory, 0o700);
|
|
204
|
+
fs.appendFileSync(journalPath, encoded, { encoding: "utf8", mode: 0o600 });
|
|
205
|
+
fs.chmodSync(journalPath, 0o600);
|
|
206
|
+
(0, runtime_1.emitNervesEvent)({
|
|
207
|
+
component: "heart",
|
|
208
|
+
event: "heart.frontend_journal_appended",
|
|
209
|
+
message: "frontend journal event appended",
|
|
210
|
+
meta: { type: event.type, sequence: event.sequence },
|
|
211
|
+
});
|
|
212
|
+
return event;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.FrontendJournalStore = FrontendJournalStore;
|