@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.
Files changed (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. 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';