@capekai/core 1.0.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/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 permission runtime: NON-REPLACEABLE lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Owns the pending-ask and waiter registries, request-id routing, timeout
|
|
5
|
+
* and cleanup, response validation against the module-level validators,
|
|
6
|
+
* raw-audit denial, waiter resolution, canonical grant construction and
|
|
7
|
+
* persistence (module-level `buildGrantParams` with the dangerous-shell
|
|
8
|
+
* once downgrade), and the persistence calls through the interaction host.
|
|
9
|
+
*
|
|
10
|
+
* The replaceable `AskPermissionPolicyService` supplies advice/config only:
|
|
11
|
+
* its validators and grant methods are NEVER consulted for mandatory
|
|
12
|
+
* decisions. A full from-scratch replacement therefore cannot approve
|
|
13
|
+
* malformed or unknown responses on the live resolveAsk path and cannot
|
|
14
|
+
* create grants outside the canonical allowed scopes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import type { Ask } from '@capekai/tool';
|
|
20
|
+
import type { AskAuthority } from '@capekai/types';
|
|
21
|
+
import type { AskApi } from '@capekai/tool';
|
|
22
|
+
import { getRuntimeHost } from '../runtime/host';
|
|
23
|
+
import type { PendingAskRecord } from '../runtime/host';
|
|
24
|
+
import type {
|
|
25
|
+
AskBroadcastFn,
|
|
26
|
+
AskPermissionPolicyService,
|
|
27
|
+
PermissionBroadcastFn,
|
|
28
|
+
PermissionRuntimeService,
|
|
29
|
+
RequestPermissionParams,
|
|
30
|
+
} from './contracts';
|
|
31
|
+
import {
|
|
32
|
+
buildGrantParams,
|
|
33
|
+
buildPermissionKey,
|
|
34
|
+
getActiveAskPermissionPolicy,
|
|
35
|
+
getAskPermissionPolicy,
|
|
36
|
+
isPermissionApproved,
|
|
37
|
+
isValidPermissionResponse,
|
|
38
|
+
resolveAskAuthority,
|
|
39
|
+
extractResolutionValue,
|
|
40
|
+
} from './policy';
|
|
41
|
+
|
|
42
|
+
export interface PermissionRuntimeCreateOptions {
|
|
43
|
+
id?: string;
|
|
44
|
+
/** The advice provider bound at composition. While a composed scope is
|
|
45
|
+
* entered, the actively seeded provider takes precedence, so unscoped
|
|
46
|
+
* `withAskPermissionPolicy` swaps affect the process runtime too. */
|
|
47
|
+
provider: AskPermissionPolicyService;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function interaction() {
|
|
51
|
+
return getRuntimeHost().interaction;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PendingAsk {
|
|
55
|
+
resolve: (response: unknown) => void;
|
|
56
|
+
reject: (error: Error) => void;
|
|
57
|
+
ask: Ask;
|
|
58
|
+
sessionId: string;
|
|
59
|
+
toolCallId: string;
|
|
60
|
+
toolName: string;
|
|
61
|
+
broadcastFn: AskBroadcastFn;
|
|
62
|
+
authority: AskAuthority;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface PermissionWaiter {
|
|
66
|
+
resolve: (value: unknown) => void;
|
|
67
|
+
reject: (error: Error) => void;
|
|
68
|
+
createdAt: number;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
toolCallId: string;
|
|
71
|
+
broadcastFn: PermissionBroadcastFn;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createPermissionRuntimeService(
|
|
75
|
+
createOptions: PermissionRuntimeCreateOptions,
|
|
76
|
+
): PermissionRuntimeService {
|
|
77
|
+
const id = createOptions.id ?? 'permission.runtime';
|
|
78
|
+
const boundProvider = createOptions.provider;
|
|
79
|
+
|
|
80
|
+
const pendingAsks = new Map<string, PendingAsk>();
|
|
81
|
+
const askTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
82
|
+
const waiters = new Map<string, PermissionWaiter>();
|
|
83
|
+
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
84
|
+
|
|
85
|
+
function activeProvider(): AskPermissionPolicyService {
|
|
86
|
+
return getActiveAskPermissionPolicy() ?? boundProvider;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Shared record helpers ────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
async function removePendingAskRecord(requestId: string): Promise<void> {
|
|
92
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
93
|
+
if (record) await interaction().removePendingAsk(record.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clearAskTimer(askId: string): void {
|
|
97
|
+
const timer = askTimers.get(askId);
|
|
98
|
+
if (!timer) return;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
askTimers.delete(askId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function clearTimer(requestId: string): void {
|
|
104
|
+
const timer = timers.get(requestId);
|
|
105
|
+
if (!timer) return;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
timers.delete(requestId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function expirePermissionRequestByRequestId(requestId: string): Promise<boolean> {
|
|
111
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
112
|
+
if (!record || record.status !== 'pending') return false;
|
|
113
|
+
return interaction().expirePermissionRequest(record.id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function persistCanonicalGrants(record: PendingAskRecord, response: unknown): Promise<void> {
|
|
117
|
+
// Canonical construction only: a replacement policy has no grant
|
|
118
|
+
// surface, so out-of-policy scopes can never reach the store.
|
|
119
|
+
if (!isValidPermissionResponse(response) || !isPermissionApproved(response)) return;
|
|
120
|
+
for (const params of buildGrantParams(record, response)) {
|
|
121
|
+
await interaction().createGrantFromOptions(params);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Generic ask lifecycle ────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function createAskApi(
|
|
128
|
+
sessionId: string,
|
|
129
|
+
toolCallId: string,
|
|
130
|
+
toolName: string,
|
|
131
|
+
broadcastFn: AskBroadcastFn,
|
|
132
|
+
workspaceId?: string,
|
|
133
|
+
rootSessionId?: string,
|
|
134
|
+
): AskApi {
|
|
135
|
+
let askCounter = 0;
|
|
136
|
+
|
|
137
|
+
const ask = async (request: Ask): Promise<unknown> => {
|
|
138
|
+
if (request.type === 'permission') {
|
|
139
|
+
return requestPermission({
|
|
140
|
+
sessionId,
|
|
141
|
+
rootSessionId,
|
|
142
|
+
toolCallId,
|
|
143
|
+
toolName,
|
|
144
|
+
ask: request,
|
|
145
|
+
broadcastFn,
|
|
146
|
+
workspaceId,
|
|
147
|
+
timeoutMs: activeProvider().askTimeoutMs,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const askId = `${toolCallId}#${++askCounter}`;
|
|
152
|
+
const authority = resolveAskAuthority(request);
|
|
153
|
+
|
|
154
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
155
|
+
broadcastFn({
|
|
156
|
+
type: 'ask.request',
|
|
157
|
+
sessionId,
|
|
158
|
+
toolCallId,
|
|
159
|
+
toolName,
|
|
160
|
+
ask: request,
|
|
161
|
+
authority,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
pendingAsks.set(askId, {
|
|
165
|
+
resolve,
|
|
166
|
+
reject,
|
|
167
|
+
ask: request,
|
|
168
|
+
sessionId,
|
|
169
|
+
toolCallId,
|
|
170
|
+
toolName,
|
|
171
|
+
broadcastFn,
|
|
172
|
+
authority,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
void (async () => {
|
|
176
|
+
await interaction().createPendingAsk({
|
|
177
|
+
sessionId,
|
|
178
|
+
toolCallId,
|
|
179
|
+
toolName,
|
|
180
|
+
ask: request,
|
|
181
|
+
createdAt: Date.now(),
|
|
182
|
+
requestId: askId,
|
|
183
|
+
status: 'pending',
|
|
184
|
+
isPermission: false,
|
|
185
|
+
workspaceId,
|
|
186
|
+
});
|
|
187
|
+
if (!pendingAsks.has(askId)) return;
|
|
188
|
+
|
|
189
|
+
const timerId = setTimeout(() => {
|
|
190
|
+
if (!pendingAsks.has(askId)) return;
|
|
191
|
+
pendingAsks.delete(askId);
|
|
192
|
+
askTimers.delete(askId);
|
|
193
|
+
void removePendingAskRecord(askId).catch((error: unknown) => console.error(error));
|
|
194
|
+
broadcastFn({
|
|
195
|
+
type: 'ask.timeout',
|
|
196
|
+
sessionId,
|
|
197
|
+
toolCallId,
|
|
198
|
+
});
|
|
199
|
+
reject(new Error('User did not respond in time'));
|
|
200
|
+
}, activeProvider().askTimeoutMs);
|
|
201
|
+
askTimers.set(askId, timerId);
|
|
202
|
+
})().catch(reject);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return ask as AskApi;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function resolvePendingAsk(key: string, pending: PendingAsk, response: unknown): Promise<boolean> {
|
|
210
|
+
clearAskTimer(key);
|
|
211
|
+
pending.resolve(extractResolutionValue(response));
|
|
212
|
+
pendingAsks.delete(key);
|
|
213
|
+
await removePendingAskRecord(key);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function resolveAsk(toolCallId: string, response: unknown, requestId?: string): Promise<boolean> {
|
|
218
|
+
if (requestId) {
|
|
219
|
+
const pending = pendingAsks.get(requestId);
|
|
220
|
+
if (pending) return await resolvePendingAsk(requestId, pending, response);
|
|
221
|
+
return await resolvePermission(requestId, response);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const exact = pendingAsks.get(toolCallId);
|
|
225
|
+
if (exact) return await resolvePendingAsk(toolCallId, exact, response);
|
|
226
|
+
|
|
227
|
+
for (const [key, pending] of pendingAsks) {
|
|
228
|
+
if (key === toolCallId || key.startsWith(`${toolCallId}#`)) {
|
|
229
|
+
return await resolvePendingAsk(key, pending, response);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function rejectPendingAsk(key: string, pending: PendingAsk, error: Error): Promise<boolean> {
|
|
236
|
+
clearAskTimer(key);
|
|
237
|
+
pending.reject(error);
|
|
238
|
+
pendingAsks.delete(key);
|
|
239
|
+
await removePendingAskRecord(key);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function rejectAsk(toolCallId: string, error: Error): Promise<boolean> {
|
|
244
|
+
const exact = pendingAsks.get(toolCallId);
|
|
245
|
+
if (exact) return await rejectPendingAsk(toolCallId, exact, error);
|
|
246
|
+
|
|
247
|
+
for (const [key, pending] of pendingAsks) {
|
|
248
|
+
if (key === toolCallId || key.startsWith(`${toolCallId}#`)) {
|
|
249
|
+
return await rejectPendingAsk(key, pending, error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function rejectPendingAsksByToolCallId(toolCallId: string, error?: Error): Promise<string[]> {
|
|
256
|
+
const rejectedIds = await rejectPermissionsByToolCallId(toolCallId, error);
|
|
257
|
+
const timeoutError = error ?? new Error('Tool execution ended');
|
|
258
|
+
|
|
259
|
+
for (const [askId, pending] of pendingAsks) {
|
|
260
|
+
if (
|
|
261
|
+
pending.toolCallId !== toolCallId
|
|
262
|
+
&& askId !== toolCallId
|
|
263
|
+
&& !askId.startsWith(`${toolCallId}#`)
|
|
264
|
+
) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
clearAskTimer(askId);
|
|
268
|
+
pending.broadcastFn({
|
|
269
|
+
type: 'ask.timeout',
|
|
270
|
+
sessionId: pending.sessionId,
|
|
271
|
+
toolCallId: pending.toolCallId,
|
|
272
|
+
});
|
|
273
|
+
pending.reject(timeoutError);
|
|
274
|
+
pendingAsks.delete(askId);
|
|
275
|
+
await removePendingAskRecord(askId);
|
|
276
|
+
rejectedIds.push(askId);
|
|
277
|
+
}
|
|
278
|
+
return rejectedIds;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function rejectPendingAsksBySession(sessionId: string, error?: Error): Promise<string[]> {
|
|
282
|
+
const rejectedIds = await rejectPermissionsBySession(sessionId, error);
|
|
283
|
+
const interruptError = error ?? new Error('Session interrupted');
|
|
284
|
+
|
|
285
|
+
for (const [askId, pending] of pendingAsks) {
|
|
286
|
+
if (pending.sessionId !== sessionId) continue;
|
|
287
|
+
clearAskTimer(askId);
|
|
288
|
+
pending.reject(interruptError);
|
|
289
|
+
pendingAsks.delete(askId);
|
|
290
|
+
await removePendingAskRecord(askId);
|
|
291
|
+
rejectedIds.push(askId);
|
|
292
|
+
}
|
|
293
|
+
return rejectedIds;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasPendingAsk(toolCallId: string): boolean {
|
|
297
|
+
if (pendingAsks.has(toolCallId)) return true;
|
|
298
|
+
for (const key of pendingAsks.keys()) {
|
|
299
|
+
if (key.startsWith(`${toolCallId}#`)) return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getAuthorityForPendingAsk(toolCallId: string): AskAuthority | undefined {
|
|
305
|
+
const exact = pendingAsks.get(toolCallId);
|
|
306
|
+
if (exact) return exact.authority;
|
|
307
|
+
for (const [key, pending] of pendingAsks) {
|
|
308
|
+
if (key === toolCallId || key.startsWith(`${toolCallId}#`)) return pending.authority;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function getSessionIdForPendingAsk(toolCallId: string, requestId?: string): Promise<string | null> {
|
|
314
|
+
if (requestId) {
|
|
315
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
316
|
+
if (record) return record.sessionId;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const exact = pendingAsks.get(toolCallId);
|
|
320
|
+
if (exact) return exact.sessionId;
|
|
321
|
+
for (const [key, pending] of pendingAsks) {
|
|
322
|
+
if (key === toolCallId || key.startsWith(`${toolCallId}#`)) return pending.sessionId;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!requestId) {
|
|
326
|
+
const record = await interaction().getPermissionRequestByRequestId(toolCallId);
|
|
327
|
+
if (record) return record.sessionId;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const listPendingAsksBySession = async (sessionId: string): Promise<PendingAskRecord[]> =>
|
|
333
|
+
interaction().listPendingAsksBySession(sessionId);
|
|
334
|
+
const listPendingAsksByRootSession = async (rootSessionId: string): Promise<PendingAskRecord[]> =>
|
|
335
|
+
interaction().listPendingAsksByRootSession(rootSessionId);
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
// ── Permission lifecycle ─────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
async function requestPermission(params: RequestPermissionParams): Promise<unknown> {
|
|
341
|
+
const {
|
|
342
|
+
sessionId,
|
|
343
|
+
rootSessionId,
|
|
344
|
+
workspaceId,
|
|
345
|
+
toolCallId,
|
|
346
|
+
toolName,
|
|
347
|
+
ask,
|
|
348
|
+
broadcastFn,
|
|
349
|
+
timeoutMs = activeProvider().permissionTimeoutMs,
|
|
350
|
+
} = params;
|
|
351
|
+
const isPermissionAsk = ask.type === 'permission';
|
|
352
|
+
const provider = activeProvider();
|
|
353
|
+
|
|
354
|
+
if (isPermissionAsk && workspaceId) {
|
|
355
|
+
const permAsk = ask as import('@capekai/tool').PermissionAsk;
|
|
356
|
+
const effectiveRootSessionId = rootSessionId ?? sessionId;
|
|
357
|
+
|
|
358
|
+
if (permAsk.intents && permAsk.intents.length > 0) {
|
|
359
|
+
for (const intent of permAsk.intents) {
|
|
360
|
+
for (const target of intent.targets) {
|
|
361
|
+
const matchResult = await interaction().matchGrant({
|
|
362
|
+
workspaceId,
|
|
363
|
+
toolName,
|
|
364
|
+
resource: intent.resource,
|
|
365
|
+
action: intent.action,
|
|
366
|
+
permissionKey: target.target,
|
|
367
|
+
rootSessionId: effectiveRootSessionId,
|
|
368
|
+
});
|
|
369
|
+
if (matchResult.matched) return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const permissionKey = buildPermissionKey(
|
|
375
|
+
toolName,
|
|
376
|
+
permAsk.resource ?? 'file',
|
|
377
|
+
permAsk.patterns,
|
|
378
|
+
);
|
|
379
|
+
const matchResult = await interaction().matchGrant({
|
|
380
|
+
workspaceId,
|
|
381
|
+
toolName,
|
|
382
|
+
resource: permAsk.resource ?? 'file',
|
|
383
|
+
permissionKey,
|
|
384
|
+
rootSessionId: effectiveRootSessionId,
|
|
385
|
+
});
|
|
386
|
+
if (matchResult.matched) return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (await provider.shouldAutoApprove(sessionId, ask)) return true;
|
|
390
|
+
|
|
391
|
+
const requestId = randomUUID();
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
await interaction().createPendingAsk({
|
|
394
|
+
sessionId,
|
|
395
|
+
rootSessionId,
|
|
396
|
+
workspaceId,
|
|
397
|
+
toolCallId,
|
|
398
|
+
toolName,
|
|
399
|
+
ask,
|
|
400
|
+
requestId,
|
|
401
|
+
status: 'pending',
|
|
402
|
+
isPermission: isPermissionAsk,
|
|
403
|
+
expiresAt: now + timeoutMs,
|
|
404
|
+
createdAt: now,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (isPermissionAsk) {
|
|
408
|
+
await interaction().notifyPermissionRequired(requestId, rootSessionId ?? sessionId);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
broadcastFn({
|
|
412
|
+
type: 'ask.request',
|
|
413
|
+
sessionId,
|
|
414
|
+
toolCallId,
|
|
415
|
+
toolName,
|
|
416
|
+
ask,
|
|
417
|
+
requestId,
|
|
418
|
+
authority: {
|
|
419
|
+
visibilityScope: 'controller_only',
|
|
420
|
+
resolutionMode: 'controller_only',
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
425
|
+
waiters.set(requestId, {
|
|
426
|
+
resolve,
|
|
427
|
+
reject,
|
|
428
|
+
createdAt: now,
|
|
429
|
+
sessionId,
|
|
430
|
+
toolCallId,
|
|
431
|
+
broadcastFn,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const timerId = setTimeout(() => {
|
|
435
|
+
if (!waiters.has(requestId)) return;
|
|
436
|
+
waiters.delete(requestId);
|
|
437
|
+
timers.delete(requestId);
|
|
438
|
+
void expirePermissionRequestByRequestId(requestId).catch((error: unknown) => console.error(error));
|
|
439
|
+
broadcastFn({
|
|
440
|
+
type: 'ask.timeout',
|
|
441
|
+
sessionId,
|
|
442
|
+
toolCallId,
|
|
443
|
+
requestId,
|
|
444
|
+
});
|
|
445
|
+
reject(new Error('User did not respond in time'));
|
|
446
|
+
}, timeoutMs);
|
|
447
|
+
timers.set(requestId, timerId);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function resolvePermission(requestId: string, response: unknown): Promise<boolean> {
|
|
452
|
+
const waiter = waiters.get(requestId);
|
|
453
|
+
// Mandatory validation from the module-level validator, never from
|
|
454
|
+
// provider advice.
|
|
455
|
+
const approved = isPermissionApproved(response);
|
|
456
|
+
|
|
457
|
+
if (!waiter) {
|
|
458
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
459
|
+
if (record?.status === 'pending') {
|
|
460
|
+
await interaction().resolvePermissionRequestByRequestId(
|
|
461
|
+
requestId,
|
|
462
|
+
approved ? 'approved' : 'denied',
|
|
463
|
+
response,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
clearTimer(requestId);
|
|
470
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
471
|
+
if (record?.status === 'pending') {
|
|
472
|
+
await interaction().resolvePermissionRequestByRequestId(
|
|
473
|
+
requestId,
|
|
474
|
+
approved ? 'approved' : 'denied',
|
|
475
|
+
response,
|
|
476
|
+
);
|
|
477
|
+
if (approved && record.isPermission) {
|
|
478
|
+
await persistCanonicalGrants(record, response);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
waiter.resolve(record?.isPermission ? approved : false);
|
|
483
|
+
waiters.delete(requestId);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function rejectPermission(requestId: string, error: Error): boolean {
|
|
488
|
+
const waiter = waiters.get(requestId);
|
|
489
|
+
if (!waiter) return false;
|
|
490
|
+
clearTimer(requestId);
|
|
491
|
+
waiter.reject(error);
|
|
492
|
+
waiters.delete(requestId);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function rejectPermissionsByToolCallId(toolCallId: string, error?: Error): Promise<string[]> {
|
|
497
|
+
const rejectedIds: string[] = [];
|
|
498
|
+
const timeoutError = error ?? new Error('Tool execution ended');
|
|
499
|
+
|
|
500
|
+
for (const [requestId, waiter] of waiters) {
|
|
501
|
+
if (waiter.toolCallId !== toolCallId) continue;
|
|
502
|
+
clearTimer(requestId);
|
|
503
|
+
await expirePermissionRequestByRequestId(requestId);
|
|
504
|
+
waiter.broadcastFn({
|
|
505
|
+
type: 'ask.timeout',
|
|
506
|
+
sessionId: waiter.sessionId,
|
|
507
|
+
toolCallId: waiter.toolCallId,
|
|
508
|
+
requestId,
|
|
509
|
+
});
|
|
510
|
+
waiter.reject(timeoutError);
|
|
511
|
+
waiters.delete(requestId);
|
|
512
|
+
rejectedIds.push(requestId);
|
|
513
|
+
}
|
|
514
|
+
return rejectedIds;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function rejectPermissionsBySession(sessionId: string, error?: Error): Promise<string[]> {
|
|
518
|
+
const rejectedIds: string[] = [];
|
|
519
|
+
const interruptError = error ?? new Error('Session interrupted');
|
|
520
|
+
await interaction().cancelPendingRequestsBySession(sessionId);
|
|
521
|
+
|
|
522
|
+
for (const [requestId, waiter] of waiters) {
|
|
523
|
+
const record = await interaction().getPermissionRequestByRequestId(requestId);
|
|
524
|
+
if (record?.sessionId !== sessionId) continue;
|
|
525
|
+
clearTimer(requestId);
|
|
526
|
+
waiter.broadcastFn({
|
|
527
|
+
type: 'ask.timeout',
|
|
528
|
+
sessionId: waiter.sessionId,
|
|
529
|
+
toolCallId: waiter.toolCallId,
|
|
530
|
+
requestId,
|
|
531
|
+
});
|
|
532
|
+
waiter.reject(interruptError);
|
|
533
|
+
waiters.delete(requestId);
|
|
534
|
+
rejectedIds.push(requestId);
|
|
535
|
+
}
|
|
536
|
+
return rejectedIds;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const getPendingRequestsByRootSession = async (rootSessionId: string): Promise<PendingAskRecord[]> =>
|
|
540
|
+
interaction().listPendingRequestsByRootSession(rootSessionId);
|
|
541
|
+
|
|
542
|
+
async function expireOldRequests(maxAgeMs: number): Promise<number> {
|
|
543
|
+
const count = await interaction().expireOldPermissionRequests(maxAgeMs);
|
|
544
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
545
|
+
for (const [requestId, waiter] of waiters) {
|
|
546
|
+
if (waiter.createdAt >= cutoff) continue;
|
|
547
|
+
clearTimer(requestId);
|
|
548
|
+
waiter.reject(new Error('Permission request expired'));
|
|
549
|
+
waiters.delete(requestId);
|
|
550
|
+
}
|
|
551
|
+
return count;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const hasPendingWaiter = (requestId: string): boolean => waiters.has(requestId);
|
|
555
|
+
const getPendingWaiterCount = (): number => waiters.size;
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
id,
|
|
559
|
+
get provider(): AskPermissionPolicyService {
|
|
560
|
+
return activeProvider();
|
|
561
|
+
},
|
|
562
|
+
createAskApi,
|
|
563
|
+
resolveAsk,
|
|
564
|
+
rejectAsk,
|
|
565
|
+
rejectPendingAsksByToolCallId,
|
|
566
|
+
rejectPendingAsksBySession,
|
|
567
|
+
hasPendingAsk,
|
|
568
|
+
getAuthorityForPendingAsk,
|
|
569
|
+
getSessionIdForPendingAsk,
|
|
570
|
+
listPendingAsksBySession,
|
|
571
|
+
listPendingAsksByRootSession,
|
|
572
|
+
requestPermission,
|
|
573
|
+
resolvePermission,
|
|
574
|
+
rejectPermission,
|
|
575
|
+
rejectPermissionsByToolCallId,
|
|
576
|
+
rejectPermissionsBySession,
|
|
577
|
+
getPendingRequestsByRootSession,
|
|
578
|
+
expireOldRequests,
|
|
579
|
+
hasPendingWaiter,
|
|
580
|
+
getPendingWaiterCount,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const scopedRuntime = new AsyncLocalStorage<PermissionRuntimeService>();
|
|
585
|
+
let processDefaultRuntime: PermissionRuntimeService | undefined;
|
|
586
|
+
|
|
587
|
+
/** Resolves the runtime seeded for the active agent scope, falling back to
|
|
588
|
+
* one lazily created process-default runtime over the process-default
|
|
589
|
+
* advice provider. The runtime is non-replaceable: only the advice provider
|
|
590
|
+
* behind it can be swapped. */
|
|
591
|
+
export function getPermissionRuntimeService(): PermissionRuntimeService {
|
|
592
|
+
return scopedRuntime.getStore()
|
|
593
|
+
?? (processDefaultRuntime ??= createPermissionRuntimeService({
|
|
594
|
+
id: 'permission.process-runtime',
|
|
595
|
+
provider: getAskPermissionPolicy(),
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Seeds a runtime for the callback duration. `enterAgentScope` seeds the
|
|
600
|
+
* composed agent scope's runtime here. */
|
|
601
|
+
export function withPermissionRuntimeService<T>(
|
|
602
|
+
service: PermissionRuntimeService,
|
|
603
|
+
callback: () => T,
|
|
604
|
+
): T {
|
|
605
|
+
return scopedRuntime.run(service, callback);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Test-only reset of the lazily created process default. Exported from this
|
|
609
|
+
* module only; no package subpath re-exports it. */
|
|
610
|
+
export function resetDefaultPermissionRuntimeForTests(): void {
|
|
611
|
+
processDefaultRuntime = undefined;
|
|
612
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CapekPlugin, PluginContext } from '../kernel/types';
|
|
2
|
+
import {
|
|
3
|
+
createCompactionService,
|
|
4
|
+
type CompactionServiceOptions,
|
|
5
|
+
} from '../compaction/policy';
|
|
6
|
+
import {
|
|
7
|
+
capekCompactionServiceKey,
|
|
8
|
+
capekRuntimeConfigurationKey,
|
|
9
|
+
} from './service-keys';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* C6 provider for the agent-scoped compaction service contract
|
|
13
|
+
* (`capek.compaction-service`). Environment variables are translated into
|
|
14
|
+
* provider options here, at composition: the plugin reads the composed
|
|
15
|
+
* runtime configuration once at setup and freezes the compaction values into
|
|
16
|
+
* the service options, so no runtime code reads compaction environment
|
|
17
|
+
* variables directly. The default provider reproduces the exact current
|
|
18
|
+
* trigger, summary, pruning, cooldown, replay, and recovery behavior.
|
|
19
|
+
*/
|
|
20
|
+
export function compactionPolicyPlugin(id: string): CapekPlugin<unknown> {
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
scope: 'agent',
|
|
24
|
+
provides: [capekCompactionServiceKey],
|
|
25
|
+
requires: [capekRuntimeConfigurationKey],
|
|
26
|
+
setup(context: PluginContext) {
|
|
27
|
+
const configuration = context.require(capekRuntimeConfigurationKey);
|
|
28
|
+
const options: CompactionServiceOptions = {
|
|
29
|
+
modelId: configuration.getCompactionModel() ?? null,
|
|
30
|
+
providerId: configuration.getCompactionProvider() ?? null,
|
|
31
|
+
maxOutputTokens: configuration.getCompactionMaxTokens(),
|
|
32
|
+
preserveRecentToolCount: configuration.getCompactionPreserveRecentToolCount(),
|
|
33
|
+
preserveSmallToolChars: configuration.getCompactionPreserveSmallToolChars(),
|
|
34
|
+
toolClearCharsThreshold: configuration.getCompactionToolClearCharsThreshold(),
|
|
35
|
+
maxPrunedToolCount: configuration.getCompactionMaxPrunedToolCount(),
|
|
36
|
+
autoThresholdRatio: configuration.getCompactionAutoThresholdRatio(),
|
|
37
|
+
autoReserveCapTokens: configuration.getCompactionAutoReserveCapTokens(),
|
|
38
|
+
autoSafetyMarginTokens: configuration.getCompactionAutoSafetyMarginTokens(),
|
|
39
|
+
};
|
|
40
|
+
context.provide(
|
|
41
|
+
capekCompactionServiceKey,
|
|
42
|
+
createCompactionService({ id, options }),
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|