Skip to content
Merged
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
5 changes: 5 additions & 0 deletions docs/architecture/detached-task-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ There are three roles:
- A **transport adapter** moves the same narrow JSON protocol over SSH or an
end-to-end encrypted account-device RPC.

Desktop (`dispatch_*` commands) and the CLI (`openbitfun dispatch outbound`) are
both controllers over the same Core controller. The account-device `host_invoke`
envelope is shared through `AccountRuntimeDeviceDispatchRpc` and its
encode/decode helpers, so the two hosts cannot drift.

The controller is never a runtime or filesystem proxy for a non-local job. It
stores only an outbound observer record under
`~/.openbitfun/dispatch/outbound/`; it must not create the target session in the
Expand Down
32 changes: 23 additions & 9 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,7 @@ const CLI_REVIEWED_CORE_FEATURES = [
'remote-connect',
'plugin-runtime',
'opencode-plugin-host',
'dispatch-store',
];

const APP_SERVER_REVIEWED_CORE_FEATURES = [
Expand Down Expand Up @@ -2912,7 +2913,7 @@ test('CLI dependency closure includes build dependencies and excluded capabiliti
assert.match(violations[0].message, /openbitfun-core\/product-full/);
});

test('CLI dependency closure excludes the Core dispatch store', () => {
test('only the CLI dependency closure may select the Core dispatch store', () => {
const core = {
...packageAt('openbitfun-core', 'src/crates/assembly/core/Cargo.toml'),
features: { 'dispatch-store': [] },
Expand All @@ -2924,17 +2925,30 @@ test('CLI dependency closure excludes the Core dispatch store', () => {
features: ['dispatch-store'],
}),
]);
const cli = packageAt('openbitfun-cli', 'src/apps/cli/Cargo.toml', [
pathDependency('src/crates/assembly/bridge', { name: 'bridge' }),
]);

const violations = findProductEntrypointCoreFeatureViolations(
[cli, bridge, core],
const closureViolations = (rootName, manifestPath) => findProductEntrypointCoreFeatureViolations(
[
packageAt(rootName, manifestPath, [
pathDependency('src/crates/assembly/bridge', { name: 'bridge' }),
]),
bridge,
core,
],
{ root: TEST_ROOT, crateLayoutRules },
);

assert.equal(violations.length, 1);
assert.match(violations[0].message, /openbitfun-core\/dispatch-store/);
// The CLI is the reviewed outbound dispatch controller.
assert.deepEqual(
closureViolations('openbitfun-cli', 'src/apps/cli/Cargo.toml')
.filter((violation) => /dispatch-store/.test(violation.message)),
[],
);

const acpViolations = closureViolations(
'openbitfun-acp',
'src/crates/interfaces/acp/Cargo.toml',
);
assert.equal(acpViolations.length, 1);
assert.match(acpViolations[0].message, /openbitfun-core\/dispatch-store/);
});

test('cargo layer checker rejects reverse edges across dependency kinds', () => {
Expand Down
10 changes: 7 additions & 3 deletions scripts/core-boundaries/cargo-dependency-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,8 @@ export function findProductEntrypointCoreFeatureViolations(
'plugin-runtime',
'opencode-plugin-host',
'ssh-remote',
// Outbound controller for `openbitfun dispatch outbound`.
'dispatch-store',
]],
['openbitfun-acp', [...new Set([...acpClientCoreFeatures, ...acpServerCoreFeatures])]],
['openbitfun-app-server', [
Expand Down Expand Up @@ -1536,6 +1538,7 @@ export function findProductEntrypointCoreFeatureViolations(
'product-search',
'opencode-plugin-host',
'remote-connect',
'dispatch-store',
]],
['openbitfun-acp', acpActiveCoreFeatures],
['openbitfun-sdk-host-app', coreCompatibilityActiveFeatures],
Expand Down Expand Up @@ -1787,9 +1790,10 @@ export function findProductEntrypointCoreFeatureViolations(

const corePackage = packages.find((pkg) => pkg.name === 'openbitfun-core');
if (corePackage) {
const forbiddenCoreFeatures = [
const forbiddenCoreFeaturesFor = (rootName) => [
'product-full',
'dispatch-store',
// The CLI is a reviewed dispatch controller; other closures are not.
...(rootName === 'openbitfun-cli' ? [] : ['dispatch-store']),
];
const reportedUnexpectedFeatures = new Set();

Expand Down Expand Up @@ -1930,7 +1934,7 @@ export function findProductEntrypointCoreFeatureViolations(

if (targetPackage.name === 'openbitfun-core') {
const activeCoreFeatures = targetState.featureState.active;
const unexpected = forbiddenCoreFeatures.find((feature) =>
const unexpected = forbiddenCoreFeaturesFor(rootName).find((feature) =>
activeCoreFeatures.has(feature))
?? [...activeCoreFeatures]
.filter((feature) => !allowedCoreFeatures.has(feature))
Expand Down
5 changes: 3 additions & 2 deletions src/apps/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ restrictions remain enforced.
- Assemble CLI through `DeliveryProfile::Cli` and validated product Runtime
parts. Hiding a command is not a backend capability restriction.
- The CLI selects the reviewed `openbitfun-core` owner-feature closure
(`agent-runtime`, `external-sources`, `plugin-runtime`, `remote-connect`, and
`ssh-remote`) plus the Code Agent atomic tool owners and the independent
(`agent-runtime`, `external-sources`, `plugin-runtime`, `remote-connect`,
`ssh-remote`, and `dispatch-store` for the `dispatch outbound` controller)
plus the Code Agent atomic tool owners and the independent
`tools-pages` owner. Pages binds to the executing host's AccountRuntime;
controller-local account state must not enable remote tools. It must not register
DeepReview, DeepResearch, MiniApp, or Canvas agents/tools. Do not replace the
Expand Down
1 change: 1 addition & 0 deletions src/apps/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ openbitfun-core = { path = "../../crates/assembly/core", features = [
"plugin-runtime",
"opencode-plugin-host",
"ssh-remote",
"dispatch-store",
"tools-pages",
"tools-basic",
"tools-git",
Expand Down
17 changes: 17 additions & 0 deletions src/apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,23 @@ protocol version before submitting or inspecting jobs. See the
[detached task architecture](../../../docs/architecture/detached-task-dispatch.md)
for the transport and workspace-snapshot contract.

To dispatch from this machine, use the controller commands. They print JSON and
work from any Git checkout:

```bash
openbitfun dispatch outbound targets
openbitfun dispatch outbound submit --target "MacBook-Pro-2.local" \
--approval-policy reject-and-report "Run the integration tests and fix failures"
openbitfun dispatch outbound status <job-id> --cursor 0
openbitfun dispatch outbound list
openbitfun dispatch outbound cancel <job-id>
```

`--target` accepts an account device name or alias, `device:<deviceId>`, or
`ssh:<connectionId>`. Device targets need this machine to be signed in to the
same account; an offline device is an error, never a local fallback. The target
works in its own worktree branched from `--base-ref` (default `HEAD`).

The `remote` approval policy lets a controller answer pending tool permissions.
Interactive questions from `AskUserQuestion` are unavailable for dispatch jobs;
provide required task choices in the prompt instead of waiting for a question.
Expand Down
1 change: 1 addition & 0 deletions src/apps/cli/src/dispatch/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod outbound;
mod permissions;
pub(crate) mod protocol;
mod query_file;
Expand Down
Loading
Loading