diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index fa43204eb..25dcd1631 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -795,6 +795,211 @@ test('optional dependency ownership rejects undeclared direct feature owners', a ); }); +test('services-core capability profiles keep heavy owners out of the empty profile', async () => { + const { coreClosedFeatureProfileRules } = await import( + './core-boundaries/rules/feature-rules.mjs' + ); + const { dependencyProfileRules } = await import( + './core-boundaries/rules/crate-rules.mjs' + ); + const { requiredContentRules } = await import( + './core-boundaries/rules/source/required-rules.mjs' + ); + const serviceManifest = 'src/crates/services/services-core/Cargo.toml'; + const profiles = new Map( + coreClosedFeatureProfileRules + .filter((rule) => rule.manifestPath === serviceManifest) + .map((rule) => [rule.featureName, rule.requiredFeatureRefs]), + ); + + assert.deepEqual(profiles.get('filesystem'), [ + 'dep:base64', + 'dep:chrono', + 'dep:ignore', + 'dep:sha2', + 'tokio/fs', + ]); + assert.deepEqual(profiles.get('local-storage'), [ + 'dep:bitfun-core-types', + 'dep:bitfun-events', + 'dep:chrono', + 'dep:fs2', + 'dep:libc', + 'dep:sha2', + 'dep:windows', + 'tokio/fs', + 'tokio/sync', + 'windows/Win32_Foundation', + 'windows/Win32_Storage_FileSystem', + ]); + assert.deepEqual(profiles.get('process-runtime'), [ + 'dep:libc', + 'dep:which', + 'dep:win32job', + 'dep:windows', + 'tokio/io-util', + 'tokio/process', + 'windows/Win32_Foundation', + 'windows/Win32_System_Diagnostics_ToolHelp', + 'windows/Win32_System_Threading', + ]); + assert.deepEqual(profiles.get('workspace-instructions'), [ + 'dep:globset', + 'tokio/fs', + 'tokio/io-util', + ]); + assert.deepEqual(profiles.get('lsp'), [ + 'dep:anyhow', + 'dep:bitfun-core-types', + 'dep:notify', + 'dep:zip', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ]); + assert.deepEqual(profiles.get('workspace-runtime'), [ + 'dep:anyhow', + 'dep:async-trait', + 'dep:bitfun-runtime-ports', + 'dep:dunce', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ]); + + const defaultProfile = dependencyProfileRules.find( + (rule) => rule.crateName === 'services-core', + ); + for (const dependency of [ + 'base64', + 'bitfun-core-types', + 'bitfun-events', + 'chrono', + 'fs2', + 'globset', + 'ignore', + 'libc', + 'sha2', + 'which', + 'win32job', + 'windows', + ]) { + assert.ok( + defaultProfile?.forbiddenNonOptionalDeps.includes(dependency), + `services-core empty profile must reject ambient ${dependency}`, + ); + } + + const sourceRule = requiredContentRules.find( + (rule) => rule.path === 'src/crates/services/services-core/src/lib.rs', + ); + const sourceContracts = sourceRule?.patterns.map((pattern) => pattern.regex.source).join('\n') ?? ''; + for (const moduleName of [ + 'filesystem', + 'json_store', + 'managed_runtime', + 'persistence', + 'process_manager', + 'process_tree', + 'session', + 'session_usage', + 'storage_cleanup', + 'system', + 'token_usage', + 'workspace_instructions', + ]) { + assert.match( + sourceContracts, + new RegExp(`pub mod ${moduleName}`), + `services-core source rule must protect the ${moduleName} capability gate`, + ); + } +}); + +test('services-core Tokio capabilities stay owner-scoped', () => { + const invalidPackage = { + name: 'bitfun-services-core', + manifest_path: 'src/crates/services/services-core/Cargo.toml', + dependencies: [ + { + name: 'tokio', + kind: null, + optional: false, + features: ['fs', 'io-util', 'process', 'rt', 'sync', 'time'], + }, + ], + features: { + filesystem: [], + 'local-storage': [], + 'process-runtime': [], + 'workspace-instructions': [], + lsp: [], + 'workspace-runtime': [], + }, + }; + + const messages = findTokioDependencyFeatureViolations([invalidPackage]).map( + (violation) => violation.message, + ); + assert.ok( + messages.some((message) => message.includes('unexpected base Tokio capabilities')), + 'services-core must reject ambient fs/io/process/sync Tokio capabilities', + ); + assert.ok( + messages.some((message) => message.includes('filesystem missing effective Tokio capabilities: fs')), + 'services-core must require filesystem to own tokio/fs', + ); + assert.ok( + messages.some((message) => message.includes('lsp missing effective Tokio capabilities')), + 'services-core must require lsp to declare its complete effective Tokio profile', + ); +}); + +test('services-core Windows API capabilities stay feature-owned', async () => { + const { findServicesCorePlatformDependencyFeatureViolations } = await import( + './core-boundaries/cargo-dependency-boundaries.mjs' + ); + assert.equal( + typeof findServicesCorePlatformDependencyFeatureViolations, + 'function', + 'Cargo boundary checker must expose the services-core platform dependency policy', + ); + const packageWithAmbientWindowsApis = { + name: 'bitfun-services-core', + manifest_path: 'src/crates/services/services-core/Cargo.toml', + dependencies: [ + { + name: 'windows', + kind: null, + optional: true, + target: 'cfg(windows)', + features: ['Win32_Storage_FileSystem', 'Win32_System_Threading'], + }, + ], + }; + + const violations = findServicesCorePlatformDependencyFeatureViolations([ + packageWithAmbientWindowsApis, + ]); + assert.equal(violations.length, 1); + assert.match( + violations[0].message, + /windows API capabilities must be selected by services-core owner features/, + ); + + assert.deepEqual( + findServicesCorePlatformDependencyFeatureViolations([ + { + ...packageWithAmbientWindowsApis, + dependencies: [{ ...packageWithAmbientWindowsApis.dependencies[0], features: [] }], + }, + ]), + [], + ); +}); + test('closed feature profiles reject product-full hidden behind a child feature', async () => { const { unexpectedReachableLocalFeatures } = await import( './core-boundaries/manifest-feature-helpers.mjs' diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index cb42ac30e..4a7a24f03 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -80,6 +80,16 @@ const SERVICES_INTEGRATIONS_TOKIO_FEATURES = new Map([ ['script-tool-runtime', ['io-util', 'process', 'rt', 'sync', 'time']], ]); +const SERVICES_CORE_TOKIO_FEATURES = new Map([ + ['filesystem', ['fs']], + ['local-storage', ['fs', 'sync']], + ['process-runtime', ['io-util', 'process']], + ['workspace-instructions', ['fs', 'io-util']], + ['lsp', ['fs', 'io-util', 'process', 'sync']], + ['workspace-runtime', ['fs', 'io-util', 'process', 'sync']], +]); +const SERVICES_CORE_BASE_TOKIO_FEATURES = ['rt', 'time']; + // The installer is an excluded standalone workspace with its own Rust checks // and packaging lifecycle; this policy governs the root product workspace. const TOKIO_DEPENDENCY_POLICY_EXCLUDED_PACKAGES = new Set(['bitfun-installer']); @@ -105,11 +115,11 @@ function effectiveTokioCapabilities(feature, featureGraph, visiting = new Set()) return capabilities; } -export function findServicesIntegrationsTokioFeatureViolations(pkg) { +function findOwnedTokioFeatureViolations(pkg, ownerProfiles) { const violations = []; const featureGraph = pkg.features ?? {}; - for (const [feature, expectedCapabilities] of SERVICES_INTEGRATIONS_TOKIO_FEATURES) { + for (const [feature, expectedCapabilities] of ownerProfiles) { if (!Object.hasOwn(featureGraph, feature)) { violations.push({ path: pkg.manifest_path, @@ -140,7 +150,7 @@ export function findServicesIntegrationsTokioFeatureViolations(pkg) { } for (const [feature, values] of Object.entries(featureGraph)) { - if (SERVICES_INTEGRATIONS_TOKIO_FEATURES.has(feature)) { + if (ownerProfiles.has(feature)) { continue; } if (values.some((value) => value.startsWith('tokio/'))) { @@ -155,6 +165,37 @@ export function findServicesIntegrationsTokioFeatureViolations(pkg) { return violations; } +export function findServicesIntegrationsTokioFeatureViolations(pkg) { + return findOwnedTokioFeatureViolations(pkg, SERVICES_INTEGRATIONS_TOKIO_FEATURES); +} + +export function findServicesCoreTokioFeatureViolations(pkg) { + return findOwnedTokioFeatureViolations(pkg, SERVICES_CORE_TOKIO_FEATURES); +} + +export function findServicesCorePlatformDependencyFeatureViolations(packages) { + const violations = []; + + for (const pkg of packages) { + if (pkg.name !== 'bitfun-services-core') { + continue; + } + for (const dependency of pkg.dependencies ?? []) { + if (dependency.name !== 'windows' || (dependency.features ?? []).length === 0) { + continue; + } + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + 'windows API capabilities must be selected by services-core owner features, not the dependency declaration', + }); + } + } + + return violations; +} + export function findTokioDependencyFeatureViolations(packages) { const violations = []; @@ -177,7 +218,29 @@ export function findTokioDependencyFeatureViolations(packages) { const featureOwnedIntegrationRuntime = pkg.name === 'bitfun-services-integrations' && (dependency.kind ?? null) === null; - if (features.length === 0 && !featureOwnedIntegrationRuntime) { + const featureOwnedServicesCoreRuntime = + pkg.name === 'bitfun-services-core' + && (dependency.kind ?? null) === null; + if (featureOwnedServicesCoreRuntime) { + const actual = [...features].sort(); + const expected = [...SERVICES_CORE_BASE_TOKIO_FEATURES].sort(); + const missing = expected.filter((feature) => !actual.includes(feature)); + const unexpected = actual.filter((feature) => !expected.includes(feature)); + if (missing.length > 0) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} missing base Tokio capabilities: ${missing.join(', ')}`, + }); + } + if (unexpected.length > 0) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: `${pkg.name} has unexpected base Tokio capabilities: ${unexpected.join(', ')}`, + }); + } + } else if (features.length === 0 && !featureOwnedIntegrationRuntime) { violations.push({ path: pkg.manifest_path, line: 1, @@ -189,6 +252,9 @@ export function findTokioDependencyFeatureViolations(packages) { if (pkg.name === 'bitfun-services-integrations') { violations.push(...findServicesIntegrationsTokioFeatureViolations(pkg)); } + if (pkg.name === 'bitfun-services-core') { + violations.push(...findServicesCoreTokioFeatureViolations(pkg)); + } } return violations; @@ -728,6 +794,7 @@ export function checkCargoDependencyBoundaries({ root, crateLayoutRules }) { ), ...findFeatureGatedTestTargetViolations(packages), ...findTokioDependencyFeatureViolations(packages), + ...findServicesCorePlatformDependencyFeatureViolations(packages), ]; } diff --git a/scripts/core-boundaries/rules/crate-rules.mjs b/scripts/core-boundaries/rules/crate-rules.mjs index 0c782ffcf..4c68435fc 100644 --- a/scripts/core-boundaries/rules/crate-rules.mjs +++ b/scripts/core-boundaries/rules/crate-rules.mjs @@ -460,12 +460,24 @@ export const dependencyProfileRules = [ forbiddenNonOptionalDeps: [ 'anyhow', 'async-trait', + 'base64', + 'bitfun-core-types', + 'bitfun-events', 'bitfun-runtime-ports', + 'chrono', 'dunce', + 'fs2', 'git2', + 'globset', + 'ignore', + 'libc', 'notify', 'rusqlite', 'serde_yaml', + 'sha2', + 'which', + 'win32job', + 'windows', 'zip', ], }, diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index 18014986e..e04f9f074 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -8,12 +8,33 @@ export const optionalDependencyFeatureOwnerRules = [ dependencies: [ { depName: 'anyhow', ownerFeatures: ['dispatch-workspace', 'lsp', 'workspace-runtime'] }, { depName: 'async-trait', ownerFeatures: ['permission', 'workspace-runtime'] }, + { depName: 'base64', ownerFeatures: ['filesystem'] }, + { depName: 'bitfun-core-types', ownerFeatures: ['local-storage', 'lsp'] }, + { depName: 'bitfun-events', ownerFeatures: ['local-storage'] }, { depName: 'bitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime'] }, + { depName: 'chrono', ownerFeatures: ['filesystem', 'local-storage'] }, { depName: 'dunce', ownerFeatures: ['runtime-ownership', 'workspace-identity', 'workspace-runtime'] }, + { depName: 'fs2', ownerFeatures: ['local-storage', 'runtime-ownership'] }, { depName: 'git2', ownerFeatures: ['session-git'] }, + { depName: 'globset', ownerFeatures: ['workspace-instructions'] }, + { depName: 'ignore', ownerFeatures: ['filesystem'] }, + { depName: 'libc', ownerFeatures: ['local-storage', 'process-runtime'] }, { depName: 'notify', ownerFeatures: ['lsp'] }, { depName: 'rusqlite', ownerFeatures: ['permission'] }, { depName: 'serde_yaml', ownerFeatures: ['markdown'] }, + { + depName: 'sha2', + ownerFeatures: [ + 'dispatch-workspace', + 'filesystem', + 'local-storage', + 'runtime-ownership', + 'workspace-identity', + ], + }, + { depName: 'which', ownerFeatures: ['process-runtime'] }, + { depName: 'win32job', ownerFeatures: ['process-runtime'] }, + { depName: 'windows', ownerFeatures: ['local-storage', 'process-runtime'] }, { depName: 'zip', ownerFeatures: ['lsp'] }, ], }, @@ -98,7 +119,7 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'bitfun-runtime-ports', ownerFeatures: ['git', 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'script-tool-runtime'] }, { depName: 'bitfun-services-core', - ownerFeatures: ['browser-control', 'git', 'hook-import', 'mcp', 'miniapp-runtime', 'process-tree', 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', 'review-platform', 'workspace-search'], + ownerFeatures: ['browser-control', 'git', 'hook-import', 'mcp', 'miniapp-runtime', 'process-tree', 'remote-connect', 'remote-ssh', 'review-platform', 'workspace-search'], }, { depName: 'bzip2', ownerFeatures: ['speech'] }, { depName: 'chrono', ownerFeatures: ['debug-log', 'git', 'miniapp-market', 'remote-connect', 'remote-ssh-concrete', 'review-platform', 'speech'] }, @@ -168,8 +189,11 @@ export const coreProductFullFeatureAssemblyRule = { 'announcement', 'dispatch-store', 'file-watch', + 'filesystem', 'git', 'lsp', + 'local-storage', + 'process-runtime', 'remote-workspace', 'review-platform', 'ssh-remote', @@ -191,27 +215,130 @@ export const coreClosedFeatureProfileRules = [ exact: true, reason: 'services-core default profile must stay empty so consumers select capabilities explicitly', }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'filesystem', + requiredFeatureRefs: ['dep:base64', 'dep:chrono', 'dep:ignore', 'dep:sha2', 'tokio/fs'], + exact: true, + reason: 'services-core filesystem must own only local file operations and recursive search dependencies', + }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'local-storage', + requiredFeatureRefs: [ + 'dep:bitfun-core-types', + 'dep:bitfun-events', + 'dep:chrono', + 'dep:fs2', + 'dep:libc', + 'dep:sha2', + 'dep:windows', + 'tokio/fs', + 'tokio/sync', + 'windows/Win32_Foundation', + 'windows/Win32_Storage_FileSystem', + ], + exact: true, + reason: 'services-core local-storage must own durable JSON, session, usage, and cleanup primitives', + }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'process-runtime', + requiredFeatureRefs: [ + 'dep:libc', + 'dep:which', + 'dep:win32job', + 'dep:windows', + 'tokio/io-util', + 'tokio/process', + 'windows/Win32_Foundation', + 'windows/Win32_System_Diagnostics_ToolHelp', + 'windows/Win32_System_Threading', + ], + exact: true, + reason: 'services-core process-runtime must own command lookup and supervised child lifecycle dependencies', + }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'workspace-instructions', + requiredFeatureRefs: ['dep:globset', 'tokio/fs', 'tokio/io-util'], + exact: true, + reason: 'services-core workspace-instructions must own declarative instruction glob expansion only', + }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'lsp', + requiredFeatureRefs: [ + 'dep:anyhow', + 'dep:bitfun-core-types', + 'dep:notify', + 'dep:zip', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ], + exact: true, + reason: 'services-core lsp must compose process supervision with only its protocol and watcher capabilities', + }, + { + manifestPath: 'src/crates/services/services-core/Cargo.toml', + featureName: 'workspace-runtime', + requiredFeatureRefs: [ + 'dep:anyhow', + 'dep:async-trait', + 'dep:bitfun-runtime-ports', + 'dep:dunce', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ], + exact: true, + reason: 'services-core workspace-runtime must compose process supervision with local workspace IO and ports', + }, { manifestPath: 'src/crates/services/services-core/Cargo.toml', featureName: 'session-git', - requiredFeatureRefs: ['dep:git2'], + requiredFeatureRefs: ['local-storage', 'dep:git2'], exact: true, reason: 'services-core session-git must own only the libgit2-backed memory workspace capability', }, { manifestPath: 'src/crates/services/services-core/Cargo.toml', featureName: 'workspace-identity', - requiredFeatureRefs: ['dep:dunce'], + requiredFeatureRefs: ['dep:dunce', 'dep:sha2'], exact: true, reason: 'services-core workspace-identity must own only canonical workspace path identity support', }, { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'dispatch-store', - requiredFeatureRefs: [], + requiredFeatureRefs: ['local-storage'], exact: true, reason: 'bitfun-core dispatch-store must expose only the durable dispatch index facade', }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'filesystem', + requiredFeatureRefs: ['bitfun-services-core/filesystem'], + exact: true, + reason: 'bitfun-core filesystem must select only the concrete local filesystem owner', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'local-storage', + requiredFeatureRefs: ['bitfun-services-core/local-storage'], + exact: true, + reason: 'bitfun-core local-storage must select only reusable local persistence owners', + }, + { + manifestPath: 'src/crates/assembly/core/Cargo.toml', + featureName: 'process-runtime', + requiredFeatureRefs: ['bitfun-services-core/process-runtime'], + exact: true, + reason: 'bitfun-core process-runtime must select only managed process and local action owners', + }, { manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'lsp', @@ -231,8 +358,12 @@ export const coreClosedFeatureProfileRules = [ featureName: 'workspace-runtime', requiredFeatureRefs: [ 'dep:serde_yaml', + 'filesystem', + 'local-storage', + 'process-runtime', 'bitfun-services-core/markdown', 'bitfun-services-core/workspace-identity', + 'bitfun-services-core/workspace-instructions', 'bitfun-services-core/workspace-runtime', ], exact: true, @@ -242,6 +373,7 @@ export const coreClosedFeatureProfileRules = [ manifestPath: 'src/crates/assembly/core/Cargo.toml', featureName: 'workspace-watch', requiredFeatureRefs: ['workspace-runtime', 'dep:notify'], + allowedTransitiveFeatureRefs: ['filesystem', 'local-storage', 'process-runtime'], exact: true, reason: 'bitfun-core workspace-watch must extend only local workspace runtime with identity watching', }, @@ -253,6 +385,7 @@ export const coreClosedFeatureProfileRules = [ 'dep:bitfun-services-integrations', 'bitfun-services-integrations/remote-ssh', ], + allowedTransitiveFeatureRefs: ['filesystem', 'local-storage', 'process-runtime'], exact: true, reason: 'bitfun-core remote-workspace must add only the remote workspace service surface', }, @@ -300,7 +433,12 @@ export const coreClosedFeatureProfileRules = [ 'remote-workspace', 'bitfun-services-integrations/remote-ssh-concrete', ], - allowedTransitiveFeatureRefs: ['workspace-runtime'], + allowedTransitiveFeatureRefs: [ + 'filesystem', + 'local-storage', + 'process-runtime', + 'workspace-runtime', + ], exact: true, reason: 'bitfun-core ssh-remote must extend only the remote workspace surface with concrete SSH and must not pull product Dispatch assembly', diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index a050a2e2d..274166aac 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -1,6 +1,65 @@ // Boundary rules for source ownership, facades, and required owner content. export const requiredContentRules = [ + { + path: 'src/crates/services/services-core/src/lib.rs', + reason: + 'services-core must compile concrete service owners only through their declared capability features', + patterns: [ + { + regex: /#\[cfg\(any\(feature = "local-storage", feature = "runtime-ownership"\)\)\]\s*mod file_lock;/, + message: 'missing shared file lock owner source gate', + }, + { + regex: /#\[cfg\(feature = "filesystem"\)\]\s*pub mod filesystem;/, + message: 'missing filesystem capability source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod json_store;/, + message: 'missing local-storage JSON owner source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod persistence;/, + message: 'missing local-storage persistence owner source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod session;/, + message: 'missing local-storage session owner source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod session_usage;/, + message: 'missing local-storage session usage owner source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod storage_cleanup;/, + message: 'missing local-storage cleanup owner source gate', + }, + { + regex: /#\[cfg\(feature = "local-storage"\)\]\s*pub mod token_usage;/, + message: 'missing local-storage token usage owner source gate', + }, + { + regex: /#\[cfg\(feature = "process-runtime"\)\]\s*pub mod managed_runtime;/, + message: 'missing process-runtime managed runtime source gate', + }, + { + regex: /#\[cfg\(feature = "process-runtime"\)\]\s*pub mod process_manager;/, + message: 'missing process-runtime process manager source gate', + }, + { + regex: /#\[cfg\(feature = "process-runtime"\)\]\s*pub mod process_tree;/, + message: 'missing process-runtime process tree source gate', + }, + { + regex: /#\[cfg\(feature = "process-runtime"\)\]\s*pub mod system;/, + message: 'missing process-runtime local system source gate', + }, + { + regex: /#\[cfg\(feature = "workspace-instructions"\)\]\s*pub mod workspace_instructions;/, + message: 'missing workspace-instructions source gate', + }, + ], + }, { path: 'src/crates/services/services-core/src/persistence.rs', reason: @@ -145,7 +204,7 @@ export const requiredContentRules = [ { path: 'src/crates/services/services-core/tests/storage_owner_contracts.rs', reason: - 'services-core owner migrations must keep persistence, cleanup, workspace instruction, and token usage behavior contracts', + 'services-core local storage owner must keep persistence, cleanup, and token usage behavior contracts', patterns: [ { regex: /\bpersistence_service_keeps_atomic_json_shape_and_backups\b/, @@ -155,13 +214,24 @@ export const requiredContentRules = [ regex: /\bcleanup_service_deletes_old_temp_and_log_files_without_product_paths\b/, message: 'missing storage cleanup owner behavior regression', }, + { + regex: /\btoken_usage_service_persists_records_and_filters_subagents_by_default\b/, + message: 'missing token usage owner behavior regression', + }, + ], + }, + { + path: 'src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs', + reason: + 'services-core workspace instruction owner must keep local and declarative discovery behavior contracts', + patterns: [ { regex: /\bworkspace_instruction_files_reads_agents_then_claude_and_skips_empty_files\b/, message: 'missing workspace instruction owner behavior regression', }, { - regex: /\btoken_usage_service_persists_records_and_filters_subagents_by_default\b/, - message: 'missing token usage owner behavior regression', + regex: /\bopencode_project_instructions_support_local_files_and_globs_only\b/, + message: 'missing declarative workspace instruction behavior regression', }, ], }, diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index e23cbf4e1..287e4ac07 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -148,8 +148,11 @@ export function runManifestParserSelfTest({ 'announcement', 'dispatch-store', 'file-watch', + 'filesystem', 'git', 'lsp', + 'local-storage', + 'process-runtime', 'remote-workspace', 'review-platform', 'ssh-remote', @@ -175,9 +178,82 @@ export function runManifestParserSelfTest({ const servicesCoreManifest = 'src/crates/services/services-core/Cargo.toml'; const expectedClosedCoreProfiles = [ [servicesCoreManifest, 'default', []], - [servicesCoreManifest, 'session-git', ['dep:git2']], - [servicesCoreManifest, 'workspace-identity', ['dep:dunce']], - [coreManifest, 'dispatch-store', []], + [ + servicesCoreManifest, + 'filesystem', + ['dep:base64', 'dep:chrono', 'dep:ignore', 'dep:sha2', 'tokio/fs'], + ], + [ + servicesCoreManifest, + 'local-storage', + [ + 'dep:bitfun-core-types', + 'dep:bitfun-events', + 'dep:chrono', + 'dep:fs2', + 'dep:libc', + 'dep:sha2', + 'dep:windows', + 'tokio/fs', + 'tokio/sync', + 'windows/Win32_Foundation', + 'windows/Win32_Storage_FileSystem', + ], + ], + [ + servicesCoreManifest, + 'process-runtime', + [ + 'dep:libc', + 'dep:which', + 'dep:win32job', + 'dep:windows', + 'tokio/io-util', + 'tokio/process', + 'windows/Win32_Foundation', + 'windows/Win32_System_Diagnostics_ToolHelp', + 'windows/Win32_System_Threading', + ], + ], + [ + servicesCoreManifest, + 'workspace-instructions', + ['dep:globset', 'tokio/fs', 'tokio/io-util'], + ], + [ + servicesCoreManifest, + 'lsp', + [ + 'dep:anyhow', + 'dep:bitfun-core-types', + 'dep:notify', + 'dep:zip', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ], + ], + [ + servicesCoreManifest, + 'workspace-runtime', + [ + 'dep:anyhow', + 'dep:async-trait', + 'dep:bitfun-runtime-ports', + 'dep:dunce', + 'process-runtime', + 'tokio/fs', + 'tokio/io-util', + 'tokio/sync', + ], + ], + [servicesCoreManifest, 'session-git', ['local-storage', 'dep:git2']], + [servicesCoreManifest, 'workspace-identity', ['dep:dunce', 'dep:sha2']], + [coreManifest, 'dispatch-store', ['local-storage']], + [coreManifest, 'filesystem', ['bitfun-services-core/filesystem']], + [coreManifest, 'local-storage', ['bitfun-services-core/local-storage']], + [coreManifest, 'process-runtime', ['bitfun-services-core/process-runtime']], [coreManifest, 'lsp', ['dep:notify', 'bitfun-services-core/lsp']], [coreManifest, 'terminal', ['dep:terminal-core']], [ @@ -185,8 +261,12 @@ export function runManifestParserSelfTest({ 'workspace-runtime', [ 'dep:serde_yaml', + 'filesystem', + 'local-storage', + 'process-runtime', 'bitfun-services-core/markdown', 'bitfun-services-core/workspace-identity', + 'bitfun-services-core/workspace-instructions', 'bitfun-services-core/workspace-runtime', ], ], @@ -719,10 +799,31 @@ export function runManifestParserSelfTest({ } } const expectedServicesCoreOwners = new Map([ + ['base64', ['filesystem']], + ['bitfun-core-types', ['local-storage', 'lsp']], + ['bitfun-events', ['local-storage']], + ['chrono', ['filesystem', 'local-storage']], + ['fs2', ['local-storage', 'runtime-ownership']], ['git2', ['session-git']], + ['globset', ['workspace-instructions']], + ['ignore', ['filesystem']], + ['libc', ['local-storage', 'process-runtime']], ['notify', ['lsp']], ['rusqlite', ['permission']], ['serde_yaml', ['markdown']], + [ + 'sha2', + [ + 'dispatch-workspace', + 'filesystem', + 'local-storage', + 'runtime-ownership', + 'workspace-identity', + ], + ], + ['which', ['process-runtime']], + ['win32job', ['process-runtime']], + ['windows', ['local-storage', 'process-runtime']], ['zip', ['lsp']], ]); for (const [dependencyName, ownerFeatures] of expectedServicesCoreOwners) { @@ -735,6 +836,19 @@ export function runManifestParserSelfTest({ } } } + const servicesCoreOptionalOwnerDeps = new Set( + servicesCoreOptionalOwnerRule?.dependencies.map((dependency) => dependency.depName) ?? [], + ); + const servicesCoreDefaultProfile = dependencyProfileRules.find( + (rule) => rule.crateName === 'services-core', + ); + for (const dep of servicesCoreDefaultProfile?.forbiddenNonOptionalDeps ?? []) { + if (!servicesCoreOptionalOwnerDeps.has(dep)) { + throw new Error( + `services-core optional dependency owner rule must cover forbidden dependency ${dep}`, + ); + } + } const servicesOptionalOwnerDeps = new Set( servicesOptionalOwnerRule?.dependencies.map((dependency) => dependency.depName) ?? [], ); diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index d4d9bd4b5..3178f9a00 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -24,7 +24,7 @@ bitfun-agent-runtime = { path = "../../crates/execution/agent-runtime" } bitfun-agent-runtime-ipc = { path = "../../crates/adapters/agent-runtime-ipc" } bitfun-runtime-ports = { path = "../../crates/contracts/runtime-ports" } bitfun-runtime-services = { path = "../../crates/execution/runtime-services" } -bitfun-services-core = { path = "../../crates/services/services-core", default-features = false, features = ["dispatch-workspace", "runtime-ownership"] } +bitfun-services-core = { path = "../../crates/services/services-core", default-features = false, features = ["dispatch-workspace", "local-storage", "process-runtime", "runtime-ownership"] } bitfun-agent-tools = { path = "../../crates/execution/tool-contracts" } bitfun-product-domains = { path = "../../crates/contracts/product-domains", default-features = false, features = ["external-sources"] } diff --git a/src/crates/adapters/ai-adapters/Cargo.toml b/src/crates/adapters/ai-adapters/Cargo.toml index eca4915a4..1f0d1375b 100644 --- a/src/crates/adapters/ai-adapters/Cargo.toml +++ b/src/crates/adapters/ai-adapters/Cargo.toml @@ -47,7 +47,7 @@ zbus-secret-service-keyring-store = { workspace = true, optional = true } subscription-auth = [ "dep:apple-native-keyring-store", "dep:base64", - "dep:bitfun-services-core", + "bitfun-services-core/process-runtime", "dep:dirs", "dep:fs2", "dep:keyring-core", diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 52b5db851..163fff14d 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -167,7 +167,10 @@ product-full = [ "bitfun-services-core/runtime-ownership", "bitfun-services-core/session-git", "dispatch-store", + "filesystem", "lsp", + "local-storage", + "process-runtime", "remote-workspace", "terminal", "workspace-runtime", @@ -211,13 +214,20 @@ file-watch = ["bitfun-services-integrations/file-watch"] git = ["bitfun-services-integrations/git"] review-platform = ["bitfun-services-integrations/review-platform"] service-integrations = ["announcement", "file-watch", "git", "review-platform"] -dispatch-store = [] +dispatch-store = ["local-storage"] +filesystem = ["bitfun-services-core/filesystem"] +local-storage = ["bitfun-services-core/local-storage"] +process-runtime = ["bitfun-services-core/process-runtime"] lsp = ["dep:notify", "bitfun-services-core/lsp"] terminal = ["dep:terminal-core"] workspace-runtime = [ "dep:serde_yaml", + "filesystem", + "local-storage", + "process-runtime", "bitfun-services-core/markdown", "bitfun-services-core/workspace-identity", + "bitfun-services-core/workspace-instructions", "bitfun-services-core/workspace-runtime", ] workspace-watch = ["workspace-runtime", "dep:notify"] diff --git a/src/crates/assembly/core/src/infrastructure/mod.rs b/src/crates/assembly/core/src/infrastructure/mod.rs index 03cf23e91..55abf5945 100644 --- a/src/crates/assembly/core/src/infrastructure/mod.rs +++ b/src/crates/assembly/core/src/infrastructure/mod.rs @@ -8,7 +8,9 @@ pub mod app_paths; #[cfg(feature = "product-full")] pub mod debug_log; pub mod events; +#[cfg(feature = "filesystem")] pub mod filesystem; +#[cfg(feature = "local-storage")] pub mod storage; #[cfg(feature = "ai-adapter-runtime")] pub mod subscription_auth; @@ -18,6 +20,7 @@ pub use ai::AIClient; pub use app_paths::{get_path_manager_arc, try_get_path_manager_arc, PathManager, StorageLevel}; #[cfg(feature = "runtime-services")] pub use events::BackendEventManager; +#[cfg(feature = "filesystem")] pub use filesystem::{ BatchedFileSearchProgressSink, FileContentSearchOptions, FileInfo, FileNameSearchOptions, FileOperationOptions, FileOperationService, FileReadResult, FileSearchOutcome, diff --git a/src/crates/assembly/core/src/service/mod.rs b/src/crates/assembly/core/src/service/mod.rs index 8e9235627..25909783e 100644 --- a/src/crates/assembly/core/src/service/mod.rs +++ b/src/crates/assembly/core/src/service/mod.rs @@ -15,6 +15,7 @@ pub mod config; // Config management pub mod cron; // Scheduled jobs #[cfg(feature = "dispatch-store")] pub mod dispatch; // Outbound dispatch observer index and target contracts +#[cfg(feature = "filesystem")] pub mod filesystem; // FileSystem management #[cfg(feature = "git")] pub mod git; // Git service @@ -31,9 +32,11 @@ pub mod remote_connect; // Remote Connect (phone → desktop) pub mod remote_ssh; // Remote SSH (desktop → server) #[cfg(feature = "review-platform")] pub mod review_platform; // Pull request review platform adapters +#[cfg(feature = "process-runtime")] pub mod runtime; // Managed runtime and capability management #[cfg(feature = "product-full")] pub mod search; // Workspace search via managed flashgrep daemon +#[cfg(feature = "local-storage")] pub mod session; // Session persistence #[cfg(feature = "product-full")] pub mod session_usage; // Session runtime usage reports @@ -56,7 +59,9 @@ pub use terminal_core as terminal; // Re-export main components. #[cfg(feature = "announcement")] pub use announcement::{AnnouncementCard, AnnouncementScheduler, AnnouncementSchedulerRef}; -pub use bitfun_services_core::{diagnostics, diff, system}; +#[cfg(feature = "process-runtime")] +pub use bitfun_services_core::system; +pub use bitfun_services_core::{diagnostics, diff}; #[cfg(feature = "file-watch")] pub use bitfun_services_integrations::file_watch; #[cfg(feature = "workspace-runtime")] @@ -77,6 +82,7 @@ pub use file_watch::{ start_file_watch, stop_file_watch, FileWatchEvent, FileWatchEventKind, FileWatchService, FileWatcherConfig, }; +#[cfg(feature = "filesystem")] pub use filesystem::{DirectoryStats, FileSystemService, FileSystemServiceFactory}; #[cfg(feature = "git")] pub use git::GitService; @@ -96,6 +102,7 @@ pub use review_platform::{ ReviewPlatformPullRequestReviewTarget, ReviewPlatformRemote, ReviewPlatformRepositoryRef, ReviewPlatformService, ReviewPlatformThread, ReviewPlatformWorkspaceSnapshot, }; +#[cfg(feature = "process-runtime")] pub use runtime::{ResolvedCommand, RuntimeCommandCapability, RuntimeManager, RuntimeSource}; #[cfg(feature = "product-full")] pub use search::{ @@ -110,6 +117,7 @@ pub use search::{ }; #[cfg(feature = "product-full")] pub use snapshot::SnapshotService; +#[cfg(feature = "process-runtime")] pub use system::{ check_command, check_commands, run_command, run_command_simple, CheckCommandResult, CommandOutput, SystemError, diff --git a/src/crates/assembly/core/src/util/mod.rs b/src/crates/assembly/core/src/util/mod.rs index f7de39284..a19a350e8 100644 --- a/src/crates/assembly/core/src/util/mod.rs +++ b/src/crates/assembly/core/src/util/mod.rs @@ -5,6 +5,7 @@ pub mod errors; pub mod front_matter_markdown; pub mod json_extract; pub mod plain_output; +#[cfg(feature = "process-runtime")] pub use bitfun_services_core::process_manager; pub mod timing; pub mod token_counter; @@ -15,6 +16,7 @@ pub use errors::*; pub use front_matter_markdown::FrontMatterMarkdown; pub use json_extract::extract_json_from_ai_response; pub use plain_output::sanitize_plain_model_output; +#[cfg(feature = "process-runtime")] pub use process_manager::*; pub use timing::*; pub use token_counter::*; diff --git a/src/crates/services/services-core/AGENTS.md b/src/crates/services/services-core/AGENTS.md index 1aa70dbcd..9eeacf0f9 100644 --- a/src/crates/services/services-core/AGENTS.md +++ b/src/crates/services/services-core/AGENTS.md @@ -17,11 +17,19 @@ crate. runtime crates. - Prefer `bitfun-core-types` for shared DTOs and `bitfun-runtime-ports` for cross-layer traits. -- Keep dependency features explicit and keep `default = []`. Consumers enable +- Keep dependency features explicit and keep `default = []`. The coarse service + capability owners are `filesystem` (local file operations/search), + `local-storage` (JSON/session/usage persistence), `process-runtime` (command + lookup and supervised child lifecycle), and `workspace-instructions` + (declarative instruction discovery). Consumers enable those or the narrower `lsp`, `workspace-runtime`, `workspace-identity`, `runtime-ownership`, - `permission`, `dispatch-workspace`, `markdown`, or `session-git` only for the - owner behavior they use. In particular, session metadata consumers must not - compile libgit2 unless they use the memory-workspace baseline/diff API. + `permission`, `dispatch-workspace`, `markdown`, and `session-git` extensions + only for behavior they use. In particular, session metadata consumers must + not compile libgit2 unless they use the memory-workspace baseline/diff API. + Keep Tokio and platform API capabilities owner-scoped too: the empty profile + carries only Tokio runtime/time support, `lsp` and `workspace-runtime` + explicitly compose `process-runtime`, and Windows storage/process bindings + must not be enabled from one shared dependency feature union. - LSP manifest and protocol DTOs belong in `bitfun-core-types`; reusable LSP package, protocol, detection, debounce, watch, and process-manager helpers belong in `services-core`; product workspace state, event emission, global @@ -53,11 +61,16 @@ crate. ## Verification ```bash -cargo test -p bitfun-services-core --features lsp +cargo check -p bitfun-services-core --no-default-features +cargo check -p bitfun-services-core --no-default-features --features filesystem +cargo test -p bitfun-services-core --no-default-features --features local-storage --test session_metadata_contracts +cargo test -p bitfun-services-core --no-default-features --features process-runtime --test process_runtime_contracts +cargo test -p bitfun-services-core --no-default-features --features workspace-instructions --test declarative_workspace_instruction_contracts +cargo test -p bitfun-services-core --no-default-features --features lsp --test lsp_plugin_registry_contracts cargo test -p bitfun-services-core --no-default-features --features session-git memory_workspace cargo check -p bitfun-services-core --no-default-features --features workspace-identity -cargo test -p bitfun-services-core --features workspace-runtime workspace -cargo test -p bitfun-services-core --features runtime-ownership --test runtime_ownership_contracts +cargo test -p bitfun-services-core --no-default-features --features workspace-runtime workspace +cargo test -p bitfun-services-core --no-default-features --features runtime-ownership --test runtime_ownership_contracts node scripts/check-core-boundaries.mjs cargo check -p bitfun-core --features product-full ``` diff --git a/src/crates/services/services-core/Cargo.toml b/src/crates/services/services-core/Cargo.toml index 657417235..1121516b4 100644 --- a/src/crates/services/services-core/Cargo.toml +++ b/src/crates/services/services-core/Cargo.toml @@ -12,56 +12,95 @@ crate-type = ["rlib"] [dependencies] anyhow = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } -bitfun-core-types = { path = "../../contracts/core-types" } -bitfun-events = { path = "../../contracts/events" } +bitfun-core-types = { path = "../../contracts/core-types", optional = true } +bitfun-events = { path = "../../contracts/events", optional = true } bitfun-runtime-ports = { path = "../../contracts/runtime-ports", optional = true } -tokio = { workspace = true, features = ["fs", "io-util", "process", "rt", "sync", "time"] } +tokio = { workspace = true, features = ["rt", "time"] } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true, optional = true } -base64 = { workspace = true } -chrono = { workspace = true } +base64 = { workspace = true, optional = true } +chrono = { workspace = true, optional = true } git2 = { workspace = true, optional = true } dunce = { workspace = true, optional = true } zip = { workspace = true, optional = true } thiserror = { workspace = true } log = { workspace = true } -fs2 = { workspace = true } +fs2 = { workspace = true, optional = true } notify = { workspace = true, optional = true } -ignore = { workspace = true } -globset = { workspace = true } -sha2 = { workspace = true } -which = { workspace = true } +ignore = { workspace = true, optional = true } +globset = { workspace = true, optional = true } +sha2 = { workspace = true, optional = true } +which = { workspace = true, optional = true } similar = { workspace = true } regex = { workspace = true } rusqlite = { version = "0.32", features = ["bundled"], optional = true } [target.'cfg(windows)'.dependencies] -win32job = { workspace = true } -windows = { workspace = true, features = [ - "Win32_Foundation", - "Win32_Storage_FileSystem", - "Win32_System_Diagnostics_ToolHelp", - "Win32_System_Threading", -] } +win32job = { workspace = true, optional = true } +windows = { workspace = true, optional = true } # Keep libgit2 self-contained on Unix, matching the product assembly dependency. [target.'cfg(not(windows))'.dependencies] git2 = { workspace = true, features = ["vendored-openssl"], optional = true } [target.'cfg(unix)'.dependencies] -libc = { workspace = true } +libc = { workspace = true, optional = true } [features] default = [] -lsp = ["dep:anyhow", "dep:notify", "dep:zip"] +filesystem = ["dep:base64", "dep:chrono", "dep:ignore", "dep:sha2", "tokio/fs"] +local-storage = [ + "dep:bitfun-core-types", + "dep:bitfun-events", + "dep:chrono", + "dep:fs2", + "dep:libc", + "dep:sha2", + "dep:windows", + "tokio/fs", + "tokio/sync", + "windows/Win32_Foundation", + "windows/Win32_Storage_FileSystem", +] +process-runtime = [ + "dep:libc", + "dep:which", + "dep:win32job", + "dep:windows", + "tokio/io-util", + "tokio/process", + "windows/Win32_Foundation", + "windows/Win32_System_Diagnostics_ToolHelp", + "windows/Win32_System_Threading", +] +workspace-instructions = ["dep:globset", "tokio/fs", "tokio/io-util"] +lsp = [ + "dep:anyhow", + "dep:bitfun-core-types", + "dep:notify", + "dep:zip", + "process-runtime", + "tokio/fs", + "tokio/io-util", + "tokio/sync", +] markdown = ["dep:serde_yaml"] -workspace-runtime = ["dep:anyhow", "dep:async-trait", "dep:bitfun-runtime-ports", "dep:dunce"] -workspace-identity = ["dep:dunce"] -runtime-ownership = ["dep:dunce"] +workspace-runtime = [ + "dep:anyhow", + "dep:async-trait", + "dep:bitfun-runtime-ports", + "dep:dunce", + "process-runtime", + "tokio/fs", + "tokio/io-util", + "tokio/sync", +] +workspace-identity = ["dep:dunce", "dep:sha2"] +runtime-ownership = ["dep:dunce", "dep:fs2", "dep:sha2"] permission = ["dep:async-trait", "dep:bitfun-runtime-ports", "dep:rusqlite", "bitfun-runtime-ports/permission"] -dispatch-workspace = ["dep:anyhow"] -session-git = ["dep:git2"] +dispatch-workspace = ["dep:anyhow", "dep:sha2"] +session-git = ["local-storage", "dep:git2"] [dev-dependencies] filetime = { workspace = true } @@ -72,6 +111,14 @@ tokio = { workspace = true, features = ["macros"] } name = "markdown_owner_contracts" required-features = ["markdown"] +[[test]] +name = "declarative_workspace_instruction_contracts" +required-features = ["workspace-instructions"] + +[[test]] +name = "json_store_contracts" +required-features = ["local-storage"] + [[test]] name = "lsp_plugin_registry_contracts" required-features = ["lsp"] @@ -90,10 +137,43 @@ required-features = ["permission"] [[test]] name = "workspace_instruction_contracts" -required-features = ["workspace-runtime"] +required-features = ["workspace-instructions", "workspace-runtime"] [[test]] name = "session_write_lock_contracts" +required-features = ["local-storage"] + +[[test]] +name = "process_runtime_contracts" +required-features = ["process-runtime"] + +[[test]] +name = "session_contracts" +required-features = ["local-storage"] + +[[test]] +name = "session_layout_contracts" +required-features = ["local-storage"] + +[[test]] +name = "session_metadata_contracts" +required-features = ["local-storage"] + +[[test]] +name = "session_page_contracts" +required-features = ["local-storage"] + +[[test]] +name = "session_usage_contracts" +required-features = ["local-storage"] + +[[test]] +name = "storage_owner_contracts" +required-features = ["local-storage"] + +[[test]] +name = "token_usage_contracts" +required-features = ["local-storage"] [lints] workspace = true diff --git a/src/crates/services/services-core/src/file_lock.rs b/src/crates/services/services-core/src/file_lock.rs index e51a7f354..551dc7fc5 100644 --- a/src/crates/services/services-core/src/file_lock.rs +++ b/src/crates/services/services-core/src/file_lock.rs @@ -19,6 +19,7 @@ pub(crate) enum FileLockError { } impl FileLock { + #[cfg(feature = "local-storage")] pub(crate) fn acquire(path: &Path, mode: FileLockMode) -> Result { let file = open_lock_file(path)?; match mode { diff --git a/src/crates/services/services-core/src/lib.rs b/src/crates/services/services-core/src/lib.rs index 170af05ba..255d5d14c 100644 --- a/src/crates/services/services-core/src/lib.rs +++ b/src/crates/services/services-core/src/lib.rs @@ -9,8 +9,11 @@ pub mod diff; pub mod dispatch_contract; #[cfg(feature = "dispatch-workspace")] pub mod dispatch_workspace; +#[cfg(any(feature = "local-storage", feature = "runtime-ownership"))] mod file_lock; +#[cfg(feature = "filesystem")] pub mod filesystem; +#[cfg(feature = "local-storage")] pub mod json_store; pub mod jsonc; pub mod local_instructions; @@ -18,24 +21,34 @@ pub mod local_instructions; pub mod local_runtime_ports; #[cfg(feature = "lsp")] pub mod lsp; +#[cfg(feature = "process-runtime")] pub mod managed_runtime; #[cfg(feature = "markdown")] pub mod markdown; #[cfg(feature = "permission")] pub mod permission_store; +#[cfg(feature = "local-storage")] pub mod persistence; +#[cfg(feature = "process-runtime")] pub mod process_manager; +#[cfg(feature = "process-runtime")] pub mod process_tree; #[cfg(feature = "runtime-ownership")] pub mod runtime_ownership; +#[cfg(feature = "local-storage")] pub mod session; +#[cfg(feature = "local-storage")] pub mod session_usage; +#[cfg(feature = "local-storage")] pub mod storage_cleanup; +#[cfg(feature = "process-runtime")] pub mod system; +#[cfg(feature = "local-storage")] pub mod token_usage; #[cfg(feature = "workspace-runtime")] pub mod workspace; #[cfg(feature = "workspace-identity")] pub mod workspace_identity; +#[cfg(feature = "workspace-instructions")] pub mod workspace_instructions; pub mod workspace_text; diff --git a/src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs b/src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs index 9ec4a3ef4..9160e1ccb 100644 --- a/src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs +++ b/src/crates/services/services-core/tests/declarative_workspace_instruction_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "workspace-instructions")] + use bitfun_services_core::workspace_instructions::read_workspace_instruction_files; use std::fs; @@ -7,6 +9,54 @@ fn instruction_names( files.iter().map(|file| file.name.as_str()).collect() } +#[tokio::test] +async fn workspace_instruction_files_reads_agents_then_claude_and_skips_empty_files() { + let temp = tempfile::tempdir().expect("tempdir"); + fs::write(temp.path().join("AGENTS.md"), "agent rules\n").expect("agents"); + fs::write(temp.path().join("CLAUDE.md"), "claude rules\n").expect("claude"); + + let files = read_workspace_instruction_files(temp.path()) + .await + .expect("instruction files"); + + assert_eq!(files.len(), 2); + assert_eq!(files[0].name, "AGENTS.md"); + assert_eq!(files[0].content, "agent rules\n"); + assert_eq!(files[1].name, "CLAUDE.md"); + assert_eq!(files[1].content, "claude rules\n"); + + fs::write(temp.path().join("AGENTS.md"), "").expect("empty agents"); + let files = read_workspace_instruction_files(temp.path()) + .await + .expect("instruction files"); + assert_eq!(files.len(), 1); + assert_eq!(files[0].name, "CLAUDE.md"); +} + +#[tokio::test] +async fn workspace_instruction_override_replaces_agents_without_hiding_claude() { + let temp = tempfile::tempdir().expect("tempdir"); + fs::write(temp.path().join("AGENTS.override.md"), "override rules\n").expect("override"); + fs::write(temp.path().join("AGENTS.md"), "base rules\n").expect("agents"); + fs::write(temp.path().join("CLAUDE.md"), "claude rules\n").expect("claude"); + + let files = read_workspace_instruction_files(temp.path()) + .await + .expect("instruction files"); + + assert_eq!(files.len(), 2); + assert_eq!(files[0].name, "AGENTS.override.md"); + assert_eq!(files[0].content, "override rules\n"); + assert_eq!(files[1].name, "CLAUDE.md"); + + fs::write(temp.path().join("AGENTS.override.md"), "").expect("empty override"); + let files = read_workspace_instruction_files(temp.path()) + .await + .expect("instruction files"); + assert_eq!(files.len(), 1); + assert_eq!(files[0].name, "CLAUDE.md"); +} + #[tokio::test] async fn claude_project_files_and_unconditional_rules_have_deterministic_order() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src/crates/services/services-core/tests/json_store_contracts.rs b/src/crates/services/services-core/tests/json_store_contracts.rs index e1b637bd4..0f45850b9 100644 --- a/src/crates/services/services-core/tests/json_store_contracts.rs +++ b/src/crates/services/services-core/tests/json_store_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::json_store::{JsonFileStore, JsonFileStoreError}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; diff --git a/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs b/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs index 42be554cc..53e787312 100644 --- a/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs +++ b/src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "lsp")] + use bitfun_core_types::lsp::{CapabilitiesConfig, LspPlugin, ServerConfig}; use bitfun_services_core::lsp::{ resolve_plugin_command_for_target, LspPluginRegistryError, LspPluginRuntimeArch, diff --git a/src/crates/services/services-core/tests/markdown_owner_contracts.rs b/src/crates/services/services-core/tests/markdown_owner_contracts.rs index 1e2fce020..18f49ad20 100644 --- a/src/crates/services/services-core/tests/markdown_owner_contracts.rs +++ b/src/crates/services/services-core/tests/markdown_owner_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "markdown")] + use bitfun_services_core::markdown::{ expand_prompt_template_arguments, expand_prompt_template_arguments_with_names, parse_prompt_shell_directives, prompt_template_expansion_upper_bound, FrontMatterMarkdown, diff --git a/src/crates/services/services-core/tests/process_runtime_contracts.rs b/src/crates/services/services-core/tests/process_runtime_contracts.rs new file mode 100644 index 000000000..f9e819c70 --- /dev/null +++ b/src/crates/services/services-core/tests/process_runtime_contracts.rs @@ -0,0 +1,11 @@ +#![cfg(feature = "process-runtime")] + +use bitfun_services_core::system::check_command; + +#[test] +fn system_check_command_preserves_missing_command_shape() { + let result = check_command("__bitfun_missing_command_for_services_core_test__"); + + assert!(!result.exists); + assert_eq!(result.path, None); +} diff --git a/src/crates/services/services-core/tests/runtime_ownership_contracts.rs b/src/crates/services/services-core/tests/runtime_ownership_contracts.rs index 7e90d34e2..cc688da18 100644 --- a/src/crates/services/services-core/tests/runtime_ownership_contracts.rs +++ b/src/crates/services/services-core/tests/runtime_ownership_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "runtime-ownership")] + use bitfun_services_core::runtime_ownership::{ RuntimeDeployment, RuntimeOwnershipError, RuntimeOwnershipKey, WorkspaceRuntimeOwnership, }; diff --git a/src/crates/services/services-core/tests/service_contracts.rs b/src/crates/services/services-core/tests/service_contracts.rs index d7dc2c675..26d293f8b 100644 --- a/src/crates/services/services-core/tests/service_contracts.rs +++ b/src/crates/services/services-core/tests/service_contracts.rs @@ -1,5 +1,4 @@ use bitfun_services_core::diff::{DiffConfig, DiffLineType, DiffService}; -use bitfun_services_core::system::check_command; #[test] fn diff_service_preserves_line_count_contract() { @@ -16,11 +15,3 @@ fn diff_service_preserves_line_count_contract() { .flat_map(|hunk| hunk.lines.iter()) .any(|line| line.line_type == DiffLineType::Add && line.content == "three")); } - -#[test] -fn system_check_command_preserves_missing_command_shape() { - let result = check_command("__bitfun_missing_command_for_services_core_test__"); - - assert!(!result.exists); - assert_eq!(result.path, None); -} diff --git a/src/crates/services/services-core/tests/session_contracts.rs b/src/crates/services/services-core/tests/session_contracts.rs index 08c98937a..1ad2858a2 100644 --- a/src/crates/services/services-core/tests/session_contracts.rs +++ b/src/crates/services/services-core/tests/session_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::session::{DialogTurnKind, SessionKind, SessionMetadata}; #[test] diff --git a/src/crates/services/services-core/tests/session_layout_contracts.rs b/src/crates/services/services-core/tests/session_layout_contracts.rs index a5df9fb14..ed0762558 100644 --- a/src/crates/services/services-core/tests/session_layout_contracts.rs +++ b/src/crates/services/services-core/tests/session_layout_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::session::SessionStorageLayout; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/src/crates/services/services-core/tests/session_metadata_contracts.rs b/src/crates/services/services-core/tests/session_metadata_contracts.rs index 3297a3981..e633495c9 100644 --- a/src/crates/services/services-core/tests/session_metadata_contracts.rs +++ b/src/crates/services/services-core/tests/session_metadata_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::session::{ build_session_index_snapshot, refresh_session_metadata_from_turns, remove_session_index_entry, try_refresh_session_metadata_for_saved_turn, upsert_session_index_entry, DialogTurnData, diff --git a/src/crates/services/services-core/tests/session_page_contracts.rs b/src/crates/services/services-core/tests/session_page_contracts.rs index e93c55394..4139f1b48 100644 --- a/src/crates/services/services-core/tests/session_page_contracts.rs +++ b/src/crates/services/services-core/tests/session_page_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_core_types::SessionKind; use bitfun_services_core::session::{ build_session_metadata_page, SessionMetadata, SessionRelationship, SessionRelationshipKind, diff --git a/src/crates/services/services-core/tests/session_usage_contracts.rs b/src/crates/services/services-core/tests/session_usage_contracts.rs index a02ce8d26..bacdc5819 100644 --- a/src/crates/services/services-core/tests/session_usage_contracts.rs +++ b/src/crates/services/services-core/tests/session_usage_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::session_usage::{ classify_tool_usage, display_workspace_relative_path, render_usage_report_terminal, SessionUsageReport, UsageToolCategory, diff --git a/src/crates/services/services-core/tests/session_write_lock_contracts.rs b/src/crates/services/services-core/tests/session_write_lock_contracts.rs index 3051c96fc..2e17652fa 100644 --- a/src/crates/services/services-core/tests/session_write_lock_contracts.rs +++ b/src/crates/services/services-core/tests/session_write_lock_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::session::{SessionWriteLock, SessionWriteLockError}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; diff --git a/src/crates/services/services-core/tests/storage_owner_contracts.rs b/src/crates/services/services-core/tests/storage_owner_contracts.rs index 9fb56041b..bf03c4ceb 100644 --- a/src/crates/services/services-core/tests/storage_owner_contracts.rs +++ b/src/crates/services/services-core/tests/storage_owner_contracts.rs @@ -1,6 +1,7 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::persistence::{PersistenceService, StorageOptions}; use bitfun_services_core::storage_cleanup::{CleanupPolicy, CleanupRoots, CleanupService}; -use bitfun_services_core::workspace_instructions::read_workspace_instruction_files; use serde::{Deserialize, Serialize}; use serde_json::json; use std::fs; @@ -165,54 +166,6 @@ async fn cleanup_service_trims_oldest_cache_files_when_size_exceeds_policy() { assert_eq!(result.categories[0].name, "Oversized Cache"); } -#[tokio::test] -async fn workspace_instruction_files_reads_agents_then_claude_and_skips_empty_files() { - let temp = tempfile::tempdir().expect("tempdir"); - fs::write(temp.path().join("AGENTS.md"), "agent rules\n").expect("agents"); - fs::write(temp.path().join("CLAUDE.md"), "claude rules\n").expect("claude"); - - let files = read_workspace_instruction_files(temp.path()) - .await - .expect("instruction files"); - - assert_eq!(files.len(), 2); - assert_eq!(files[0].name, "AGENTS.md"); - assert_eq!(files[0].content, "agent rules\n"); - assert_eq!(files[1].name, "CLAUDE.md"); - assert_eq!(files[1].content, "claude rules\n"); - - fs::write(temp.path().join("AGENTS.md"), "").expect("empty agents"); - let files = read_workspace_instruction_files(temp.path()) - .await - .expect("instruction files"); - assert_eq!(files.len(), 1); - assert_eq!(files[0].name, "CLAUDE.md"); -} - -#[tokio::test] -async fn workspace_instruction_override_replaces_agents_without_hiding_claude() { - let temp = tempfile::tempdir().expect("tempdir"); - fs::write(temp.path().join("AGENTS.override.md"), "override rules\n").expect("override"); - fs::write(temp.path().join("AGENTS.md"), "base rules\n").expect("agents"); - fs::write(temp.path().join("CLAUDE.md"), "claude rules\n").expect("claude"); - - let files = read_workspace_instruction_files(temp.path()) - .await - .expect("instruction files"); - - assert_eq!(files.len(), 2); - assert_eq!(files[0].name, "AGENTS.override.md"); - assert_eq!(files[0].content, "override rules\n"); - assert_eq!(files[1].name, "CLAUDE.md"); - - fs::write(temp.path().join("AGENTS.override.md"), "").expect("empty override"); - let files = read_workspace_instruction_files(temp.path()) - .await - .expect("instruction files"); - assert_eq!(files.len(), 1); - assert_eq!(files[0].name, "CLAUDE.md"); -} - #[tokio::test] async fn token_usage_service_persists_records_and_filters_subagents_by_default() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src/crates/services/services-core/tests/token_usage_contracts.rs b/src/crates/services/services-core/tests/token_usage_contracts.rs index bc7cfae85..7a365c25e 100644 --- a/src/crates/services/services-core/tests/token_usage_contracts.rs +++ b/src/crates/services/services-core/tests/token_usage_contracts.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "local-storage")] + use bitfun_services_core::token_usage::{ ModelTokenStats, SessionTokenStats, TimeRange, TokenUsageQuery, TokenUsageRecord, }; diff --git a/src/crates/services/services-core/tests/workspace_instruction_contracts.rs b/src/crates/services/services-core/tests/workspace_instruction_contracts.rs index 0d722fca7..465378ef7 100644 --- a/src/crates/services/services-core/tests/workspace_instruction_contracts.rs +++ b/src/crates/services/services-core/tests/workspace_instruction_contracts.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "workspace-runtime")] +#![cfg(all(feature = "workspace-instructions", feature = "workspace-runtime"))] use bitfun_services_core::workspace::LocalWorkspaceFs; use bitfun_services_core::workspace_instructions::{ diff --git a/src/crates/services/services-integrations/Cargo.toml b/src/crates/services/services-integrations/Cargo.toml index 5033c3e74..ef92140cb 100644 --- a/src/crates/services/services-integrations/Cargo.toml +++ b/src/crates/services/services-integrations/Cargo.toml @@ -95,7 +95,7 @@ zbus-secret-service-keyring-store = { workspace = true, optional = true } [features] default = [] announcement = ["reqwest", "tokio/fs", "tokio/sync"] -browser-control = ["anyhow", "bitfun-services-core", "dirs", "reqwest", "thiserror", "tokio/time"] +browser-control = ["anyhow", "bitfun-services-core/process-runtime", "dirs", "reqwest", "thiserror", "tokio/time"] canvas-runtime = [ "dep:bitfun-product-domains", "oxc", @@ -109,7 +109,7 @@ deep-research = ["bitfun-agent-runtime", "tokio/fs"] git = [ "async-trait", "bitfun-runtime-ports", - "bitfun-services-core", + "bitfun-services-core/process-runtime", "chrono", "git2", "thiserror", @@ -131,7 +131,7 @@ mcp = [ "async-trait", "base64", "bitfun-agent-tools", - "bitfun-services-core", + "bitfun-services-core/process-runtime", "futures", "hex", "rand", @@ -153,7 +153,7 @@ mcp = [ miniapp-runtime = [ "base64", "bitfun-product-domains/miniapp", - "bitfun-services-core", + "bitfun-services-core/process-runtime", "dep:bitfun-product-domains", "dirs", "reqwest", @@ -201,7 +201,7 @@ plugin-source = [ hook-import = [ "bitfun-agent-runtime", "bitfun-product-domains/external-sources", - "bitfun-services-core", + "bitfun-services-core/local-storage", "dep:bitfun-product-domains", "hex", "sha2", @@ -218,7 +218,8 @@ remote-connect = [ "async-trait", "base64", "bitfun-agent-tools", - "bitfun-services-core", + "bitfun-services-core/local-storage", + "bitfun-services-core/process-runtime", "bitfun-runtime-ports", "chrono", "dirs", @@ -252,7 +253,8 @@ remote-connect = [ remote-ssh = [ "anyhow", "async-trait", - "bitfun-services-core", + "bitfun-services-core/filesystem", + "bitfun-services-core/process-runtime", "bitfun-services-core/workspace-identity", "bitfun-runtime-ports", "sha2", @@ -274,7 +276,6 @@ remote-ssh-concrete = [ "anyhow", "async-trait", "base64", - "bitfun-services-core", "bitfun-runtime-ports", "chrono", "dirs", @@ -292,7 +293,7 @@ remote-ssh-concrete = [ ] review-platform = [ "async-trait", - "bitfun-services-core", + "bitfun-services-core/process-runtime", "chrono", "futures", "reqwest", @@ -326,7 +327,8 @@ speech = [ ] workspace-search = [ "async-trait", - "bitfun-services-core", + "bitfun-services-core/filesystem", + "bitfun-services-core/process-runtime", "dunce", "thiserror", "tokio/io-util", @@ -335,7 +337,7 @@ workspace-search = [ "tokio/time", "which", ] -process-tree = ["bitfun-services-core"] +process-tree = ["bitfun-services-core/process-runtime"] script-tool-runtime = [ "async-trait", "bitfun-runtime-ports",