Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions docs/features/remote-workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,33 @@ working execution and text-query paths and receive an explicit upgrade instructi
for a missing binary capability. Existing Remote Connect file commands and
persisted session records remain compatible; the file-chunk revision is additive.

## User Skill resources

User Skill availability is controlled by the runtime host's
`workspace.remote_user_skills` preference: `skill_md_only` (the upgrade-compatible
default), `disabled`, or `full`. It applies to remote discovery, prompt listings
and explicit invocation, without changing remote project Skills. See the
[Desktop guide](../../src/apps/desktop/README.md#user-skills-in-remote-workspaces)
for prerequisites and behavior.

Full loading deploys the selected directory through the bound filesystem
provider, never through a local fallback. SSH workspaces use the existing SFTP
connection; synchronization does not execute a remote shell or require Python,
an archive extractor or an external synchronization program. Deterministic file
hashes and permissions identify package versions. Bounded parallel streamed
reads check the complete remote tree before reuse; otherwise files are uploaded
and verified in a staging directory before publication. Many small files can
still take time over high-latency connections, so progress remains visible.
Published directories are never moved or removed by synchronization. A changed
or damaged cached copy leads to a new generation; matching verified generations
can be reused. This preserves resource paths already returned to other turns
without a persistent target-side lock.
Returned Skill paths use target POSIX semantics,
and references rooted in the original package are mapped to that deployment.
The runtime reads instructions back from the deployed snapshot before reporting
success. This copies package resources; it does not install interpreters,
relocate executable hooks, or resolve external machine-specific dependencies.

## Upgrade compatibility

Existing SSH profiles remain plain SSH targets because the new `proxyJump` and
Expand Down
74 changes: 74 additions & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ test('Services Core feature-free dependencies stay behind exact text and async I
'workspace-runtime',
'workspace-text-runtime',
'workspace-transfer',
'skill-package-sync',
]),
);
});
Expand Down Expand Up @@ -4315,6 +4316,7 @@ test('Services Core accepts only the reviewed feature-owned Tokio runtime graph'
],
'session-git': ['local-storage'],
'workspace-transfer': ['dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt', 'tokio/sync'],
'skill-package-sync': ['dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt'],
},
};

Expand Down Expand Up @@ -5068,6 +5070,78 @@ test('workspace transfer admits IO without process or networking capabilities',
v.message.includes('workspace-transfer has unexpected effective Tokio capabilities')));
});

test('Skill package synchronization keeps streamed workspace IO without archive or process owners', async () => {
const manifestPath = 'src/crates/services/services-core/Cargo.toml';
const manifest = await readFile(new URL(`../${manifestPath}`, import.meta.url), 'utf8');
const features = parseManifestFeatures(manifest);
const expected = [
'dep:anyhow', 'dep:openbitfun-runtime-ports',
'openbitfun-runtime-ports/workspace-ports', 'dep:sha2', 'dep:futures', 'dep:uuid',
'dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt',
];
const profile = coreClosedFeatureProfileRules.find(rule =>
rule.manifestPath === manifestPath && rule.featureName === 'skill-package-sync');
assert.equal(profile?.exact, true);
assert.deepEqual(profile.requiredFeatureRefs, expected);
assert.deepEqual(features['skill-package-sync'], expected);
const serviceOwners = optionalDependencyFeatureOwnerRules.find(rule => rule.crateName === 'services-core');
assert.deepEqual(serviceOwners.dependencies.find(rule => rule.depName === 'zip').ownerFeatures, ['pet-packages']);
assert.deepEqual(serviceOwners.dependencies.find(rule => rule.depName === 'futures').ownerFeatures, ['skill-package-sync']);

const pkg = {
name: 'openbitfun-services-core',
manifest_path: manifestPath,
dependencies: [{ name: 'tokio', kind: null, optional: true, features: [] }],
features,
};
assert.deepEqual(findTokioDependencyFeatureViolations([pkg]), []);
for (const added of ['tokio/process', 'tokio/net', 'workspace-runtime']) {
const mutated = structuredClone(pkg);
mutated.features['skill-package-sync'].push(added);
assert.ok(findTokioDependencyFeatureViolations([mutated]).some(violation =>
violation.message.includes('skill-package-sync has unexpected effective Tokio capabilities')), added);
}

const { findCapabilityContractConsumerViolations } = await import(
'./core-boundaries/cargo-dependency-boundaries.mjs'
);
const runtimePorts = capabilityPackage(
'openbitfun-runtime-ports',
'src/crates/contracts/runtime-ports/Cargo.toml',
RUNTIME_PORT_FEATURE_PROFILES,
);
const consumer = {
...packageAt('openbitfun-services-core', manifestPath, [
pathDependency('src/crates/contracts/runtime-ports', {
name: 'openbitfun-runtime-ports', optional: true, usesDefaultFeatures: false,
}),
]),
features,
};
const rules = capabilityContractDependencyRules.filter(rule => rule.packageName === 'openbitfun-runtime-ports');
assert.deepEqual(findTestCapabilityViolations(findCapabilityContractConsumerViolations, [runtimePorts, consumer], rules), []);
for (const added of ['runtime-event-port', 'remote-exec-port', 'tool-runtime-handles']) {
const mutated = structuredClone(consumer);
mutated.features['skill-package-sync'].push(`openbitfun-runtime-ports/${added}`);
assert.ok(findTestCapabilityViolations(findCapabilityContractConsumerViolations, [runtimePorts, mutated], rules)
.some(violation => violation.message.includes('skill-package-sync') && violation.message.includes(added)), added);
}
});

test('remote workspace feature selects the Skill package service only at its owning surface', async () => {
const manifestPath = 'src/crates/assembly/core/Cargo.toml';
const manifest = await readFile(new URL(`../${manifestPath}`, import.meta.url), 'utf8');
const expected = [
'workspace-runtime', 'dep:openbitfun-services-integrations',
'openbitfun-services-integrations/remote-ssh', 'openbitfun-services-core/skill-package-sync',
];
const profile = coreClosedFeatureProfileRules.find(rule =>
rule.manifestPath === manifestPath && rule.featureName === 'remote-workspace');
assert.equal(profile?.exact, true);
assert.deepEqual(profile.requiredFeatureRefs, expected);
assert.deepEqual(new Set(parseManifestFeatures(manifest)['remote-workspace']), new Set(expected));
});


test('core loopback WebSocket fixture cannot broaden the runtime dependency', () => {
const runtime = { name: 'tokio-tungstenite', kind: null, optional: true, uses_default_features: true, features: [] };
Expand Down
1 change: 1 addition & 0 deletions scripts/core-boundaries/cargo-dependency-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const SERVICES_CORE_TOKIO_FEATURES = new Map([
['workspace-text-runtime', ['rt']],
['workspace-runtime', ['fs', 'io-util', 'process', 'rt', 'sync', 'time']],
['workspace-transfer', ['fs', 'io-util', 'rt', 'sync']],
['skill-package-sync', ['fs', 'io-util', 'rt']],
]);
const SERVICES_CORE_BASE_TOKIO_FEATURES = [];
const SERVICES_INTEGRATIONS_TOKIO_AGGREGATES = new Set(['product-full']);
Expand Down
26 changes: 21 additions & 5 deletions scripts/core-boundaries/rules/feature-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,22 @@ export const optionalDependencyFeatureOwnerRules = [
dependencies: [
{ depName: 'image', ownerFeatures: ['pet-packages'] },
{ depName: 'zip', ownerFeatures: ['pet-packages'] },
{ depName: 'uuid', ownerFeatures: ['pet-packages', 'resource-capacity'] },
{ depName: 'uuid', ownerFeatures: ['pet-packages', 'resource-capacity', 'skill-package-sync'] },
{ depName: 'aes-gcm', ownerFeatures: ['credential-vault'] },
{ depName: 'anyhow', ownerFeatures: ['credential-vault', 'dispatch-workspace', 'workspace-runtime', 'workspace-transfer', 'session-recovery'] },
{ depName: 'anyhow', ownerFeatures: ['credential-vault', 'dispatch-workspace', 'workspace-runtime', 'workspace-transfer', 'session-recovery', 'skill-package-sync'] },
{ depName: 'async-trait', ownerFeatures: ['permission', 'workspace-runtime'] },
{ depName: 'base64', ownerFeatures: ['pet-packages', 'credential-vault', 'filesystem'] },
{
depName: 'openbitfun-core-types',
ownerFeatures: ['filesystem', 'local-storage', 'product-identity', 'workspace-persistence'],
},
{ depName: 'openbitfun-events', ownerFeatures: ['local-storage', 'session-event-format'] },
{ depName: 'openbitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime', 'workspace-persistence', 'workspace-transfer'] },
{ depName: 'openbitfun-runtime-ports', ownerFeatures: ['permission', 'workspace-runtime', 'workspace-persistence', 'workspace-transfer', 'skill-package-sync'] },
{ depName: 'chrono', ownerFeatures: ['filesystem', 'local-storage', 'workspace-persistence'] },
{ depName: 'chrono-tz', ownerFeatures: ['token-usage-statistics'] },
{ depName: 'dunce', ownerFeatures: ['pet-packages', 'runtime-ownership', 'workspace-identity', 'workspace-runtime'] },
{ depName: 'fs2', ownerFeatures: ['pet-packages', 'credential-vault', 'json-io', 'local-storage', 'runtime-ownership'] },
{ depName: 'futures', ownerFeatures: ['skill-package-sync'] },
{ depName: 'git2', ownerFeatures: ['session-git'] },
{ depName: 'globset', ownerFeatures: ['workspace-instructions'] },
{ depName: 'ignore', ownerFeatures: ['filesystem'] },
Expand Down Expand Up @@ -92,6 +93,7 @@ export const optionalDependencyFeatureOwnerRules = [
'resource-capacity',
'workspace-identity',
'workspace-transfer',
'skill-package-sync',
],
},
{ depName: 'which', ownerFeatures: ['process-runtime'] },
Expand All @@ -112,6 +114,7 @@ export const optionalDependencyFeatureOwnerRules = [
'workspace-runtime',
'workspace-text-runtime',
'workspace-transfer',
'skill-package-sync',
],
},
],
Expand Down Expand Up @@ -517,8 +520,9 @@ export const capabilityContractDependencyRules = [
capabilityForwarder('workspace-runtime', 'runtime-event-port'),
capabilityForwarder('workspace-runtime', 'workspace-ports'),
capabilityForwarder('workspace-transfer', 'workspace-ports'),
capabilityForwarder('skill-package-sync', 'workspace-ports'),
],
['permission', 'workspace-runtime', 'workspace-persistence', 'workspace-transfer'],
['permission', 'workspace-runtime', 'workspace-persistence', 'workspace-transfer', 'skill-package-sync'],
['session-recovery'],
)],
['openbitfun-services-integrations', capabilityConsumer(
Expand Down Expand Up @@ -1672,6 +1676,17 @@ export const coreClosedFeatureProfileRules = [
exact: true,
reason: 'workspace-transfer owns bounded upload IO and optimistic commit through injected workspace ports, without process or transport implementations',
},
{
manifestPath: 'src/crates/services/services-core/Cargo.toml',
featureName: 'skill-package-sync',
requiredFeatureRefs: [
'dep:anyhow', 'dep:openbitfun-runtime-ports',
'openbitfun-runtime-ports/workspace-ports', 'dep:sha2', 'dep:futures', 'dep:uuid',
'dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt',
],
exact: true,
reason: 'skill-package-sync owns bounded concurrent file transfer and streamed verification through injected workspace filesystem ports, without archive, process or transport implementations',
},
{
manifestPath: 'src/crates/services/services-core/Cargo.toml',
featureName: 'workspace-runtime',
Expand Down Expand Up @@ -1776,10 +1791,11 @@ export const coreClosedFeatureProfileRules = [
'workspace-runtime',
'dep:openbitfun-services-integrations',
'openbitfun-services-integrations/remote-ssh',
'openbitfun-services-core/skill-package-sync',
],
allowedTransitiveFeatureRefs: ['filesystem', 'local-storage', 'process-runtime'],
exact: true,
reason: 'openbitfun-core remote-workspace must add only the remote workspace service surface',
reason: 'openbitfun-core remote-workspace must add only SSH workspace services and on-demand user Skill package deployment',
},
{
manifestPath: 'src/crates/assembly/core/Cargo.toml',
Expand Down
10 changes: 10 additions & 0 deletions scripts/core-boundaries/self-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,15 @@ export function runManifestParserSelfTest({
],
],
[servicesCoreManifest, 'session-git', ['local-storage', 'dep:git2']],
[
servicesCoreManifest,
'skill-package-sync',
[
'dep:anyhow', 'dep:openbitfun-runtime-ports',
'openbitfun-runtime-ports/workspace-ports', 'dep:sha2', 'dep:futures', 'dep:uuid',
'dep:tokio', 'tokio/fs', 'tokio/io-util', 'tokio/rt',
],
],
[
servicesCoreManifest,
'token-usage-statistics',
Expand Down Expand Up @@ -555,6 +564,7 @@ export function runManifestParserSelfTest({
'workspace-runtime',
'dep:openbitfun-services-integrations',
'openbitfun-services-integrations/remote-ssh',
'openbitfun-services-core/skill-package-sync',
],
],
[
Expand Down
40 changes: 40 additions & 0 deletions src/apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,46 @@ setup. Detached Dispatch does not provision WSL connections. SSH port forwarding
is unavailable for native WSL targets; use Windows WSL networking to reach a
Linux service.

## User Skills in remote workspaces

In **Settings → Workspace & Git → User Skills in remote workspaces**, choose:

- **Only inject SKILL.md** (default): load local user instructions without copying
their resources. The loaded Skill and its completed tool card explicitly report
that supporting files have not been synced.
- **Do not run**: omit host-local user Skills from remote session listings and
reject their invocation. Remote project Skills remain available.
- **Full**: on invocation, check the complete package against its remote copy;
transfer files through the bound workspace filesystem and expose a verified
POSIX resource path. SSH workspaces use SFTP over the existing SSH connection,
with bounded parallel file operations. The Skill card shows preparation,
upload and verification progress.
Its completed state confirms package availability, not that runtime dependencies
are installed or that the Skill has executed successfully.

Full mode needs a workspace file provider that supports streamed reads/writes,
metadata, permissions and staging publication. It does not require Python, an
archive extractor or another synchronization program on the target. Any runtime
dependencies the Skill itself needs must still be available there. Packages are cached
under `<remote-workspace>/.openbitfun/cache/user-skills`; exclude that cache from
version control if your project tracks `.openbitfun`. Many small files or large
packages can take time to verify and transfer, especially on high-latency SSH
connections. Reuse checks read and hash remote file contents before skipping
uploads. Nested symbolic links and special
files are rejected with an explicit error; files outside the Skill directory are
not copied. Synchronization reuses a matching verified directory. If a cached
copy was modified or damaged, it publishes a new generation and retains the old
directory, so concurrent loads never move another turn's resource path.
Project file operations and commands continue to run on the remote host.
Standalone Markdown Skills in a shared directory must first be converted into
an isolated `name/SKILL.md` package for Full mode; OpenBitFun never copies their
entire shared parent directory implicitly.

This preference belongs to the OpenBitFun runtime host. A mobile or peer
controller uses that host's setting and Skills; it does not upload its own local
Skill library. Older hosts without this setting show an unsupported state in the
settings UI.

## Remote SSH file handle errors

If writing files and browsing directories both start failing with
Expand Down
11 changes: 11 additions & 0 deletions src/crates/assembly/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ Narrower local guides already exist for some subtrees:

## Verification

For remote user Skill availability, Markdown-only warnings, package path mapping,
and on-demand deployment, use:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,remote-workspace --lib remote_user_skills
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,remote-workspace --lib skill_tool::tests
```

These owner fixtures use local providers or mocks. The live SSH package resource
fixture is documented in `services-integrations/AGENTS.md`.

For smart routing, unchanged subagent model policy and resumed subagent choices:

```bash
Expand Down
1 change: 1 addition & 0 deletions src/crates/assembly/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ workspace-runtime = [
workspace-watch = ["workspace-runtime", "dep:notify"]
remote-workspace = [
"workspace-runtime",
"openbitfun-services-core/skill-package-sync",
"dep:openbitfun-services-integrations",
"openbitfun-services-integrations/remote-ssh",
]
Expand Down
Loading
Loading