@capekai/core 1.0.3 → 1.0.5
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 +43 -0
- package/package.json +3 -3
- package/src/core/message-utils.ts +4 -1
- package/src/core/stream-handlers.ts +6 -1
- package/src/core/tool-builders/external-tools.ts +9 -5
- package/src/internal/workspace.ts +1 -0
- package/src/plugins/facade-plugins.ts +5 -1
- package/src/plugins/workspace-policy.ts +6 -13
- package/src/tool-output/policy.ts +11 -0
- package/src/tools/model-output.ts +75 -0
- package/src/workspace/policy.ts +18 -1
package/README.md
CHANGED
|
@@ -10,3 +10,46 @@ Requires Bun 1.3 or newer.
|
|
|
10
10
|
npm install @capekai/core
|
|
11
11
|
```
|
|
12
12
|
Public subpaths include `composition`, `plugins`, `hosts`, `execution`, `providers`, `tools`, `ask-authority`, `sandbox`, `workspace`, `configuration`, `tool`, and `storage`.
|
|
13
|
+
|
|
14
|
+
## Workspace policy
|
|
15
|
+
|
|
16
|
+
Čapek owns path resolution and containment. Embedding hosts can supply their own blocked paths, sensitive patterns, and home directory when composing an agent:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createComposition } from '@capekai/core/composition';
|
|
20
|
+
|
|
21
|
+
const composition = await createComposition(processScope, {
|
|
22
|
+
...values,
|
|
23
|
+
workspacePolicy: {
|
|
24
|
+
blockedPaths: ['/proc/', '/sys/'],
|
|
25
|
+
sensitivePatterns: ['.env', '.pem', '.key'],
|
|
26
|
+
homeDir: '/home/agent',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Hosts building a custom plugin profile can configure the same policy directly:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { workspacePolicyPlugin } from '@capekai/core/plugins';
|
|
35
|
+
|
|
36
|
+
workspacePolicyPlugin('host.workspace-policy', {
|
|
37
|
+
blockedPaths: [],
|
|
38
|
+
sensitivePatterns: ['credentials'],
|
|
39
|
+
homeDir: '/srv/agent',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For workspace helpers used outside an agent scope, configure the process-wide policy during host bootstrap:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { configureWorkspacePolicy } from '@capekai/core/workspace';
|
|
47
|
+
|
|
48
|
+
configureWorkspacePolicy({
|
|
49
|
+
blockedPaths: ['/proc/', '/sys/'],
|
|
50
|
+
sensitivePatterns: ['.env', '.pem', '.key'],
|
|
51
|
+
homeDir: '/home/agent',
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Call `configureWorkspacePolicy()` with no argument, or omit composition options, to retain the compatibility defaults.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capekai/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Bun-native composable agent runtime and framework for Capek.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@ai-sdk/deepseek": "^2.0.35",
|
|
79
79
|
"@ai-sdk/openai": "^3.0.84",
|
|
80
|
-
"@capekai/tool": "^1.0.
|
|
81
|
-
"@capekai/types": "^1.0.
|
|
80
|
+
"@capekai/tool": "^1.0.3",
|
|
81
|
+
"@capekai/types": "^1.0.2",
|
|
82
82
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
83
83
|
"@zip.js/zip.js": "^2.7.60",
|
|
84
84
|
"ai": "^6.0.116",
|
|
@@ -4,6 +4,7 @@ import { isTextPart, isToolPart, isImagePart, isFilePart, parseToolInput } from
|
|
|
4
4
|
import { stripVisualization } from '../utils/strip-visualization';
|
|
5
5
|
import { getAttachment } from '../storage/runtime';
|
|
6
6
|
import { isToolOutputArtifactReference, RETRIEVE_TOOL_OUTPUT_NAME } from '../tool-output/policy';
|
|
7
|
+
import { toolModelOutputToAiSdk } from '../tools/model-output';
|
|
7
8
|
|
|
8
9
|
type AiSdkContent = string | Array<{
|
|
9
10
|
type: 'text' | 'tool-call' | 'tool-result' | 'image' | 'file';
|
|
@@ -121,7 +122,9 @@ export async function convertToAiSdkMessages(
|
|
|
121
122
|
type: 'tool-result' as const,
|
|
122
123
|
toolCallId: toolPart.callId,
|
|
123
124
|
toolName: toolPart.name,
|
|
124
|
-
output:
|
|
125
|
+
output: toolPart.state.modelOutput
|
|
126
|
+
? toolModelOutputToAiSdk(toolPart.state.modelOutput)
|
|
127
|
+
: { type: 'json' as const, value: stripVisualization(toolPart.state.output) },
|
|
125
128
|
});
|
|
126
129
|
}
|
|
127
130
|
} else if (toolPart.state.status === 'error') {
|
|
@@ -2,6 +2,7 @@ import type { TextPart, ToolPart, ReasoningPart, MessageEvent } from '@capekai/t
|
|
|
2
2
|
import { createPart, updatePart, getPart, persistStreamingPartSnapshots } from '../storage/runtime';
|
|
3
3
|
import { parseToolInput } from './part-utils';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
+
import { isCapekToolOutputEnvelope } from '../tools/model-output';
|
|
5
6
|
|
|
6
7
|
const STREAM_PART_PERSIST_INTERVAL_MS = 300;
|
|
7
8
|
|
|
@@ -184,8 +185,11 @@ export function createStreamHandlers(ctx: StreamHandlerContext) {
|
|
|
184
185
|
const latestPart = await getPart(existingToolPart.id) as ToolPart | null;
|
|
185
186
|
const latestState = latestPart?.state;
|
|
186
187
|
|
|
188
|
+
const capekOutput = isCapekToolOutputEnvelope(delta.output) ? delta.output : undefined;
|
|
187
189
|
let resultData: unknown;
|
|
188
|
-
if (
|
|
190
|
+
if (capekOutput) {
|
|
191
|
+
resultData = capekOutput.value;
|
|
192
|
+
} else if (typeof delta.output === 'string') {
|
|
189
193
|
try {
|
|
190
194
|
resultData = JSON.parse(delta.output);
|
|
191
195
|
} catch {
|
|
@@ -218,6 +222,7 @@ export function createStreamHandlers(ctx: StreamHandlerContext) {
|
|
|
218
222
|
status: 'completed' as const,
|
|
219
223
|
input: existingToolPart.state.input,
|
|
220
224
|
output: resultData,
|
|
225
|
+
...(capekOutput && { modelOutput: capekOutput.modelOutput }),
|
|
221
226
|
startedAt: Date.now(),
|
|
222
227
|
completedAt: Date.now(),
|
|
223
228
|
...(existingChildSessionId && { childSessionId: existingChildSessionId }),
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
import { isToolAllowedInContext, type ToolExecutionScope } from '../tool-capabilities';
|
|
21
21
|
import type { ToolMap } from './types';
|
|
22
22
|
import type { BroadcastFn } from '../../runtime/host-dependencies';
|
|
23
|
+
import {
|
|
24
|
+
capekToolOutputToAiSdk,
|
|
25
|
+
createCapekToolOutputEnvelope,
|
|
26
|
+
} from '../../tools/model-output';
|
|
23
27
|
|
|
24
28
|
export interface ExternalToolsOptions {
|
|
25
29
|
toolNames: string[];
|
|
@@ -88,6 +92,7 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
88
92
|
tools[name] = tool({
|
|
89
93
|
description: definition.description,
|
|
90
94
|
inputSchema: jsonSchema(definition.inputSchema),
|
|
95
|
+
toModelOutput: ({ output }) => capekToolOutputToAiSdk(output),
|
|
91
96
|
execute: async (args: Record<string, unknown>, { toolCallId }: { toolCallId: string }) => {
|
|
92
97
|
const toolAbortController = interruptManager.registerToolExecution(sessionId, toolCallId);
|
|
93
98
|
|
|
@@ -121,11 +126,10 @@ export async function buildExternalTools(options: ExternalToolsOptions): Promise
|
|
|
121
126
|
return { error: result.error ?? 'Tool execution failed' };
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return result.result;
|
|
129
|
+
const clientResult = result.visualization && result.result && typeof result.result === 'object'
|
|
130
|
+
? { ...result.result as Record<string, unknown>, _visualization: result.visualization }
|
|
131
|
+
: result.result;
|
|
132
|
+
return createCapekToolOutputEnvelope(clientResult, result.modelOutput);
|
|
129
133
|
} finally {
|
|
130
134
|
interruptManager.unregisterToolExecution(sessionId, toolCallId);
|
|
131
135
|
await rejectPendingAsksByToolCallId(toolCallId);
|
|
@@ -16,6 +16,7 @@ import type { SandboxController } from '../sandbox/controller';
|
|
|
16
16
|
import type { StorageBundle } from '../storage/contracts';
|
|
17
17
|
import type { ToolRegistryResolver } from '../tools/registry';
|
|
18
18
|
import type { WorkspaceToolDiscovery } from '../tools/tool-source';
|
|
19
|
+
import type { WorkspacePolicyOptions } from '../workspace/contracts';
|
|
19
20
|
import { getSchedulerHost } from '../scheduler/host';
|
|
20
21
|
import { getSessionSearchHost } from '../session-search/host';
|
|
21
22
|
import { createContextSectionsPlugin } from './context-sections';
|
|
@@ -64,6 +65,9 @@ export interface FacadeScopeValues {
|
|
|
64
65
|
host: RuntimeHost;
|
|
65
66
|
contextSources: Partial<ContextSources>;
|
|
66
67
|
workspaceToolDiscovery: WorkspaceToolDiscovery;
|
|
68
|
+
/** Host-owned path classification values. When omitted, the current
|
|
69
|
+
* compatibility defaults apply. */
|
|
70
|
+
workspacePolicy?: WorkspacePolicyOptions;
|
|
67
71
|
/** Optional compatibility resolver. When omitted, the facade
|
|
68
72
|
* composition derives the resolver from the composed scope's effective
|
|
69
73
|
* contributed tool payloads. The explicit value is the rollback
|
|
@@ -109,7 +113,7 @@ export function createFacadeAgentPlugins(values: FacadeScopeValues): readonly Ca
|
|
|
109
113
|
retryPolicyPlugin('facade.retry-policy'),
|
|
110
114
|
compactionPolicyPlugin('facade.compaction-policy'),
|
|
111
115
|
permissionPolicyPlugin('facade.permission-policy'),
|
|
112
|
-
workspacePolicyPlugin('facade.workspace-policy'),
|
|
116
|
+
workspacePolicyPlugin('facade.workspace-policy', values.workspacePolicy),
|
|
113
117
|
toolOutputPolicyPlugin('facade.tool-output-policy'),
|
|
114
118
|
contextSourcesValuePlugin('facade.context-sources', values.contextSources),
|
|
115
119
|
// Facade context parity: the facade keeps the legacy self-delegation and
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { CapekPlugin, PluginContext } from '../kernel/types';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
BLOCKED_PATHS,
|
|
6
|
-
createWorkspaceService,
|
|
7
|
-
type WorkspacePolicyOptions,
|
|
8
|
-
} from '../workspace/policy';
|
|
2
|
+
import type { WorkspacePolicyOptions } from '../workspace/contracts';
|
|
3
|
+
import { createWorkspaceService } from '../workspace/policy';
|
|
9
4
|
import { capekWorkspacePolicyKey } from './service-keys';
|
|
10
5
|
|
|
11
6
|
/**
|
|
@@ -17,17 +12,15 @@ import { capekWorkspacePolicyKey } from './service-keys';
|
|
|
17
12
|
* current containment, root classification, expansion, and sensitive/blocked
|
|
18
13
|
* denial behavior.
|
|
19
14
|
*/
|
|
20
|
-
export function workspacePolicyPlugin(
|
|
15
|
+
export function workspacePolicyPlugin(
|
|
16
|
+
id: string,
|
|
17
|
+
options?: WorkspacePolicyOptions,
|
|
18
|
+
): CapekPlugin<unknown> {
|
|
21
19
|
return {
|
|
22
20
|
id,
|
|
23
21
|
scope: 'agent',
|
|
24
22
|
provides: [capekWorkspacePolicyKey],
|
|
25
23
|
setup(context: PluginContext) {
|
|
26
|
-
const options: WorkspacePolicyOptions = {
|
|
27
|
-
blockedPaths: [...BLOCKED_PATHS],
|
|
28
|
-
sensitivePatterns: [...SENSITIVE_FILE_PATTERNS],
|
|
29
|
-
homeDir: homedir(),
|
|
30
|
-
};
|
|
31
24
|
context.provide(
|
|
32
25
|
capekWorkspacePolicyKey,
|
|
33
26
|
createWorkspaceService({ id, options }),
|
|
@@ -34,6 +34,10 @@ import type {
|
|
|
34
34
|
ToolOutputPolicyContext,
|
|
35
35
|
ToolOutputPolicyOptions,
|
|
36
36
|
} from './contracts';
|
|
37
|
+
import {
|
|
38
|
+
isCapekToolOutputEnvelope,
|
|
39
|
+
type CapekToolOutputEnvelope,
|
|
40
|
+
} from '../tools/model-output';
|
|
37
41
|
|
|
38
42
|
export const TOOL_OUTPUT_THRESHOLD_CHARS = 50_000;
|
|
39
43
|
export const TOOL_OUTPUT_PREVIEW_CHARS = 10_000;
|
|
@@ -116,6 +120,13 @@ export function createToolOutputService(
|
|
|
116
120
|
const outputPolicyWrappedTools = new WeakSet<object>();
|
|
117
121
|
|
|
118
122
|
async function applyToolOutputPolicy(result: unknown, context: ToolOutputPolicyContext): Promise<unknown> {
|
|
123
|
+
if (isCapekToolOutputEnvelope(result)) {
|
|
124
|
+
return {
|
|
125
|
+
...result,
|
|
126
|
+
value: await applyToolOutputPolicy(result.value, context),
|
|
127
|
+
} satisfies CapekToolOutputEnvelope;
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
const visualization = result && typeof result === 'object' && !Array.isArray(result)
|
|
120
131
|
? (result as Record<string, unknown>)._visualization
|
|
121
132
|
: undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { JSONValue, Tool } from 'ai';
|
|
2
|
+
import type { ToolModelOutputPart } from '@capekai/types';
|
|
3
|
+
|
|
4
|
+
type AiSdkToolResultOutput = Awaited<ReturnType<NonNullable<Tool['toModelOutput']>>>;
|
|
5
|
+
|
|
6
|
+
const CAPEK_TOOL_OUTPUT_TYPE = 'capek-tool-output';
|
|
7
|
+
|
|
8
|
+
export interface CapekToolOutputEnvelope {
|
|
9
|
+
type: typeof CAPEK_TOOL_OUTPUT_TYPE;
|
|
10
|
+
value: unknown;
|
|
11
|
+
modelOutput: ToolModelOutputPart[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isToolModelOutputPart(value: unknown): value is ToolModelOutputPart {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
16
|
+
const part = value as Record<string, unknown>;
|
|
17
|
+
if (part.type === 'text') {
|
|
18
|
+
return typeof part.text === 'string';
|
|
19
|
+
}
|
|
20
|
+
if (part.type === 'image') {
|
|
21
|
+
return typeof part.data === 'string'
|
|
22
|
+
&& part.data.length > 0
|
|
23
|
+
&& !part.data.startsWith('data:')
|
|
24
|
+
&& typeof part.mediaType === 'string'
|
|
25
|
+
&& part.mediaType.startsWith('image/');
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeToolModelOutput(value: unknown): ToolModelOutputPart[] | undefined {
|
|
31
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(isToolModelOutputPart)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createCapekToolOutputEnvelope(
|
|
38
|
+
value: unknown,
|
|
39
|
+
modelOutput: unknown,
|
|
40
|
+
): unknown {
|
|
41
|
+
const normalized = normalizeToolModelOutput(modelOutput);
|
|
42
|
+
return normalized
|
|
43
|
+
? { type: CAPEK_TOOL_OUTPUT_TYPE, value, modelOutput: normalized }
|
|
44
|
+
: value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isCapekToolOutputEnvelope(value: unknown): value is CapekToolOutputEnvelope {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
49
|
+
const record = value as Record<string, unknown>;
|
|
50
|
+
return record.type === CAPEK_TOOL_OUTPUT_TYPE
|
|
51
|
+
&& 'value' in record
|
|
52
|
+
&& normalizeToolModelOutput(record.modelOutput) !== undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toolModelOutputToAiSdk(parts: ToolModelOutputPart[]): AiSdkToolResultOutput {
|
|
56
|
+
return {
|
|
57
|
+
type: 'content',
|
|
58
|
+
value: parts.map((part) => part.type === 'text'
|
|
59
|
+
? { type: 'text' as const, text: part.text }
|
|
60
|
+
: {
|
|
61
|
+
type: 'image-data' as const,
|
|
62
|
+
data: part.data,
|
|
63
|
+
mediaType: part.mediaType,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function capekToolOutputToAiSdk(output: unknown): AiSdkToolResultOutput {
|
|
69
|
+
if (isCapekToolOutputEnvelope(output)) {
|
|
70
|
+
return toolModelOutputToAiSdk(output.modelOutput);
|
|
71
|
+
}
|
|
72
|
+
return typeof output === 'string'
|
|
73
|
+
? { type: 'text', value: output }
|
|
74
|
+
: { type: 'json', value: (output ?? null) as JSONValue };
|
|
75
|
+
}
|
package/src/workspace/policy.ts
CHANGED
|
@@ -47,6 +47,14 @@ function defaultOptions(): WorkspacePolicyOptions {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function freezeOptions(options: WorkspacePolicyOptions): Readonly<WorkspacePolicyOptions> {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
blockedPaths: Object.freeze([...options.blockedPaths]),
|
|
53
|
+
sensitivePatterns: Object.freeze([...options.sensitivePatterns]),
|
|
54
|
+
homeDir: options.homeDir,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
// ── Mandatory containment runtime (C6 step 6) ───────────────────────────
|
|
51
59
|
// The tool-runtime capability is constructed HERE, not by provider methods:
|
|
52
60
|
// a custom provider supplies only frozen options (blocked paths, sensitive
|
|
@@ -130,7 +138,7 @@ export function createWorkspaceService(
|
|
|
130
138
|
createOptions: WorkspaceServiceCreateOptions = {},
|
|
131
139
|
): WorkspaceService {
|
|
132
140
|
const id = createOptions.id ?? 'workspace.default';
|
|
133
|
-
const options = createOptions.options ?? defaultOptions();
|
|
141
|
+
const options = freezeOptions(createOptions.options ?? defaultOptions());
|
|
134
142
|
|
|
135
143
|
const service: WorkspaceService = {
|
|
136
144
|
id,
|
|
@@ -251,6 +259,15 @@ export function getWorkspaceService(): WorkspaceService {
|
|
|
251
259
|
?? (processDefaultService ??= createWorkspaceService({ id: 'workspace.process-default' }));
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/** Configures the process-wide policy used outside an agent scope. Omitting
|
|
263
|
+
* options restores the compatibility defaults. */
|
|
264
|
+
export function configureWorkspacePolicy(options?: WorkspacePolicyOptions): void {
|
|
265
|
+
processDefaultService = createWorkspaceService({
|
|
266
|
+
id: 'workspace.process-default',
|
|
267
|
+
options,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
/** Builds the tool-runtime capability over the active workspace policy. */
|
|
255
272
|
export function createWorkspaceCapability(host: WorkspaceCapabilityHost): WorkspaceCapability {
|
|
256
273
|
return createWorkspaceCapabilityWithOptions(host, getWorkspaceService().options);
|