@rebon/cli-linux-arm64 1.18.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.txt +13 -13
- package/bin/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -296
- package/bin/compose-runtime/payload/vendor/schemastery/index.mjs +633 -654
- package/bin/compose-runtime/src/bridge.mjs +19 -0
- package/bin/compose-runtime/src/entries.mjs +53 -0
- package/bin/compose-runtime/src/loader-runtime.mjs +130 -0
- package/bin/compose-runtime/src/plugin.mjs +31 -2
- package/bin/compose-runtime/src/probe.mjs +194 -18
- package/bin/compose-runtime/src/provided-modules.json +13 -11
- package/bin/compose-runtime/src/realm.mjs +26 -4
- package/bin/compose-runtime/src/registry.mjs +10 -1
- package/bin/compose-runtime/src/remote.mjs +237 -0
- package/bin/compose-runtime/src/resolve.mjs +34 -8
- package/bin/compose-runtime/src/subprocess-runtime.mjs +169 -0
- package/bin/compose-runtime/src/systemprompt-runtime.mjs +80 -13
- package/bin/compose-runtime/src/tools-runtime.mjs +21 -0
- package/bin/compose-runtime/src/working-directory-runtime.mjs +71 -0
- package/bin/rebon +2 -2
- package/bin/rebon-boa-helper +0 -0
- package/bin/rebon-browser-mcp +0 -0
- package/bin/rebon-computer-use +0 -0
- package/bin/rebon-lsp-mcp +0 -0
- package/bin/rebon-verve +0 -0
- package/bin/sandbox-win +0 -0
- package/package.json +1 -1
|
@@ -72,6 +72,25 @@ export function seat(name, method, params = null, options = {}) {
|
|
|
72
72
|
return require(`seat ${JSON.stringify(name)}`, via).seat(name, method, params, call);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/// Calls a kernel seat as the plugin `via` belongs to, whatever call is in
|
|
76
|
+
/// flight.
|
|
77
|
+
///
|
|
78
|
+
/// `seat()` speaks for the call being answered, which is right for code
|
|
79
|
+
/// serving that call. This is for the composition acting *on behalf of one
|
|
80
|
+
/// entry* while answering someone else — the configuration editor
|
|
81
|
+
/// persisting an entry's own config while a settings form asked for it.
|
|
82
|
+
/// The seat then sees that entry as the caller, so the entry's own ceiling,
|
|
83
|
+
/// not the composition's, decides what the call may do.
|
|
84
|
+
export function seatFor(via, name, method, params = null) {
|
|
85
|
+
const session = sessionOf(via);
|
|
86
|
+
if (session === undefined) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[NO_CALL_CONTEXT] seat ${JSON.stringify(name)} cannot be reached for this entry: it is not attached to a session`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return session.seat(name, method, params);
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
/// Invokes one rebon core tool on the session this call belongs to.
|
|
76
95
|
///
|
|
77
96
|
/// Three gates stand in front of it and only the first is here: the plugin's
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The realm's entry table, as the dsh Loader vocabulary sees it.
|
|
2
|
+
//
|
|
3
|
+
// One view per mounted entry, shaped like a `cordis-plugin-loader` entry
|
|
4
|
+
// (`id`, `options.{id,name,config}`, `fiber`), so the services that answer
|
|
5
|
+
// for the loader (`loader-runtime.mjs`) hand upstream code the shape it
|
|
6
|
+
// reads. `realm.mjs` writes it as entries mount and leave; nothing else
|
|
7
|
+
// does. Its own module so both sides import it without importing each other.
|
|
8
|
+
|
|
9
|
+
/** Mounted entries by plugin id, in load order. */
|
|
10
|
+
export const entries = new Map();
|
|
11
|
+
|
|
12
|
+
let inflight = 0;
|
|
13
|
+
let waiters = [];
|
|
14
|
+
|
|
15
|
+
/// Records one mount for its whole duration, so `settled()` can wait it out.
|
|
16
|
+
export async function mounting(work) {
|
|
17
|
+
inflight += 1;
|
|
18
|
+
try {
|
|
19
|
+
return await work();
|
|
20
|
+
} finally {
|
|
21
|
+
inflight -= 1;
|
|
22
|
+
if (inflight === 0) {
|
|
23
|
+
const ready = waiters;
|
|
24
|
+
waiters = [];
|
|
25
|
+
for (const resolve of ready) resolve();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Resolves once nothing is mounting.
|
|
31
|
+
export function settled() {
|
|
32
|
+
if (inflight === 0) return Promise.resolve();
|
|
33
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// The loader-shaped view of one mounted entry.
|
|
37
|
+
export function track(pluginId, request, fiber) {
|
|
38
|
+
const view = {
|
|
39
|
+
id: pluginId,
|
|
40
|
+
options: { id: pluginId, name: request.name ?? pluginId, config: request.config ?? undefined },
|
|
41
|
+
fiber,
|
|
42
|
+
// Where the entry's module is, for what reads its package (its client
|
|
43
|
+
// half): the load request's package root and module.
|
|
44
|
+
root: request.root,
|
|
45
|
+
module: request.entry,
|
|
46
|
+
};
|
|
47
|
+
entries.set(pluginId, view);
|
|
48
|
+
return view;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function untrack(pluginId) {
|
|
52
|
+
entries.delete(pluginId);
|
|
53
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The composition is DeepSeek Harness's Loader, as far as a plugin can tell.
|
|
2
|
+
//
|
|
3
|
+
// In dsh a profile (`cordis.yml` plus its patches) is loaded by
|
|
4
|
+
// `cordis-plugin-loader`, and the boot packages built on it — the
|
|
5
|
+
// configuration editor, the profile context — are what upstream plugins
|
|
6
|
+
// reach when they need to know what is mounted or to change its config.
|
|
7
|
+
// `@deepseek-ai/dsh-settings` is the one that matters most: its settings
|
|
8
|
+
// forms are entry configs, read through `ctx.configEditor` and written back
|
|
9
|
+
// through it.
|
|
10
|
+
//
|
|
11
|
+
// In rebon the profile is `kernelPlugins` and the loader is the plugin plane:
|
|
12
|
+
// rebon reads the entry list, the realm mounts one entry per `plugin/load`.
|
|
13
|
+
// So the three services here answer the dsh vocabulary from the realm's own
|
|
14
|
+
// table rather than mounting dsh's loader beside it — a second loader would
|
|
15
|
+
// be a second answer to "what is running":
|
|
16
|
+
//
|
|
17
|
+
// * `loader` — the mounted entries, and `await()` for the loads in flight;
|
|
18
|
+
// * `configEditor` — an entry's config as two layers (nothing inherited;
|
|
19
|
+
// the user's layer is its `kernelPlugins` row), and `edit`, which checks
|
|
20
|
+
// the next config against the plugin's schema, persists it as the entry
|
|
21
|
+
// itself through the `settings` seat, and commits its volatile fields
|
|
22
|
+
// into the running plugin the way dsh's loader does;
|
|
23
|
+
// * `profileContext` — the profile's home, `<config home>/dsh`, rebon's
|
|
24
|
+
// counterpart of `DSH_HOME`.
|
|
25
|
+
//
|
|
26
|
+
// What is deliberately not here: the loader's create/remove/reorder API.
|
|
27
|
+
// Which entries exist is rebon's composition, changed by installing and
|
|
28
|
+
// configuring, not by a plugin.
|
|
29
|
+
import { Context, Service, resolveConfig } from 'cordis';
|
|
30
|
+
import { deepEqual, updateVolatile, volatileEntries } from 'cosmokit';
|
|
31
|
+
import { seatFor } from './bridge.mjs';
|
|
32
|
+
import { entries, settled } from './entries.mjs';
|
|
33
|
+
|
|
34
|
+
class LoaderError extends Error {
|
|
35
|
+
constructor(code, message) {
|
|
36
|
+
super(`${code} ${message}`);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = 'LoaderError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clone = (value) => (value === undefined ? {} : structuredClone(value));
|
|
43
|
+
|
|
44
|
+
export class RebonLoader extends Service {
|
|
45
|
+
constructor(ctx) {
|
|
46
|
+
super(ctx, 'loader');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Every mounted entry, in load order.
|
|
50
|
+
entries() {
|
|
51
|
+
return [...entries.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Resolves once no entry is being mounted.
|
|
55
|
+
await() {
|
|
56
|
+
return settled();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class RebonConfigEditor extends Service {
|
|
61
|
+
constructor(ctx, profile) {
|
|
62
|
+
super(ctx, 'configEditor');
|
|
63
|
+
this.profile = profile ?? {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// The file a person edits by hand, when rebon named one.
|
|
67
|
+
get documentPath() {
|
|
68
|
+
return typeof this.profile.documentPath === 'string' ? this.profile.documentPath : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
entries() {
|
|
72
|
+
return [...entries.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// dsh's two layers per entry. Rebon has one: what the entry's row says.
|
|
76
|
+
configuration() {
|
|
77
|
+
return this.entries().map((entry) => ({
|
|
78
|
+
entry,
|
|
79
|
+
inherited: {},
|
|
80
|
+
override: clone(entry.options.config),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Validates, persists and applies an entry's next config.
|
|
85
|
+
///
|
|
86
|
+
/// The order is dsh's: a config the plugin's own schema refuses is
|
|
87
|
+
/// refused before anything is written, and the running plugin changes
|
|
88
|
+
/// only after rebon has the new config.
|
|
89
|
+
///
|
|
90
|
+
/// What changes in the running plugin is what dsh's loader changes without
|
|
91
|
+
/// a remount: the `.volatile()` fields, committed into the references the
|
|
92
|
+
/// plugin reads them through, and announced as `loader/volatile-update` to
|
|
93
|
+
/// that plugin alone. An ordinary field is a remount in dsh. Here it waits
|
|
94
|
+
/// for rebon to load the entry again: what an entry registers is reported
|
|
95
|
+
/// once per load, so a plugin restarted in place could not re-register.
|
|
96
|
+
/// Settings forms only ever edit volatile fields.
|
|
97
|
+
async edit(entry, change) {
|
|
98
|
+
if (entries.get(entry?.id) !== entry || entry.fiber === undefined) {
|
|
99
|
+
throw new LoaderError('[UNKNOWN_ENTRY]', 'that configuration entry is no longer mounted');
|
|
100
|
+
}
|
|
101
|
+
const fiber = entry.fiber;
|
|
102
|
+
if (fiber.state !== 2) {
|
|
103
|
+
throw new LoaderError('[ENTRY_INACTIVE]', `${entry.id} is not active`);
|
|
104
|
+
}
|
|
105
|
+
const next = change(clone(entry.options.config), {});
|
|
106
|
+
const candidate = resolveConfig(fiber.runtime, next);
|
|
107
|
+
await seatFor(fiber.ctx, 'settings', 'entryConfig', { config: next });
|
|
108
|
+
entry.options.config = next;
|
|
109
|
+
if (!deepEqual(fiber.config, candidate, true)) return;
|
|
110
|
+
const paths = volatileEntries(fiber.config).flatMap(({ path, ref }) => {
|
|
111
|
+
const source = path.reduce((value, key) => Reflect.get(value, key), candidate);
|
|
112
|
+
if (deepEqual(ref.get(), source.get(), true)) return [];
|
|
113
|
+
updateVolatile(ref, source);
|
|
114
|
+
return [path];
|
|
115
|
+
});
|
|
116
|
+
if (paths.length === 0) return;
|
|
117
|
+
const self = Object.create(fiber.ctx);
|
|
118
|
+
self[Context.filter] = (owner) => owner.fiber?.uid === fiber.uid;
|
|
119
|
+
fiber.ctx.emit(self, 'loader/volatile-update', paths);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class RebonProfileContext extends Service {
|
|
124
|
+
constructor(ctx, profile) {
|
|
125
|
+
super(ctx, 'profileContext');
|
|
126
|
+
this.home = profile?.home;
|
|
127
|
+
this.name = profile?.name ?? 'rebon';
|
|
128
|
+
this.dir = this.home;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -25,8 +25,13 @@ export async function activate(api, config = {}) {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const { withCall } = await import('./bridge.mjs');
|
|
28
|
-
const { builtinSeats, createRealm, mounted, reportFor } = await import('./realm.mjs');
|
|
29
|
-
|
|
28
|
+
const { builtinSeats, createRealm, mounted, realm, reportFor } = await import('./realm.mjs');
|
|
29
|
+
const { remote, REMOTE_EVENTS } = await import('./remote.mjs');
|
|
30
|
+
await createRealm({
|
|
31
|
+
web: config.web ?? null,
|
|
32
|
+
entries: config.entries ?? [],
|
|
33
|
+
profile: config.profile ?? null,
|
|
34
|
+
});
|
|
30
35
|
|
|
31
36
|
// Registered before any entry is loaded, so an entry claiming the same
|
|
32
37
|
// service name hits the registrar's refusal rather than replacing this.
|
|
@@ -40,6 +45,9 @@ export async function activate(api, config = {}) {
|
|
|
40
45
|
if (kind === 'list') {
|
|
41
46
|
return { plugins: mounted(), builtin: builtinSeats() };
|
|
42
47
|
}
|
|
48
|
+
if (kind === 'remote') {
|
|
49
|
+
return remote(String(request?.endpoint ?? ''), request?.args ?? {});
|
|
50
|
+
}
|
|
43
51
|
if (kind === 'report') {
|
|
44
52
|
const pluginId = String(request?.pluginId ?? '');
|
|
45
53
|
const report = reportFor(pluginId);
|
|
@@ -54,6 +62,27 @@ export async function activate(api, config = {}) {
|
|
|
54
62
|
error.code = '[UNKNOWN_CONTROL]';
|
|
55
63
|
throw error;
|
|
56
64
|
}));
|
|
65
|
+
|
|
66
|
+
// The Host events a dsh client hears, put on rebon's event plane. They are
|
|
67
|
+
// the composition's own facts — a settings form changing under a page —
|
|
68
|
+
// and nothing is asking when they happen, so they go out through the
|
|
69
|
+
// session the control plugin is attached to rather than an inbound call.
|
|
70
|
+
const sessions = new Set();
|
|
71
|
+
api.scope((scope) => {
|
|
72
|
+
sessions.add(scope);
|
|
73
|
+
return () => sessions.delete(scope);
|
|
74
|
+
});
|
|
75
|
+
for (const event of REMOTE_EVENTS) {
|
|
76
|
+
realm().ctx.on(event, (...args) => {
|
|
77
|
+
for (const scope of sessions) {
|
|
78
|
+
void scope.publish(REMOTE_EVENT_TOPIC, { event, args: JSON.parse(JSON.stringify(args)) }).catch(() => {});
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
57
83
|
}
|
|
58
84
|
|
|
85
|
+
/** The topic forwarded Host events are published on. */
|
|
86
|
+
export const REMOTE_EVENT_TOPIC = 'compose:remote-event';
|
|
87
|
+
|
|
59
88
|
export default activate;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Mounts
|
|
1
|
+
// Mounts Cordis plugin packages on a realm of their own and says what each
|
|
2
2
|
// registered, or why it could not mount.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// rebon-verve --permission --allow-fs-read=... probe.mjs <package root> <entry> [config JSON]
|
|
5
|
+
// rebon-verve --permission --allow-fs-read=... probe.mjs --bundle <bundle root> [configs JSON]
|
|
5
6
|
//
|
|
6
7
|
// This is how rebon reads a DeepSeek Harness package published to npm, which
|
|
7
8
|
// carries no rebon manifest: the package is mounted against rebon's own
|
|
@@ -10,11 +11,23 @@
|
|
|
10
11
|
// needs a service rebon does not offer fails here with that service's name —
|
|
11
12
|
// which is the compatibility answer, given before anything is installed.
|
|
12
13
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// A *bundle* is several packages installed into one `node_modules` and run in
|
|
15
|
+
// one realm, because that is how dsh packages reach each other: a seam
|
|
16
|
+
// implementation (`dsh-fs-local`) provides the service a consumer
|
|
17
|
+
// (`dsh-tool-fs`) injects. Its members are the rows of the bundle's dsh
|
|
18
|
+
// patch (`package.json` `dsh.bundle.patch`, the format dsh's own profile
|
|
19
|
+
// bundles use), or, without one, its dependencies that are Cordis plugins.
|
|
20
|
+
// They are mounted in the order their injections allow, and each member's
|
|
21
|
+
// report says which realm services it provided, so rebon can keep that order
|
|
22
|
+
// when it loads them.
|
|
23
|
+
//
|
|
24
|
+
// The caller runs it under the permission model (the packages and this
|
|
25
|
+
// runtime readable, nothing writable, no processes): the code is someone
|
|
26
|
+
// else's, executing for the first time.
|
|
16
27
|
//
|
|
17
28
|
// One line of JSON on stdout, whatever happens.
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
18
31
|
import path from 'node:path';
|
|
19
32
|
import { pathToFileURL } from 'node:url';
|
|
20
33
|
import { installModuleResolution } from './resolve.mjs';
|
|
@@ -28,6 +41,19 @@ function injects(plugin) {
|
|
|
28
41
|
return { required: [], optional: [] };
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/// What one mounted sink reports, in the shape the manifest is written from.
|
|
45
|
+
function registered(sink) {
|
|
46
|
+
const loaded = sink.loaded();
|
|
47
|
+
return {
|
|
48
|
+
tools: loaded.tools.map((tool) => ({ name: tool.name, description: tool.description ?? '' })),
|
|
49
|
+
services: loaded.services,
|
|
50
|
+
llmProviders: loaded.llmProviders,
|
|
51
|
+
eventTopics: loaded.eventTopics,
|
|
52
|
+
commands: loaded.commands.map((command) => ({ name: command.name, description: command.description })),
|
|
53
|
+
report: sink.report(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
async function probe(root, entry, config) {
|
|
32
58
|
installModuleResolution();
|
|
33
59
|
const { cordisPluginOf } = await import('./loader.mjs');
|
|
@@ -49,27 +75,177 @@ async function probe(root, entry, config) {
|
|
|
49
75
|
const base = { name: plugin.name ?? null, inject: wanted, seats };
|
|
50
76
|
try {
|
|
51
77
|
const sink = await mountEntry({ pluginId: 'probe', config }, plugin, { probe: true });
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
ok: true,
|
|
55
|
-
...base,
|
|
56
|
-
tools: loaded.tools.map((tool) => ({ name: tool.name, description: tool.description ?? '' })),
|
|
57
|
-
services: loaded.services,
|
|
58
|
-
llmProviders: loaded.llmProviders,
|
|
59
|
-
eventTopics: loaded.eventTopics,
|
|
60
|
-
commands: loaded.commands.map((command) => ({ name: command.name, description: command.description })),
|
|
61
|
-
report: sink.report(),
|
|
62
|
-
};
|
|
78
|
+
return { ok: true, ...base, ...registered(sink) };
|
|
63
79
|
} catch (cause) {
|
|
64
80
|
return { ok: false, ...base, missing, code: cause?.code ?? '[ACTIVATE_FAILED]', error: String(cause?.message ?? cause) };
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
// ---- bundles ------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
|
+
|
|
88
|
+
/// The module a package runs: `main`, else the `.` export, else `index.js` —
|
|
89
|
+
/// the rule rebon's installer uses (`dsh_npm::entry_of`).
|
|
90
|
+
function entryOf(manifest) {
|
|
91
|
+
if (typeof manifest.main === 'string') return manifest.main.replace(/^\.\//, '');
|
|
92
|
+
const dot = manifest.exports?.['.'];
|
|
93
|
+
const fromExports = typeof dot === 'string' ? dot : (dot?.default ?? dot?.import);
|
|
94
|
+
return typeof fromExports === 'string' ? fromExports.replace(/^\.\//, '') : 'index.js';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Whether a parsed config holds a dsh `!!js` expression anywhere.
|
|
98
|
+
function holdsExpression(value) {
|
|
99
|
+
if (value === null || typeof value !== 'object') return false;
|
|
100
|
+
if (Object.hasOwn(value, '__jsExpr')) return true;
|
|
101
|
+
return Object.values(value).some(holdsExpression);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The rows a bundle's dsh patch inserts, with later `{id, config}` rows
|
|
105
|
+
/// replacing a row's config the way dsh applies a patch layer.
|
|
106
|
+
function patchRows(root, manifest) {
|
|
107
|
+
const patch = manifest.dsh?.bundle?.patch;
|
|
108
|
+
if (typeof patch !== 'string') return undefined;
|
|
109
|
+
// dsh writes its patches in YAML and parses them with the `yaml` package,
|
|
110
|
+
// which every bundle that carries one installs; `!!js` values are dsh
|
|
111
|
+
// Loader expressions and are kept as markers, not evaluated.
|
|
112
|
+
const yaml = createRequire(path.join(root, 'package.json'))('yaml');
|
|
113
|
+
const text = fs.readFileSync(path.resolve(root, patch), 'utf8');
|
|
114
|
+
const doc = yaml.parse(text, {
|
|
115
|
+
customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: (source) => ({ __jsExpr: source }) }],
|
|
116
|
+
});
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (const op of Array.isArray(doc) ? doc : []) {
|
|
119
|
+
if (Array.isArray(op?.insert)) {
|
|
120
|
+
for (const row of op.insert) {
|
|
121
|
+
if (row && typeof row.name === 'string') rows.push({ id: row.id ?? row.name, name: row.name, config: row.config, disabled: row.disabled });
|
|
122
|
+
}
|
|
123
|
+
} else if (op && typeof op.id === 'string' && Object.hasOwn(op, 'config')) {
|
|
124
|
+
const row = rows.find((candidate) => candidate.id === op.id);
|
|
125
|
+
if (row) row.config = op.config;
|
|
126
|
+
} else if (op && typeof op.remove === 'string') {
|
|
127
|
+
const index = rows.findIndex((candidate) => candidate.id === op.remove);
|
|
128
|
+
if (index >= 0) rows.splice(index, 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// The members of a bundle: its patch rows, or its dependencies.
|
|
135
|
+
function bundleMembers(root) {
|
|
136
|
+
const manifest = readJson(path.join(root, 'package.json'));
|
|
137
|
+
const rows = patchRows(root, manifest);
|
|
138
|
+
if (rows !== undefined) return rows;
|
|
139
|
+
return Object.keys(manifest.dependencies ?? {}).map((name) => ({ id: name.split('/').pop(), name }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function probeBundle(root, overrides = {}) {
|
|
143
|
+
installModuleResolution();
|
|
144
|
+
const { cordisPluginOf } = await import('./loader.mjs');
|
|
145
|
+
const { createRealm, mountEntry, builtinSeats } = await import('./realm.mjs');
|
|
146
|
+
const live = await createRealm({});
|
|
147
|
+
const skipped = [];
|
|
148
|
+
const candidates = [];
|
|
149
|
+
for (const row of bundleMembers(root)) {
|
|
150
|
+
const dir = path.join(root, 'node_modules', ...row.name.split('/'));
|
|
151
|
+
let manifest;
|
|
152
|
+
try {
|
|
153
|
+
manifest = readJson(path.join(dir, 'package.json'));
|
|
154
|
+
} catch {
|
|
155
|
+
skipped.push({ id: row.id, name: row.name, code: '[NOT_INSTALLED]', error: `${row.name} is not installed in the bundle` });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (row.disabled === true) {
|
|
159
|
+
skipped.push({ id: row.id, name: row.name, code: '[DISABLED]', error: `${row.id} is disabled in the bundle` });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (holdsExpression(row.disabled)) {
|
|
163
|
+
skipped.push({ id: row.id, name: row.name, code: '[CONFIG_EXPRESSION]', error: `whether ${row.id} runs is a dsh Loader expression, which rebon does not evaluate` });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const config = Object.hasOwn(overrides, row.id) ? overrides[row.id] : row.config;
|
|
167
|
+
if (holdsExpression(config)) {
|
|
168
|
+
skipped.push({ id: row.id, name: row.name, code: '[CONFIG_EXPRESSION]', error: `${row.id}'s config is a dsh Loader expression, which rebon does not evaluate` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const entry = path.posix.join('node_modules', row.name, entryOf(manifest));
|
|
172
|
+
let module;
|
|
173
|
+
try {
|
|
174
|
+
module = await import(pathToFileURL(path.join(root, entry)).href);
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
skipped.push({ id: row.id, name: row.name, code: '[ENTRY_FAILED]', error: String(cause?.message ?? cause) });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const plugin = cordisPluginOf(module);
|
|
180
|
+
// A dependency that is a library, not a plugin, is simply not a member.
|
|
181
|
+
if (plugin === undefined) continue;
|
|
182
|
+
candidates.push({ ...row, entry, config, plugin, inject: injects(plugin) });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Every name any member injects, so what each one provided can be read off
|
|
186
|
+
// the realm before and after it mounts.
|
|
187
|
+
const watched = [...new Set(candidates.flatMap((member) => [...member.inject.required, ...member.inject.optional]))];
|
|
188
|
+
const present = () => new Set(watched.filter((name) => live.ctx.get(name) !== undefined));
|
|
189
|
+
const entries = [];
|
|
190
|
+
const failed = new Set();
|
|
191
|
+
let progress = true;
|
|
192
|
+
while (progress) {
|
|
193
|
+
progress = false;
|
|
194
|
+
for (const member of candidates) {
|
|
195
|
+
if (failed.has(member) || entries.some((done) => done.id === member.id)) continue;
|
|
196
|
+
if (member.inject.required.some((name) => live.ctx.get(name) === undefined)) continue;
|
|
197
|
+
const before = present();
|
|
198
|
+
try {
|
|
199
|
+
const sink = await mountEntry({ pluginId: member.id, name: member.name, config: member.config ?? undefined }, member.plugin, { probe: true });
|
|
200
|
+
const after = present();
|
|
201
|
+
entries.push({
|
|
202
|
+
id: member.id,
|
|
203
|
+
name: member.name,
|
|
204
|
+
entry: member.entry,
|
|
205
|
+
config: member.config ?? null,
|
|
206
|
+
inject: member.inject,
|
|
207
|
+
provides: [...after].filter((name) => !before.has(name)),
|
|
208
|
+
...registered(sink),
|
|
209
|
+
});
|
|
210
|
+
progress = true;
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
failed.add(member);
|
|
213
|
+
// A row that *is* one of rebon's seats — dsh's own `llm`, `tools`,
|
|
214
|
+
// `web`, … services — is answered by rebon, not missing.
|
|
215
|
+
const taken = /service "([^"]+)" has been registered/.exec(String(cause?.message ?? ''))?.[1];
|
|
216
|
+
if (taken !== undefined && builtinSeats().includes(taken)) {
|
|
217
|
+
skipped.push({ id: member.id, name: member.name, code: '[REBON_PROVIDES]', error: `rebon provides ${JSON.stringify(taken)} itself` });
|
|
218
|
+
} else {
|
|
219
|
+
skipped.push({ id: member.id, name: member.name, code: cause?.code ?? '[ACTIVATE_FAILED]', error: String(cause?.message ?? cause) });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const seats = builtinSeats();
|
|
225
|
+
for (const member of candidates) {
|
|
226
|
+
if (failed.has(member) || entries.some((done) => done.id === member.id)) continue;
|
|
227
|
+
const missing = member.inject.required.filter((name) => live.ctx.get(name) === undefined);
|
|
228
|
+
skipped.push({
|
|
229
|
+
id: member.id,
|
|
230
|
+
name: member.name,
|
|
231
|
+
code: '[MISSING_INJECT]',
|
|
232
|
+
missing,
|
|
233
|
+
error: `${member.id} requires ${missing.map((name) => JSON.stringify(name)).join(', ')}, which neither rebon nor another member of the bundle provides`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return { ok: entries.length > 0, bundle: true, seats, entries, skipped };
|
|
237
|
+
}
|
|
238
|
+
|
|
68
239
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
69
|
-
const
|
|
240
|
+
const args = process.argv.slice(2);
|
|
70
241
|
let answer;
|
|
71
242
|
try {
|
|
72
|
-
|
|
243
|
+
if (args[0] === '--bundle') {
|
|
244
|
+
answer = await probeBundle(args[1], args[2] ? JSON.parse(args[2]) : {});
|
|
245
|
+
} else {
|
|
246
|
+
const [root, entry, config] = args;
|
|
247
|
+
answer = await probe(root, entry, config ? JSON.parse(config) : null);
|
|
248
|
+
}
|
|
73
249
|
} catch (cause) {
|
|
74
250
|
answer = { ok: false, code: '[PROBE_FAILED]', error: String(cause?.message ?? cause) };
|
|
75
251
|
}
|
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
"note": "The bare specifiers the composition answers itself, and from where. `payload` entries are relative to the payload directory
|
|
3
|
-
"
|
|
2
|
+
"note": "The bare specifiers the composition answers itself, and from where. `owned` entries are rebon's for every importer: the module is the other half of a seat rebon implements (cordis, dsh-tools' scheduler symbol, dsh-llm's adapter base, the environment that shows only what was granted), so a second copy would be a second, wrong answer. `payload` entries are the vendored packages rebon's own payload is built from: they answer importers inside the payload and this runtime, while a package installed from npm resolves its own copy and only falls back to these when it has none. Both are relative to the payload directory; `runtime` entries are relative to this package's src/. Read by src/resolve.mjs and by rebon's installer, which installs from npm every peer of a package that is not `owned` or `runtime`.",
|
|
3
|
+
"owned": {
|
|
4
4
|
"cordis": "vendor/cordis/index.js",
|
|
5
5
|
"cosmokit": "vendor/cosmokit/index.mjs",
|
|
6
6
|
"schemastery": "vendor/schemastery/index.mjs",
|
|
7
7
|
"@deepseek-ai/cordis": "vendor/cordis/index.js",
|
|
8
8
|
"@deepseek-ai/cosmokit": "vendor/cosmokit/index.mjs",
|
|
9
9
|
"@deepseek-ai/schemastery": "vendor/schemastery/index.mjs",
|
|
10
|
+
"@deepseek-ai/dsh-llm": "compose/shims/dsh-llm.js",
|
|
11
|
+
"@deepseek-ai/dsh-credentials": "compose/shims/dsh-credentials.js",
|
|
12
|
+
"@deepseek-ai/dsh-launch-environment": "compose/shims/dsh-launch-environment.js",
|
|
13
|
+
"@deepseek-ai/dsh-anonymous-user-id": "compose/shims/dsh-anonymous-user-id.js",
|
|
14
|
+
"@deepseek-ai/dsh-user-id": "compose/shims/dsh-anonymous-user-id.js",
|
|
15
|
+
"@deepseek-ai/dsh-environment": "compose/shims/dsh-environment.js",
|
|
16
|
+
"@deepseek-ai/dsh-tools": "compose/shims/dsh-tools.js",
|
|
17
|
+
"@deepseek-ai/dsh-web": "compose/shims/dsh-web.js"
|
|
18
|
+
},
|
|
19
|
+
"payload": {
|
|
10
20
|
"@deepseek-ai/cordis-plugin-timer": "vendor/dsh/timer.js",
|
|
11
21
|
"@deepseek-ai/cordis-plugin-logger-console": "vendor/dsh/logger-console.js",
|
|
12
22
|
"@deepseek-ai/dsh-llm-deepseek": "vendor/dsh/llm-deepseek.js",
|
|
13
23
|
"@deepseek-ai/dsh-timeout": "vendor/dsh/timeout.js",
|
|
14
|
-
"@deepseek-ai/dsh-llm": "compose/shims/dsh-llm.js",
|
|
15
|
-
"@deepseek-ai/dsh-credentials": "compose/shims/dsh-credentials.js",
|
|
16
24
|
"@deepseek-ai/dsh-settings": "compose/shims/dsh-settings.js",
|
|
17
|
-
"@deepseek-ai/dsh-launch-environment": "compose/shims/dsh-launch-environment.js",
|
|
18
|
-
"@deepseek-ai/dsh-anonymous-user-id": "compose/shims/dsh-anonymous-user-id.js",
|
|
19
25
|
"eventsource-parser": "vendor/eventsource-parser/index.js",
|
|
20
|
-
"@deepseek-ai/dsh-tools": "compose/shims/dsh-tools.js",
|
|
21
26
|
"@deepseek-ai/dsh-tool-todo": "vendor/dsh/tool-todo.js",
|
|
22
27
|
"zod": "compose/shims/zod-lite.js",
|
|
23
|
-
"@deepseek-ai/dsh-web": "compose/shims/dsh-web.js",
|
|
24
28
|
"@deepseek-ai/dsh-web-search-exa": "vendor/dsh/web-search-exa.js",
|
|
25
29
|
"@deepseek-ai/dsh-tool-web": "vendor/dsh/tool-web.js",
|
|
26
30
|
"@deepseek-ai/dsh-scope": "vendor/dsh/scope.js",
|
|
27
31
|
"@deepseek-ai/dsh-session": "vendor/dsh/session.js",
|
|
28
32
|
"@deepseek-ai/dsh-agent": "vendor/dsh/agent.js",
|
|
29
33
|
"@deepseek-ai/dsh-system-prompt": "vendor/dsh/system-prompt.js",
|
|
30
|
-
"@deepseek-ai/dsh-agent-loop": "vendor/dsh/agent-loop.js"
|
|
31
|
-
"@deepseek-ai/dsh-environment": "compose/shims/dsh-environment.js",
|
|
32
|
-
"@deepseek-ai/dsh-user-id": "compose/shims/dsh-anonymous-user-id.js"
|
|
34
|
+
"@deepseek-ai/dsh-agent-loop": "vendor/dsh/agent-loop.js"
|
|
33
35
|
},
|
|
34
36
|
"runtime": {
|
|
35
37
|
"rebon": "./bridge.mjs",
|
|
@@ -28,10 +28,21 @@ import RebonToolsRuntime from './tools-runtime.mjs';
|
|
|
28
28
|
import RebonWebRuntime from './web-runtime.mjs';
|
|
29
29
|
import RebonSystemPromptRuntime from './systemprompt-runtime.mjs';
|
|
30
30
|
import RebonCommandsRuntime from './commands-runtime.mjs';
|
|
31
|
+
import RebonWorkingDirectory from './working-directory-runtime.mjs';
|
|
32
|
+
import RebonSubprocess from './subprocess-runtime.mjs';
|
|
33
|
+
import { RebonConfigEditor, RebonLoader, RebonProfileContext } from './loader-runtime.mjs';
|
|
34
|
+
import { mounting, track, untrack } from './entries.mjs';
|
|
31
35
|
|
|
32
36
|
/** The built-in seats, in mount order: a dsh `inject` is satisfied at mount
|
|
33
|
-
* time, so a seat that arrives after the plugin needing it never arrives.
|
|
34
|
-
|
|
37
|
+
* time, so a seat that arrives after the plugin needing it never arrives.
|
|
38
|
+
* The last five are services an upstream package injects that dsh's boot
|
|
39
|
+
* layer and session model provide — the composition answers them itself
|
|
40
|
+
* (see `working-directory-runtime.mjs`, `subprocess-runtime.mjs` and
|
|
41
|
+
* `loader-runtime.mjs`). */
|
|
42
|
+
const BUILTIN_SEATS = [
|
|
43
|
+
'llm', 'credentials', 'tools', 'web', 'systemPrompt', 'commands',
|
|
44
|
+
'workingDirectory', 'subprocess', 'loader', 'profileContext', 'configEditor',
|
|
45
|
+
];
|
|
35
46
|
|
|
36
47
|
let state;
|
|
37
48
|
|
|
@@ -90,7 +101,7 @@ function place(nodes, parent, placements, groups, realms) {
|
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/// Creates the realm. Called once, by the composition control plugin.
|
|
93
|
-
export async function createRealm({ web = null, entries = [] } = {}) {
|
|
104
|
+
export async function createRealm({ web = null, entries = [], profile = null } = {}) {
|
|
94
105
|
if (state !== undefined) {
|
|
95
106
|
throw new ComposeError('[REALM_EXISTS]', 'the composition realm is already created');
|
|
96
107
|
}
|
|
@@ -106,6 +117,11 @@ export async function createRealm({ web = null, entries = [] } = {}) {
|
|
|
106
117
|
await ctx.plugin(RebonWebRuntime, { web });
|
|
107
118
|
await ctx.plugin(RebonSystemPromptRuntime);
|
|
108
119
|
await ctx.plugin(RebonCommandsRuntime);
|
|
120
|
+
await ctx.plugin(RebonWorkingDirectory);
|
|
121
|
+
await ctx.plugin(RebonSubprocess);
|
|
122
|
+
await ctx.plugin(RebonLoader);
|
|
123
|
+
await ctx.plugin(RebonProfileContext, profile ?? {});
|
|
124
|
+
await ctx.plugin(RebonConfigEditor, profile ?? {});
|
|
109
125
|
|
|
110
126
|
state = { ctx, placements, groups, realms, fibers: new Map(), sinks: new Map() };
|
|
111
127
|
return state;
|
|
@@ -150,7 +166,11 @@ function requiredInjects(plugin) {
|
|
|
150
166
|
/// The sink rides on the entry's own derived context, which is what lets a seat
|
|
151
167
|
/// attribute a registration synchronously — the moment a plugin registers, the
|
|
152
168
|
/// context it registered through says which plugin it was.
|
|
153
|
-
export async function mountEntry(request, plugin,
|
|
169
|
+
export async function mountEntry(request, plugin, options = {}) {
|
|
170
|
+
return mounting(() => mount(request, plugin, options));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function mount(request, plugin, { probe = false } = {}) {
|
|
154
174
|
const live = realm();
|
|
155
175
|
const { pluginId } = request;
|
|
156
176
|
if (live.groups.has(pluginId)) {
|
|
@@ -199,6 +219,7 @@ export async function mountEntry(request, plugin, { probe = false } = {}) {
|
|
|
199
219
|
sink.seal();
|
|
200
220
|
live.fibers.set(pluginId, fiber);
|
|
201
221
|
live.sinks.set(pluginId, sink);
|
|
222
|
+
track(pluginId, request, fiber);
|
|
202
223
|
return sink;
|
|
203
224
|
}
|
|
204
225
|
|
|
@@ -209,6 +230,7 @@ export async function disposeEntry(pluginId) {
|
|
|
209
230
|
const fiber = state.fibers.get(pluginId);
|
|
210
231
|
state.fibers.delete(pluginId);
|
|
211
232
|
state.sinks.delete(pluginId);
|
|
233
|
+
untrack(pluginId);
|
|
212
234
|
if (fiber === undefined) return;
|
|
213
235
|
await fiber.dispose();
|
|
214
236
|
}
|
|
@@ -176,13 +176,22 @@ export class RegistrationSink {
|
|
|
176
176
|
/// What the `compose` service reports for this plugin.
|
|
177
177
|
report() {
|
|
178
178
|
return {
|
|
179
|
-
sections: this.sections
|
|
179
|
+
sections: this.sections
|
|
180
|
+
.map((section) => ({ ...section, text: resolvedText(section.text) }))
|
|
181
|
+
.filter((section) => section.text.length > 0),
|
|
180
182
|
providers: [...this.catalogs].map(([provider, catalog]) => ({ provider, ...catalog })),
|
|
181
183
|
webProviders: this.webProviders.map((entry) => ({ ...entry })),
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
/// A section's text as rebon receives it: a provider is asked once, with the
|
|
189
|
+
/// empty AssembleContext a whole-composition prompt is assembled with.
|
|
190
|
+
function resolvedText(text) {
|
|
191
|
+
const value = typeof text === 'function' ? text({}) : text;
|
|
192
|
+
return typeof value === 'string' ? value : '';
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
/// The session this plugin is attached to, if it is attached to one.
|
|
187
196
|
///
|
|
188
197
|
/// With more than one open, the first is used: a plugin acting on its own
|