@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,130 @@
|
|
|
1
|
+
import type { BroadcastFn, BroadcastSessionFn } from '../runtime/host';
|
|
2
|
+
import {
|
|
3
|
+
runOrchestratorSession,
|
|
4
|
+
type OrchestratorSessionOptions,
|
|
5
|
+
type OrchestratorSessionResult,
|
|
6
|
+
} from './orchestrator-session';
|
|
7
|
+
|
|
8
|
+
/** Workflow domain: leaf result synthesis. Moved byte-for-byte from
|
|
9
|
+
* `core/workflow-synthesizer.ts`; the unscoped export keeps the pre-C5
|
|
10
|
+
* module-accessor behavior, and the WithDeps variant runs against the
|
|
11
|
+
* injected orchestrator contract captured by the domain plugin. */
|
|
12
|
+
|
|
13
|
+
/** Result from a single leaf agent execution. */
|
|
14
|
+
export interface LeafResult {
|
|
15
|
+
index: number;
|
|
16
|
+
text: string;
|
|
17
|
+
structuredResult?: Record<string, unknown>;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Result from the synthesis phase. */
|
|
22
|
+
export interface SynthesisResult {
|
|
23
|
+
text: string;
|
|
24
|
+
structuredResult?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SynthesizeResultsOptions {
|
|
28
|
+
originalPrompt: string;
|
|
29
|
+
leafResults: LeafResult[];
|
|
30
|
+
outputSchema?: Record<string, unknown>;
|
|
31
|
+
parentSessionId: string;
|
|
32
|
+
abortSignal?: AbortSignal;
|
|
33
|
+
broadcast?: BroadcastFn;
|
|
34
|
+
broadcastSessionCreated?: BroadcastSessionFn;
|
|
35
|
+
broadcastSessionUpdated?: BroadcastSessionFn;
|
|
36
|
+
runOrchestrator?: typeof runOrchestratorSession;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SynthesizeResultsDeps {
|
|
40
|
+
orchestrator: { run(options: OrchestratorSessionOptions): Promise<OrchestratorSessionResult> };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Unscoped synthesis over the module orchestrator implementation. */
|
|
44
|
+
export async function synthesizeResults(options: SynthesizeResultsOptions): Promise<SynthesisResult> {
|
|
45
|
+
return synthesizeResultsWithDeps(options, { orchestrator: { run: runOrchestratorSession } });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Composed synthesis over the injected orchestrator contract. */
|
|
49
|
+
export async function synthesizeResultsWithDeps(
|
|
50
|
+
options: SynthesizeResultsOptions,
|
|
51
|
+
deps: SynthesizeResultsDeps,
|
|
52
|
+
): Promise<SynthesisResult> {
|
|
53
|
+
console.log('[workflow:synthesize] Starting synthesis', {
|
|
54
|
+
parentSessionId: options.parentSessionId,
|
|
55
|
+
leafResultCount: options.leafResults.length,
|
|
56
|
+
hasOutputSchema: !!options.outputSchema,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const formattedResults = options.leafResults
|
|
60
|
+
.map((r) => {
|
|
61
|
+
const status = r.error ? '[FAILED]' : '[success]';
|
|
62
|
+
const parts: string[] = [`Sub-agent ${r.index + 1} ${status}:`];
|
|
63
|
+
if (r.error) {
|
|
64
|
+
parts.push(` Error: ${r.error}`);
|
|
65
|
+
} else {
|
|
66
|
+
parts.push(` Text: ${r.text || '(no text output)'}`);
|
|
67
|
+
if (r.structuredResult) {
|
|
68
|
+
parts.push(` Structured: ${JSON.stringify(r.structuredResult)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return parts.join('\n');
|
|
72
|
+
})
|
|
73
|
+
.join('\n\n');
|
|
74
|
+
|
|
75
|
+
let system = [
|
|
76
|
+
'You are a synthesis agent. You have been given the results of several parallel',
|
|
77
|
+
'sub-agents that were each working on part of a larger task.',
|
|
78
|
+
'',
|
|
79
|
+
'Your job:',
|
|
80
|
+
'1. Review all sub-agent results below.',
|
|
81
|
+
'2. Identify overlapping findings, contradictions, and gaps.',
|
|
82
|
+
'3. Produce a single consolidated answer that addresses the original task.',
|
|
83
|
+
'',
|
|
84
|
+
`Original task: ${options.originalPrompt}`,
|
|
85
|
+
'',
|
|
86
|
+
'Sub-agent results:',
|
|
87
|
+
formattedResults,
|
|
88
|
+
].join('\n');
|
|
89
|
+
|
|
90
|
+
let userPrompt = 'Synthesize the sub-agent results into a final answer.';
|
|
91
|
+
|
|
92
|
+
// If structured output is requested, inject the schema into the prompt
|
|
93
|
+
// (same approach as agent.ts prompt-based structured output — works universally)
|
|
94
|
+
if (options.outputSchema) {
|
|
95
|
+
const schemaStr = JSON.stringify(options.outputSchema, null, 2);
|
|
96
|
+
system += '\n\n' + [
|
|
97
|
+
'You must respond with ONLY valid JSON that conforms to the following JSON Schema.',
|
|
98
|
+
'Do not include any text before or after the JSON object. Do not wrap it in markdown code fences.',
|
|
99
|
+
'',
|
|
100
|
+
'JSON Schema:',
|
|
101
|
+
schemaStr,
|
|
102
|
+
].join('\n');
|
|
103
|
+
userPrompt = 'Synthesize the sub-agent results into a final answer. Respond with ONLY valid JSON conforming to the schema.';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await (options.runOrchestrator ?? deps.orchestrator.run)({
|
|
107
|
+
parentSessionId: options.parentSessionId,
|
|
108
|
+
title: `Synthesize: ${options.originalPrompt.slice(0, 50)}`,
|
|
109
|
+
agentName: 'synthesizer',
|
|
110
|
+
systemPrompt: system,
|
|
111
|
+
userPrompt,
|
|
112
|
+
maxTokens: 8192,
|
|
113
|
+
abortSignal: options.abortSignal,
|
|
114
|
+
broadcast: options.broadcast,
|
|
115
|
+
broadcastSessionCreated: options.broadcastSessionCreated,
|
|
116
|
+
broadcastSessionUpdated: options.broadcastSessionUpdated,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log('[workflow:synthesize] Orchestrator session returned', { textLength: result.text?.length });
|
|
120
|
+
|
|
121
|
+
if (options.outputSchema) {
|
|
122
|
+
if (result.json) {
|
|
123
|
+
console.log('[workflow:synthesize] Structured output parsed successfully');
|
|
124
|
+
return { text: result.text, structuredResult: result.json };
|
|
125
|
+
}
|
|
126
|
+
console.warn('[workflow:synthesize] Failed to parse structured output, returning raw text');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { text: result.text };
|
|
130
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 workspace policy contracts.
|
|
3
|
+
*
|
|
4
|
+
* The agent-scoped workspace policy owns containment and path
|
|
5
|
+
* classification. Two surfaces are unified here:
|
|
6
|
+
*
|
|
7
|
+
* - The tool-runtime capability surface previously living in
|
|
8
|
+
* `tools/workspace-capability.ts` (`isLexicallyContained` plus the
|
|
9
|
+
* capability built over a `WorkspaceCapabilityHost`: tilde/absolute/
|
|
10
|
+
* relative resolution, effective-root and additional-root containment,
|
|
11
|
+
* sensitive and blocked classification, environment overlay, and
|
|
12
|
+
* additional-root mutation through the host).
|
|
13
|
+
* - The server file-access surface previously living in the Jean2 workspace
|
|
14
|
+
* domain (`expandPath`, `resolvePath`, `isPathWithinWorkspace`,
|
|
15
|
+
* `isPathInside`, `isInsideUnselectedAdditionalRoot`,
|
|
16
|
+
* `resolveCandidatePath`, `resolveRootForQuery`, `selectEditableRoot`).
|
|
17
|
+
*
|
|
18
|
+
* The host interface keeps filesystem I/O on the host side: capability
|
|
19
|
+
* construction only classifies paths and delegates mutations and
|
|
20
|
+
* environment reads to the host. The Jean2 server fulfills the policy
|
|
21
|
+
* through the inward-facing `WorkspacePathPolicyPort` adapter
|
|
22
|
+
* (`adapters/capek/workspace-paths.ts`).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Host supplied by the Jean2 workspace adapter or the facade bindings.
|
|
26
|
+
* Filesystem I/O stays behind this interface; the policy never performs
|
|
27
|
+
* I/O itself. */
|
|
28
|
+
export interface WorkspaceCapabilityHost {
|
|
29
|
+
root?: string;
|
|
30
|
+
additionalRoots?: string[];
|
|
31
|
+
allowedRoots?: string[];
|
|
32
|
+
tempDir: string;
|
|
33
|
+
getEnvironmentValue?: (key: string) => string | undefined;
|
|
34
|
+
addAdditionalRoot?: (path: string) => boolean | Promise<boolean>;
|
|
35
|
+
removeAdditionalRoot?: (path: string) => boolean | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkspaceCapability {
|
|
39
|
+
effectiveRoot: string;
|
|
40
|
+
additionalRoots: string[];
|
|
41
|
+
allowedRoots: string[];
|
|
42
|
+
tempDir: string;
|
|
43
|
+
resolvePath(path: string): string;
|
|
44
|
+
isWithinWorkspace(path: string): boolean;
|
|
45
|
+
isSensitivePath(path: string): boolean;
|
|
46
|
+
isBlockedPath(path: string): boolean;
|
|
47
|
+
getEnvironmentValue(key: string): string | undefined;
|
|
48
|
+
addWorkspacePath(path: string): Promise<boolean>;
|
|
49
|
+
removeWorkspacePath(path: string): Promise<boolean>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Structural workspace shape used by the server-side root functions. */
|
|
53
|
+
export interface WorkspaceLike {
|
|
54
|
+
path: string;
|
|
55
|
+
additionalPaths: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Composition-time provider options. The defaults reproduce the exact
|
|
59
|
+
* pre-C6 constants; a custom provider may swap the blocked-path list, the
|
|
60
|
+
* sensitive pattern list, or the home directory without the consumers
|
|
61
|
+
* changing. */
|
|
62
|
+
export interface WorkspacePolicyOptions {
|
|
63
|
+
blockedPaths: readonly string[];
|
|
64
|
+
sensitivePatterns: readonly string[];
|
|
65
|
+
homeDir: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pure path classification policy. Every function is deterministic over its
|
|
70
|
+
* inputs and the frozen options; none performs filesystem I/O. The
|
|
71
|
+
* mandatory containment and sensitive/blocked denial invariants are part of
|
|
72
|
+
* the default provider and are not options (a custom provider replacing
|
|
73
|
+
* them takes over the invariant, exactly like the other C6 policies).
|
|
74
|
+
*/
|
|
75
|
+
export interface WorkspacePolicy {
|
|
76
|
+
/** Expands `~` to the frozen home directory and resolves. */
|
|
77
|
+
expandPath(inputPath: string): string;
|
|
78
|
+
/** Server-style resolution: tilde joins the frozen home, absolute inputs
|
|
79
|
+
* resolve verbatim, relative inputs anchor to the workspace path. */
|
|
80
|
+
resolvePathFor(path: string, workspacePath: string): string;
|
|
81
|
+
/** Relative-based containment over the workspace root and additional
|
|
82
|
+
* roots; separator-aware so `/main-other` never matches `/main`. */
|
|
83
|
+
isPathWithinWorkspace(
|
|
84
|
+
targetPath: string,
|
|
85
|
+
workspacePath: string,
|
|
86
|
+
additionalPaths?: string[],
|
|
87
|
+
): boolean;
|
|
88
|
+
/** Separator-aware containment so `/foo` does not match `/foobar`. */
|
|
89
|
+
isPathInside(child: string, parent: string): boolean;
|
|
90
|
+
/** Whether the candidate lies inside an additional root other than the
|
|
91
|
+
* selected one. */
|
|
92
|
+
isInsideUnselectedAdditionalRoot(
|
|
93
|
+
candidate: string,
|
|
94
|
+
selectedRoot: string,
|
|
95
|
+
additionalPaths: string[],
|
|
96
|
+
): boolean;
|
|
97
|
+
/** Resolves the client-supplied path against the selected root; absolute
|
|
98
|
+
* inputs resolve verbatim, relative inputs anchor to the root, and
|
|
99
|
+
* Windows backslashes normalize to forward slashes. */
|
|
100
|
+
resolveCandidatePath(root: string, inputPath: string): string;
|
|
101
|
+
/** Resolves an optional `root` query to an allowed root, falling back to
|
|
102
|
+
* the main workspace root when missing or invalid. */
|
|
103
|
+
resolveRootForQuery(
|
|
104
|
+
workspace: WorkspaceLike,
|
|
105
|
+
rootQuery?: string,
|
|
106
|
+
): { root: string; isMain: boolean };
|
|
107
|
+
/** Editable-file root selection: an explicit root must exactly match the
|
|
108
|
+
* main root or one additional root, otherwise `valid` is false (the
|
|
109
|
+
* caller rejects with the exact 'Invalid workspace root' error). */
|
|
110
|
+
selectEditableRoot(
|
|
111
|
+
workspace: WorkspaceLike,
|
|
112
|
+
rootQuery?: string,
|
|
113
|
+
): { root: string; valid: boolean };
|
|
114
|
+
/** Tool-runtime lexical containment: resolved path equals the root or
|
|
115
|
+
* starts with `root + sep`. */
|
|
116
|
+
isLexicallyContained(path: string, root: string): boolean;
|
|
117
|
+
/** Case-insensitive sensitive-pattern classification. */
|
|
118
|
+
isSensitivePath(path: string): boolean;
|
|
119
|
+
/** Blocked-path classification over the frozen blocked list (resolved
|
|
120
|
+
* input, case-sensitive). */
|
|
121
|
+
isBlockedPath(path: string): boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The combined agent-scoped workspace service: the pure policy plus the
|
|
126
|
+
* tool-runtime capability construction over a host.
|
|
127
|
+
*/
|
|
128
|
+
export interface WorkspaceService extends WorkspacePolicy {
|
|
129
|
+
readonly id: string;
|
|
130
|
+
/** Frozen composition-time options. */
|
|
131
|
+
readonly options: Readonly<WorkspacePolicyOptions>;
|
|
132
|
+
/** Builds the tool-runtime capability over a host using this service's
|
|
133
|
+
* policy decisions. */
|
|
134
|
+
createCapability(host: WorkspaceCapabilityHost): WorkspaceCapability;
|
|
135
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 workspace policy default provider and scoped service.
|
|
3
|
+
*
|
|
4
|
+
* `createWorkspaceService` reproduces the exact pre-C6 behavior: the
|
|
5
|
+
* tool-runtime capability policy from `tools/workspace-capability.ts` and
|
|
6
|
+
* the server file-access policy from the Jean2 workspace domain, unified
|
|
7
|
+
* over frozen composition-time options (blocked paths, sensitive patterns,
|
|
8
|
+
* home directory). The mandatory containment and sensitive/blocked
|
|
9
|
+
* classification invariants are part of the default provider.
|
|
10
|
+
*
|
|
11
|
+
* Scope ownership: a composed agent scope gets its own service instance.
|
|
12
|
+
* Consumers that run outside a composed scope (the current Jean2 server
|
|
13
|
+
* path) fall back to one lazily created process-default service with the
|
|
14
|
+
* exact default options, until C8 retires the compat surface.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
import { isAbsolute, join, relative, resolve, sep } from 'path';
|
|
20
|
+
import { SENSITIVE_FILE_PATTERNS } from '@capekai/types';
|
|
21
|
+
import type {
|
|
22
|
+
WorkspaceCapability,
|
|
23
|
+
WorkspaceCapabilityHost,
|
|
24
|
+
WorkspaceLike,
|
|
25
|
+
WorkspacePolicyOptions,
|
|
26
|
+
WorkspaceService,
|
|
27
|
+
} from './contracts';
|
|
28
|
+
|
|
29
|
+
export const BLOCKED_PATHS = [
|
|
30
|
+
'/etc/', '/usr/', '/bin/', '/sbin/', '/boot/', '/dev/',
|
|
31
|
+
'/proc/', '/sys/', '/root/',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export interface WorkspaceServiceCreateOptions {
|
|
35
|
+
id?: string;
|
|
36
|
+
/** Frozen composition-time options. When omitted (the process-default
|
|
37
|
+
* fallback), the exact pre-C6 defaults apply: the current blocked and
|
|
38
|
+
* sensitive constants and the process home directory. */
|
|
39
|
+
options?: WorkspacePolicyOptions;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultOptions(): WorkspacePolicyOptions {
|
|
43
|
+
return {
|
|
44
|
+
blockedPaths: BLOCKED_PATHS,
|
|
45
|
+
sensitivePatterns: SENSITIVE_FILE_PATTERNS,
|
|
46
|
+
homeDir: homedir(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Mandatory containment runtime (C6 step 6) ───────────────────────────
|
|
51
|
+
// The tool-runtime capability is constructed HERE, not by provider methods:
|
|
52
|
+
// a custom provider supplies only frozen options (blocked paths, sensitive
|
|
53
|
+
// patterns, home directory). Containment, lexical checks, and the
|
|
54
|
+
// capability algorithms are non-overridable runtime evidence.
|
|
55
|
+
|
|
56
|
+
export function isLexicallyContained(path: string, root: string): boolean {
|
|
57
|
+
const resolvedPath = resolve(path);
|
|
58
|
+
const resolvedRoot = resolve(root);
|
|
59
|
+
return resolvedPath === resolvedRoot
|
|
60
|
+
|| resolvedPath.startsWith(resolvedRoot.endsWith(sep) ? resolvedRoot : `${resolvedRoot}${sep}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isSensitiveWith(patterns: readonly string[], candidate: string): boolean {
|
|
64
|
+
const lower = candidate.toLowerCase();
|
|
65
|
+
return patterns.some((pattern) => lower.includes(pattern));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isBlockedWith(blockedPaths: readonly string[], candidate: string): boolean {
|
|
69
|
+
const resolvedPath = resolve(candidate);
|
|
70
|
+
return blockedPaths.some((blockedPath) => resolvedPath.startsWith(blockedPath));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createWorkspaceCapabilityWithOptions(
|
|
74
|
+
host: WorkspaceCapabilityHost,
|
|
75
|
+
options: WorkspacePolicyOptions,
|
|
76
|
+
): WorkspaceCapability {
|
|
77
|
+
const effectiveRoot = resolve(host.root || process.cwd());
|
|
78
|
+
const additionalRoots = (host.additionalRoots ?? []).map((path) => resolve(path));
|
|
79
|
+
const allowedRoots = (host.allowedRoots ?? []).map((path) => resolve(path));
|
|
80
|
+
|
|
81
|
+
function resolvePath(path: string): string {
|
|
82
|
+
if (path === '~' || path.startsWith('~/')) {
|
|
83
|
+
return join(options.homeDir, path.slice(1));
|
|
84
|
+
}
|
|
85
|
+
if (isAbsolute(path)) {
|
|
86
|
+
return resolve(path);
|
|
87
|
+
}
|
|
88
|
+
return resolve(effectiveRoot, path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
effectiveRoot,
|
|
93
|
+
additionalRoots,
|
|
94
|
+
allowedRoots,
|
|
95
|
+
tempDir: host.tempDir,
|
|
96
|
+
resolvePath,
|
|
97
|
+
isWithinWorkspace(path: string): boolean {
|
|
98
|
+
const resolvedPath = resolvePath(path);
|
|
99
|
+
return [effectiveRoot, ...additionalRoots]
|
|
100
|
+
.some((root) => isLexicallyContained(resolvedPath, root));
|
|
101
|
+
},
|
|
102
|
+
isSensitivePath(path: string): boolean {
|
|
103
|
+
return isSensitiveWith(options.sensitivePatterns, path);
|
|
104
|
+
},
|
|
105
|
+
isBlockedPath(path: string): boolean {
|
|
106
|
+
const resolvedPath = resolvePath(path);
|
|
107
|
+
return isBlockedWith(options.blockedPaths, resolvedPath);
|
|
108
|
+
},
|
|
109
|
+
getEnvironmentValue(key: string): string | undefined {
|
|
110
|
+
return host.getEnvironmentValue?.(key) ?? process.env[key];
|
|
111
|
+
},
|
|
112
|
+
async addWorkspacePath(path: string): Promise<boolean> {
|
|
113
|
+
if (!host.addAdditionalRoot) return false;
|
|
114
|
+
return host.addAdditionalRoot(resolve(path));
|
|
115
|
+
},
|
|
116
|
+
async removeWorkspacePath(path: string): Promise<boolean> {
|
|
117
|
+
if (!host.removeAdditionalRoot) return false;
|
|
118
|
+
return host.removeAdditionalRoot(resolve(path));
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The C6 default provider wrapping the exact pre-C6 behavior. */
|
|
124
|
+
export function createWorkspaceService(
|
|
125
|
+
createOptions: WorkspaceServiceCreateOptions = {},
|
|
126
|
+
): WorkspaceService {
|
|
127
|
+
const id = createOptions.id ?? 'workspace.default';
|
|
128
|
+
const options = createOptions.options ?? defaultOptions();
|
|
129
|
+
|
|
130
|
+
const service: WorkspaceService = {
|
|
131
|
+
id,
|
|
132
|
+
options,
|
|
133
|
+
|
|
134
|
+
expandPath(inputPath: string): string {
|
|
135
|
+
let expanded = inputPath;
|
|
136
|
+
if (expanded.startsWith('~/') || expanded === '~') {
|
|
137
|
+
expanded = join(options.homeDir, expanded.slice(1));
|
|
138
|
+
}
|
|
139
|
+
return resolve(expanded);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
resolvePathFor(path: string, workspacePath: string): string {
|
|
143
|
+
if (path.startsWith('~/') || path === '~') {
|
|
144
|
+
return join(options.homeDir, path.slice(1));
|
|
145
|
+
}
|
|
146
|
+
if (isAbsolute(path)) {
|
|
147
|
+
return resolve(path);
|
|
148
|
+
}
|
|
149
|
+
return resolve(workspacePath, path);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
isPathWithinWorkspace(
|
|
153
|
+
targetPath: string,
|
|
154
|
+
workspacePath: string,
|
|
155
|
+
additionalPaths: string[] = [],
|
|
156
|
+
): boolean {
|
|
157
|
+
const resolvedPath = service.resolvePathFor(targetPath, workspacePath);
|
|
158
|
+
const allAllowed = [resolve(workspacePath), ...additionalPaths.map((p) => resolve(p))];
|
|
159
|
+
return allAllowed.some((allowed) => {
|
|
160
|
+
const relativePath = relative(allowed, resolvedPath);
|
|
161
|
+
return relativePath === ''
|
|
162
|
+
|| (relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
isPathInside(child: string, parent: string): boolean {
|
|
167
|
+
if (child === parent) return true;
|
|
168
|
+
if (parent === sep) return true;
|
|
169
|
+
return child.startsWith(parent + sep);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
isInsideUnselectedAdditionalRoot(
|
|
173
|
+
candidate: string,
|
|
174
|
+
selectedRoot: string,
|
|
175
|
+
additionalPaths: string[],
|
|
176
|
+
): boolean {
|
|
177
|
+
return additionalPaths.some((path) => {
|
|
178
|
+
const additionalRoot = resolve(path);
|
|
179
|
+
return additionalRoot !== selectedRoot && service.isPathInside(candidate, additionalRoot);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
resolveCandidatePath(root: string, inputPath: string): string {
|
|
184
|
+
const normalized = inputPath.replace(/\\/g, '/');
|
|
185
|
+
return isAbsolute(normalized) ? resolve(normalized) : resolve(join(root, normalized));
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
resolveRootForQuery(
|
|
189
|
+
workspace: WorkspaceLike,
|
|
190
|
+
rootQuery?: string,
|
|
191
|
+
): { root: string; isMain: boolean } {
|
|
192
|
+
const main = resolve(workspace.path);
|
|
193
|
+
if (!rootQuery) return { root: main, isMain: true };
|
|
194
|
+
const resolvedRoot = resolve(rootQuery);
|
|
195
|
+
if (resolvedRoot === main) return { root: main, isMain: true };
|
|
196
|
+
for (const p of workspace.additionalPaths) {
|
|
197
|
+
if (resolve(p) === resolvedRoot) return { root: resolvedRoot, isMain: false };
|
|
198
|
+
}
|
|
199
|
+
return { root: main, isMain: true };
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
selectEditableRoot(
|
|
203
|
+
workspace: WorkspaceLike,
|
|
204
|
+
rootQuery?: string,
|
|
205
|
+
): { root: string; valid: boolean } {
|
|
206
|
+
const mainRoot = resolve(workspace.path);
|
|
207
|
+
if (!rootQuery) return { root: mainRoot, valid: true };
|
|
208
|
+
|
|
209
|
+
const requestedRoot = resolve(rootQuery);
|
|
210
|
+
const allowedRoots = [mainRoot, ...workspace.additionalPaths.map((path) => resolve(path))];
|
|
211
|
+
if (!allowedRoots.includes(requestedRoot)) {
|
|
212
|
+
return { root: mainRoot, valid: false };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { root: requestedRoot, valid: true };
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
isLexicallyContained,
|
|
219
|
+
|
|
220
|
+
isSensitivePath(path: string): boolean {
|
|
221
|
+
const lower = path.toLowerCase();
|
|
222
|
+
return options.sensitivePatterns.some((pattern) => lower.includes(pattern));
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
isBlockedPath(path: string): boolean {
|
|
226
|
+
const resolvedPath = resolve(path);
|
|
227
|
+
return options.blockedPaths.some((blockedPath) => resolvedPath.startsWith(blockedPath));
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
createCapability: (host: WorkspaceCapabilityHost): WorkspaceCapability =>
|
|
231
|
+
createWorkspaceCapabilityWithOptions(host, options),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return service;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const scopedService = new AsyncLocalStorage<WorkspaceService>();
|
|
238
|
+
let processDefaultService: WorkspaceService | undefined;
|
|
239
|
+
|
|
240
|
+
/** Resolves the service seeded for the active agent scope, falling back to
|
|
241
|
+
* one lazily created process-default service for consumers that run outside
|
|
242
|
+
* a composed scope (the current Jean2 server path). The process default
|
|
243
|
+
* carries the exact pre-C6 option defaults. */
|
|
244
|
+
export function getWorkspaceService(): WorkspaceService {
|
|
245
|
+
return scopedService.getStore()
|
|
246
|
+
?? (processDefaultService ??= createWorkspaceService({ id: 'workspace.process-default' }));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Builds the tool-runtime capability over the active workspace policy. */
|
|
250
|
+
export function createWorkspaceCapability(host: WorkspaceCapabilityHost): WorkspaceCapability {
|
|
251
|
+
return createWorkspaceCapabilityWithOptions(host, getWorkspaceService().options);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Seeds a service for the callback duration. `enterAgentScope` seeds the
|
|
255
|
+
* composed agent scope's service here. */
|
|
256
|
+
export function withWorkspaceService<T>(service: WorkspaceService, callback: () => T): T {
|
|
257
|
+
return scopedService.run(service, callback);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Test-only reset of the lazily created process default. Exported from this
|
|
261
|
+
* module only; no package subpath re-exports it. */
|
|
262
|
+
export function resetDefaultWorkspaceServiceForTests(): void {
|
|
263
|
+
processDefaultService = undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Compatibility free functions over the scoped service ────────────────
|
|
267
|
+
// The exact pre-C6 names from the Jean2 workspace domain, exported through
|
|
268
|
+
// the compat barrel so the server `WorkspacePathPolicyPort` adapter keeps
|
|
269
|
+
// one ownership of the algorithms.
|
|
270
|
+
|
|
271
|
+
/** Expands `~` to the active service's frozen home directory and resolves. */
|
|
272
|
+
export function expandPath(inputPath: string): string {
|
|
273
|
+
return getWorkspaceService().expandPath(inputPath);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Server-style resolution over the active service. */
|
|
277
|
+
export function resolvePath(path: string, workspacePath: string): string {
|
|
278
|
+
return getWorkspaceService().resolvePathFor(path, workspacePath);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function isPathWithinWorkspace(
|
|
282
|
+
targetPath: string,
|
|
283
|
+
workspacePath: string,
|
|
284
|
+
additionalPaths: string[] = [],
|
|
285
|
+
): boolean {
|
|
286
|
+
return getWorkspaceService().isPathWithinWorkspace(targetPath, workspacePath, additionalPaths);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function isPathInside(child: string, parent: string): boolean {
|
|
290
|
+
return getWorkspaceService().isPathInside(child, parent);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function isInsideUnselectedAdditionalRoot(
|
|
294
|
+
candidate: string,
|
|
295
|
+
selectedRoot: string,
|
|
296
|
+
additionalPaths: string[],
|
|
297
|
+
): boolean {
|
|
298
|
+
return getWorkspaceService().isInsideUnselectedAdditionalRoot(
|
|
299
|
+
candidate,
|
|
300
|
+
selectedRoot,
|
|
301
|
+
additionalPaths,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function resolveCandidatePath(root: string, inputPath: string): string {
|
|
306
|
+
return getWorkspaceService().resolveCandidatePath(root, inputPath);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function resolveRootForQuery(
|
|
310
|
+
workspace: WorkspaceLike,
|
|
311
|
+
rootQuery?: string,
|
|
312
|
+
): { root: string; isMain: boolean } {
|
|
313
|
+
return getWorkspaceService().resolveRootForQuery(workspace, rootQuery);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function selectEditableRoot(
|
|
317
|
+
workspace: WorkspaceLike,
|
|
318
|
+
rootQuery?: string,
|
|
319
|
+
): { root: string; valid: boolean } {
|
|
320
|
+
return getWorkspaceService().selectEditableRoot(workspace, rootQuery);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export type {
|
|
324
|
+
WorkspacePolicy,
|
|
325
|
+
WorkspacePolicyOptions,
|
|
326
|
+
WorkspaceService,
|
|
327
|
+
} from './contracts';
|