Skip to content

Commit 0f17da7

Browse files
committed
feat(extensions): add host-owned external integration policy
1 parent 26cb8c3 commit 0f17da7

46 files changed

Lines changed: 8156 additions & 640 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/core-boundaries/rules/feature-rules.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ export const productCoreFeatureAssemblyRules = [
128128
requiredFeatures: ['product-full'],
129129
reason: 'CLI must explicitly assemble the full bitfun-core product runtime',
130130
},
131+
{
132+
manifestPath: 'src/apps/server/Cargo.toml',
133+
dependencyName: 'bitfun-core',
134+
requiredFeatures: ['product-full'],
135+
reason: 'Server must explicitly assemble the full bitfun-core product runtime',
136+
},
131137
{
132138
manifestPath: 'src/crates/interfaces/acp/Cargo.toml',
133139
dependencyName: 'bitfun-core',

scripts/core-boundaries/rules/source/public-api-rules.mjs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const publicApiContractSlices = [
99
'external-source-tool-contract',
1010
'external-source-subagent-contract',
1111
'external-source-mcp-contract',
12+
'external-integration-policy-contract',
1213
];
1314

1415
const contractSlices = {
@@ -20,6 +21,7 @@ const contractSlices = {
2021
externalSourceToolContract: 'external-source-tool-contract',
2122
externalSourceSubagentContract: 'external-source-subagent-contract',
2223
externalSourceMcpContract: 'external-source-mcp-contract',
24+
externalIntegrationPolicyContract: 'external-integration-policy-contract',
2325
};
2426

2527
function pluginRuntimeEntry(symbol, p0, consumer, verification, contractSlice, wireImpact = true) {
@@ -230,6 +232,55 @@ function externalSourceEntry(symbol, owner, consumer, wireImpact = false) {
230232
};
231233
}
232234

235+
function externalIntegrationPolicyEntry(
236+
symbol,
237+
owner = 'product-domains external integration policy contract owner',
238+
consumer = 'bitfun-core product composition and cross-host product surfaces',
239+
wireImpact = true,
240+
) {
241+
return {
242+
symbol,
243+
owner,
244+
consumer,
245+
verification:
246+
'external integration policy contract tests, core policy lifecycle tests, cross-host route tests, and Web policy-control tests',
247+
p0: 'host-owned external integration policy and OpenCode-compatible product defaults',
248+
contractSlice: contractSlices.externalIntegrationPolicyContract,
249+
wireImpact,
250+
rationale:
251+
'all product surfaces need one ecosystem-neutral, versioned, fail-closed policy contract while concrete ecosystem defaults remain in product assembly',
252+
exit:
253+
'remove only through a reviewed policy-contract migration with equivalent compatibility, safety-ceiling, and cross-host behavior tests',
254+
};
255+
}
256+
257+
export const externalIntegrationPolicyPublicApiEntries = [
258+
'EXTERNAL_INTEGRATION_POLICY_SCHEMA_MAJOR',
259+
'ExternalIntegrationMode',
260+
'ExternalIntegrationAccess',
261+
'ExternalEcosystemPolicy',
262+
'ExternalIntegrationPolicySettings',
263+
'ExternalIntegrationPolicySettingsView',
264+
'ExternalEcosystemPolicyOverride',
265+
'ExternalEcosystemPolicyOverrideView',
266+
'ExternalIntegrationPolicyOverride',
267+
'ExternalIntegrationPolicyOverrideView',
268+
'ExternalIntegrationPolicyDocument',
269+
'ExternalIntegrationCapabilityDescriptor',
270+
'ExternalIntegrationEcosystemDescriptor',
271+
'ExternalEcosystemPolicyView',
272+
'EffectiveExternalEcosystemPolicy',
273+
'EffectiveExternalIntegrationPolicy',
274+
'ExternalIntegrationPolicyStatus',
275+
'ExternalIntegrationPolicySnapshot',
276+
'ExternalIntegrationPolicyScope',
277+
'ExternalIntegrationPolicyOperation',
278+
'ExternalIntegrationPolicyMutation',
279+
'evaluate_external_integration_policy',
280+
'external_integration_policy_snapshot',
281+
'incompatible_external_integration_policy_snapshot',
282+
].map((symbol) => externalIntegrationPolicyEntry(symbol));
283+
233284
function externalToolEntry(symbol, owner, consumer, wireImpact = false) {
234285
return {
235286
symbol,
@@ -298,6 +349,9 @@ export const externalSourceContractPublicApiEntries = [
298349
'ExternalSourceContext',
299350
'ExternalWatchRoot',
300351
'ExternalSourceProviderError',
352+
'ExternalSourceOperationErrorCode',
353+
'ExternalSourceOperationError',
354+
'ExternalSourceOperationResult',
301355
'PromptCommandSourceProvider',
302356
'ExternalSourceLifecycleState',
303357
'ExternalSourceCatalogEntry',
@@ -306,6 +360,10 @@ export const externalSourceContractPublicApiEntries = [
306360
'PromptCommandConflict',
307361
'prompt_command_conflict_key',
308362
'ExternalSourceCatalogSnapshot',
363+
'ExternalPromptCommandDefinitionSummary',
364+
'ExternalPromptCommandSummary',
365+
'ExternalSourcePublicSnapshot',
366+
'ExternalSourceHostCapabilities',
309367
].map((symbol) =>
310368
externalSourceEntry(
311369
symbol,
@@ -459,6 +517,31 @@ export const externalSourceCoordinatorPublicApiEntries = [
459517
];
460518

461519
export const externalSourceCorePublicApiEntries = [
520+
...[
521+
'ExternalIntegrationAccess',
522+
'ExternalIntegrationMode',
523+
'ExternalIntegrationPolicyMutation',
524+
'ExternalIntegrationPolicyOperation',
525+
'ExternalIntegrationPolicyScope',
526+
'EffectiveExternalIntegrationPolicy',
527+
'ExternalIntegrationPolicySnapshot',
528+
'ExternalIntegrationPolicyStatus',
529+
'EcosystemId',
530+
'ExternalIntegrationCapabilityId',
531+
'OPENCODE_ECOSYSTEM_ID',
532+
'EXTERNAL_CAPABILITY_COMMAND',
533+
'EXTERNAL_CAPABILITY_TOOL',
534+
'EXTERNAL_CAPABILITY_SUBAGENT',
535+
'EXTERNAL_CAPABILITY_MCP',
536+
'update_external_integration_policy',
537+
].map((symbol) =>
538+
externalIntegrationPolicyEntry(
539+
symbol,
540+
'bitfun-core external integration policy composition facade',
541+
'bitfun-cli, Desktop, Server, Peer Host, and Web API adapters',
542+
true,
543+
),
544+
),
462545
...[
463546
'ExpandedPromptCommand',
464547
'ExternalSourceCatalogEntry',
@@ -467,6 +550,10 @@ export const externalSourceCorePublicApiEntries = [
467550
'ExternalSourceDiagnostic',
468551
'ExternalSourceDiagnosticSeverity',
469552
'ExternalSourceLifecycleState',
553+
'ExternalSourceHostCapabilities',
554+
'ExternalSourceOperationError',
555+
'ExternalSourceOperationErrorCode',
556+
'ExternalSourceOperationResult',
470557
'PromptCommandAvailability',
471558
'PromptCommandCatalogEntry',
472559
'PromptCommandDefinition',
@@ -480,6 +567,7 @@ export const externalSourceCorePublicApiEntries = [
480567
'expand_external_prompt_command',
481568
'subscribe_external_source_updates',
482569
'ExternalSourceSubscription',
570+
'ExternalSourcePublicSnapshot',
483571
].map((symbol) =>
484572
externalSourceEntry(
485573
symbol,
@@ -493,6 +581,7 @@ export const externalSourceCorePublicApiEntries = [
493581
'ExternalToolCapability',
494582
'ExternalToolCatalogEntry',
495583
'ExternalToolConflict',
584+
'ExternalToolConflictCandidateKind',
496585
'ExternalToolRuntimeKind',
497586
'set_external_tool_target_decision',
498587
'set_external_tool_conflict_choice',
@@ -657,6 +746,12 @@ export const publicApiAllowlistRules = [
657746
'managed plugin package and trust contracts must stay explicitly budgeted and ecosystem-neutral',
658747
allowedSymbolEntries: pluginSourceContractPublicApiEntries,
659748
},
749+
{
750+
path: 'src/crates/contracts/product-domains/src/external_integration_policy.rs',
751+
reason:
752+
'external integration policy contracts must stay ecosystem-neutral, versioned, fail-closed, and explicitly consumer-backed',
753+
allowedSymbolEntries: externalIntegrationPolicyPublicApiEntries,
754+
},
660755
{
661756
path: 'src/crates/contracts/product-domains/src/external_sources.rs',
662757
reason:

scripts/core-boundaries/self-test.mjs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export function runManifestParserSelfTest({
165165
for (const manifestPath of [
166166
'src/apps/desktop/Cargo.toml',
167167
'src/apps/cli/Cargo.toml',
168+
'src/apps/server/Cargo.toml',
168169
'src/crates/interfaces/acp/Cargo.toml',
169170
]) {
170171
if (!productCoreRulePaths.has(manifestPath)) {
@@ -195,15 +196,21 @@ export function runManifestParserSelfTest({
195196
},
196197
{
197198
manifestPath: 'src/apps/server/Cargo.toml',
198-
text: '[dependencies]\naxum = { workspace = true }',
199+
text:
200+
'[dependencies]\nbitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] }',
199201
},
200202
{
201203
manifestPath: 'src/crates/interfaces/acp/Cargo.toml',
202204
text: '[dependencies."bitfun-core"]\npath = "../../assembly/core"\ndefault-features = false\nfeatures = ["product-full"]',
203205
},
204206
]);
205-
if (discoveredProductCoreManifests.join(',') !== 'src/apps/desktop/Cargo.toml,src/crates/interfaces/acp/Cargo.toml') {
206-
throw new Error('product core dependency scanner must discover only manifests that depend on bitfun-core');
207+
if (
208+
discoveredProductCoreManifests.join(',') !==
209+
'src/apps/desktop/Cargo.toml,src/apps/server/Cargo.toml,src/crates/interfaces/acp/Cargo.toml'
210+
) {
211+
throw new Error(
212+
'product core dependency scanner must discover only manifests that depend on bitfun-core',
213+
);
207214
}
208215
const ownerFeatureRulePaths = new Set(
209216
ownerCrateFeatureAssemblyRules.map((rule) => rule.manifestPath),

src/apps/cli/src/main.rs

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ mod ui;
2828

2929
use anyhow::{anyhow, Result};
3030
use bitfun_core::service::remote_connect::DeviceIdentity;
31-
use clap::{Parser, Subcommand};
31+
use clap::{Parser, Subcommand, ValueEnum};
3232
use std::sync::atomic::{AtomicU8, Ordering};
3333
use std::sync::OnceLock;
3434

@@ -400,6 +400,78 @@ enum ConfigAction {
400400
Edit,
401401
/// Reset to default configuration
402402
Reset,
403+
/// Inspect or change external AI application compatibility
404+
External {
405+
#[command(subcommand)]
406+
action: ExternalConfigAction,
407+
},
408+
}
409+
410+
#[derive(Subcommand)]
411+
enum ExternalConfigAction {
412+
/// Show effective global and project compatibility settings
413+
Status,
414+
/// Enable or disable external compatibility
415+
SetEnabled {
416+
enabled: bool,
417+
#[arg(long, value_enum, default_value = "project")]
418+
scope: ExternalPolicyScopeArg,
419+
},
420+
/// Select an external ecosystem compatibility mode
421+
SetMode {
422+
#[arg(value_enum)]
423+
mode: ExternalPolicyModeArg,
424+
/// Ecosystem id; optional when exactly one ecosystem is registered
425+
#[arg(long)]
426+
ecosystem: Option<String>,
427+
#[arg(long, value_enum, default_value = "project")]
428+
scope: ExternalPolicyScopeArg,
429+
},
430+
/// Customize one external ecosystem capability
431+
SetCapability {
432+
#[arg(value_enum)]
433+
capability: ExternalCapabilityArg,
434+
#[arg(value_enum)]
435+
access: ExternalAccessArg,
436+
/// Ecosystem id; optional when exactly one ecosystem is registered
437+
#[arg(long)]
438+
ecosystem: Option<String>,
439+
#[arg(long, value_enum, default_value = "project")]
440+
scope: ExternalPolicyScopeArg,
441+
},
442+
/// Remove this project's overrides and inherit global settings
443+
ResetProject,
444+
/// Back up and reset a policy written by an incompatible BitFun version
445+
ResetIncompatible,
446+
}
447+
448+
#[derive(Clone, Copy, Debug, ValueEnum)]
449+
enum ExternalPolicyScopeArg {
450+
Global,
451+
Project,
452+
}
453+
454+
#[derive(Clone, Copy, Debug, ValueEnum)]
455+
enum ExternalPolicyModeArg {
456+
Recommended,
457+
DiscoverOnly,
458+
Off,
459+
}
460+
461+
#[derive(Clone, Copy, Debug, ValueEnum)]
462+
enum ExternalCapabilityArg {
463+
Command,
464+
Tool,
465+
Agent,
466+
Mcp,
467+
}
468+
469+
#[derive(Clone, Copy, Debug, ValueEnum)]
470+
enum ExternalAccessArg {
471+
Off,
472+
Discover,
473+
Ask,
474+
Auto,
403475
}
404476

405477
#[derive(Subcommand)]
@@ -887,7 +959,7 @@ async fn run_cli() -> Result<()> {
887959
}
888960

889961
Some(Commands::Config { action }) => {
890-
root_handlers::handle_config_action(action, &config)?;
962+
root_handlers::handle_config_action(action, &config).await?;
891963
}
892964

893965
Some(Commands::Health) => {
@@ -1143,6 +1215,88 @@ mod plugin_command_tests {
11431215
}
11441216
}
11451217

1218+
#[cfg(test)]
1219+
mod external_config_command_tests {
1220+
use super::{
1221+
Cli, Commands, ConfigAction, ExternalAccessArg, ExternalCapabilityArg,
1222+
ExternalConfigAction, ExternalPolicyModeArg, ExternalPolicyScopeArg,
1223+
};
1224+
use clap::Parser;
1225+
1226+
#[test]
1227+
fn external_config_commands_keep_scope_and_capability_explicit() {
1228+
let status = Cli::try_parse_from(["bitfun-cli", "config", "external", "status"])
1229+
.expect("parse external status");
1230+
assert!(matches!(
1231+
status.command,
1232+
Some(Commands::Config {
1233+
action: ConfigAction::External {
1234+
action: ExternalConfigAction::Status
1235+
}
1236+
})
1237+
));
1238+
1239+
let mode = Cli::try_parse_from([
1240+
"bitfun-cli",
1241+
"config",
1242+
"external",
1243+
"set-mode",
1244+
"discover-only",
1245+
"--scope",
1246+
"global",
1247+
])
1248+
.expect("parse external mode");
1249+
assert!(matches!(
1250+
mode.command,
1251+
Some(Commands::Config {
1252+
action: ConfigAction::External {
1253+
action: ExternalConfigAction::SetMode {
1254+
mode: ExternalPolicyModeArg::DiscoverOnly,
1255+
ecosystem: None,
1256+
scope: ExternalPolicyScopeArg::Global,
1257+
}
1258+
}
1259+
})
1260+
));
1261+
1262+
let capability = Cli::try_parse_from([
1263+
"bitfun-cli",
1264+
"config",
1265+
"external",
1266+
"set-capability",
1267+
"mcp",
1268+
"ask",
1269+
"--ecosystem",
1270+
"opencode",
1271+
])
1272+
.expect("parse external capability");
1273+
assert!(matches!(
1274+
capability.command,
1275+
Some(Commands::Config {
1276+
action: ConfigAction::External {
1277+
action: ExternalConfigAction::SetCapability {
1278+
capability: ExternalCapabilityArg::Mcp,
1279+
access: ExternalAccessArg::Ask,
1280+
ecosystem: Some(ref ecosystem),
1281+
scope: ExternalPolicyScopeArg::Project,
1282+
}
1283+
}
1284+
}) if ecosystem == "opencode"
1285+
));
1286+
1287+
let reset = Cli::try_parse_from(["bitfun-cli", "config", "external", "reset-incompatible"])
1288+
.expect("parse incompatible policy reset");
1289+
assert!(matches!(
1290+
reset.command,
1291+
Some(Commands::Config {
1292+
action: ConfigAction::External {
1293+
action: ExternalConfigAction::ResetIncompatible
1294+
}
1295+
})
1296+
));
1297+
}
1298+
}
1299+
11461300
#[cfg(test)]
11471301
mod bootstrap_profile_tests {
11481302
use super::{exec_requests_json_output, BootstrapProfile, SessionAction};

0 commit comments

Comments
 (0)