@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 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",
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.2",
81
- "@capekai/types": "^1.0.1",
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: { type: 'json' as const, value: stripVisualization(toolPart.state.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 (typeof delta.output === 'string') {
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
- if (result.visualization && result.result && typeof result.result === 'object') {
125
- return { ...result.result as Record<string, unknown>, _visualization: result.visualization };
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);
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  export {
12
+ configureWorkspacePolicy,
12
13
  expandPath,
13
14
  isInsideUnselectedAdditionalRoot,
14
15
  isPathInside,
@@ -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 { homedir } from 'os';
3
- import { SENSITIVE_FILE_PATTERNS } from '@capekai/types';
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(id: string): CapekPlugin<unknown> {
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
+ }
@@ -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);