@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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// The Host's half of DeepSeek Harness's client contract, for what this realm
|
|
2
|
+
// owns.
|
|
3
|
+
//
|
|
4
|
+
// A dsh client plugin (`lib/client.js`, React on a page) reaches the Host
|
|
5
|
+
// through `ctx.remote.<namespace>.<method>(...)`. dsh's gateway turns that
|
|
6
|
+
// into a call on a Host service with exact named arguments and answers
|
|
7
|
+
// `{ ok: true, value }` or `{ ok: false, error: { code, message, details } }`
|
|
8
|
+
// — never a thrown carrier fault. Rebon's plane is the carrier: the desktop
|
|
9
|
+
// app (or a test) sends `{ endpoint: 'settings/describe', args: {...} }` to
|
|
10
|
+
// the composition's `compose` service as `kind: 'remote'`, and this module
|
|
11
|
+
// answers the namespaces whose facts live in the realm:
|
|
12
|
+
//
|
|
13
|
+
// * `settings` — upstream `@deepseek-ai/dsh-settings` over rebon's
|
|
14
|
+
// configuration editor, read redacted and written with dsh's revision
|
|
15
|
+
// fence, the way dsh's `settings-controller` does;
|
|
16
|
+
// * `pluginInventory` — the entries mounted here, with their phases;
|
|
17
|
+
// * `rebon/clientModules` — rebon's own: which mounted entries ship a
|
|
18
|
+
// browser half (`package.json` `dsh.client`), for the client realm to
|
|
19
|
+
// load. dsh injects this as `__DSH_BOOT__`; rebon asks for it.
|
|
20
|
+
//
|
|
21
|
+
// Namespaces this realm does not hold (`pluginManager`, `credentials`,
|
|
22
|
+
// sessions, …) are the plane's to answer or `gateway/method-unavailable`, as
|
|
23
|
+
// dsh's gateway says
|
|
24
|
+
// for an endpoint no live service claims. The events the client hears
|
|
25
|
+
// (`settings/document-updated`, …) are published by the control plugin on
|
|
26
|
+
// `compose:remote-event`, see `plugin.mjs`.
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { realm } from './realm.mjs';
|
|
30
|
+
import { entries } from './entries.mjs';
|
|
31
|
+
|
|
32
|
+
/** The forwarded events dsh's remotes allowlist names, that a realm emits. */
|
|
33
|
+
export const REMOTE_EVENTS = Object.freeze([
|
|
34
|
+
'settings/document-updated',
|
|
35
|
+
'credentials/record-updated',
|
|
36
|
+
'credentials/reference-updated',
|
|
37
|
+
'llm/adapters-updated',
|
|
38
|
+
'commands/change',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
class RemoteError extends Error {
|
|
42
|
+
constructor(code, message, details = {}) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.details = details;
|
|
46
|
+
this.name = 'RemoteError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// One redacted descriptor as the wire carries it, field by field
|
|
51
|
+
/// (`settings-controller`'s `namespaceView`).
|
|
52
|
+
function namespaceView(descriptor) {
|
|
53
|
+
return {
|
|
54
|
+
ns: String(descriptor.ns),
|
|
55
|
+
autoGenerate: descriptor.autoGenerate,
|
|
56
|
+
schema: descriptor.schema,
|
|
57
|
+
value: descriptor.value,
|
|
58
|
+
...(descriptor.base === undefined ? {} : { base: descriptor.base }),
|
|
59
|
+
...(descriptor.user === undefined ? {} : { user: descriptor.user }),
|
|
60
|
+
applies: descriptor.applies,
|
|
61
|
+
secrets: (descriptor.secrets ?? []).map((secret) => ({ path: [...secret.path], set: secret.set })),
|
|
62
|
+
revision: descriptor.revision,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function settingsService() {
|
|
67
|
+
const settings = realm().ctx.get('settings');
|
|
68
|
+
if (settings === undefined) {
|
|
69
|
+
throw new RemoteError(
|
|
70
|
+
'gateway/method-unavailable',
|
|
71
|
+
'no settings service is mounted in this composition: install @deepseek-ai/dsh-settings with the packages it configures',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return settings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// A refusal from the settings service, classified as dsh classifies it: a
|
|
78
|
+
/// stale revision is its own outcome, everything else is a rejection.
|
|
79
|
+
function settingsRefusal(ns, error) {
|
|
80
|
+
if (error?.code === 'SETTINGS_CONFLICT' && typeof error.expected === 'number' && typeof error.actual === 'number') {
|
|
81
|
+
return new RemoteError('settings/conflict', String(error.message), { ns, expected: error.expected, actual: error.actual });
|
|
82
|
+
}
|
|
83
|
+
return new RemoteError('settings/rejected', String(error?.message ?? error), { ns });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function settingsWrite(mode, args) {
|
|
87
|
+
const ns = args?.ns;
|
|
88
|
+
if (typeof ns !== 'string' || ns.length === 0) {
|
|
89
|
+
throw new RemoteError('gateway/bad-request', `invalid payload for settings.${mode}`, { field: 'ns' });
|
|
90
|
+
}
|
|
91
|
+
const settings = settingsService();
|
|
92
|
+
const input = mode === 'mutate' ? args.ops : mode === 'replace' ? args.section : args.patch;
|
|
93
|
+
try {
|
|
94
|
+
await settings[mode](ns, input, args.expectedRevision ?? undefined);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw settingsRefusal(ns, error);
|
|
97
|
+
}
|
|
98
|
+
const descriptor = settings.describe({ redactSecrets: true }).find((candidate) => candidate.ns === ns);
|
|
99
|
+
if (descriptor === undefined) {
|
|
100
|
+
throw new RemoteError('gateway/internal', `settings namespace "${ns}" was disposed after the ${mode}`);
|
|
101
|
+
}
|
|
102
|
+
return namespaceView(descriptor);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// dsh's fiber phases, from a Cordis fiber state.
|
|
106
|
+
function phaseOf(fiber) {
|
|
107
|
+
switch (fiber?.state) {
|
|
108
|
+
case 0: return 'pending';
|
|
109
|
+
case 1: return 'loading';
|
|
110
|
+
case 2: return 'active';
|
|
111
|
+
case 3: return 'failed';
|
|
112
|
+
case 4: return 'unloading';
|
|
113
|
+
default: return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// The name of the package an entry's module belongs to, else its id.
|
|
118
|
+
function moduleNameOf(entry) {
|
|
119
|
+
const root = packageRoot(entry);
|
|
120
|
+
if (root !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
123
|
+
if (typeof name === 'string') return name;
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
return entry.options.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function inventory() {
|
|
130
|
+
return {
|
|
131
|
+
// Which entries run is rebon's composition, changed where rebon's
|
|
132
|
+
// plugins are configured; a page reads it.
|
|
133
|
+
managementAvailable: false,
|
|
134
|
+
entries: [...entries.values()].map((entry) => ({
|
|
135
|
+
entryId: entry.id,
|
|
136
|
+
moduleName: moduleNameOf(entry),
|
|
137
|
+
enabled: true,
|
|
138
|
+
fiberPhase: phaseOf(entry.fiber),
|
|
139
|
+
})),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// The client half a package ships, as its `package.json` declares it.
|
|
144
|
+
function clientHalf(root) {
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const client = manifest?.dsh?.client;
|
|
152
|
+
if (client === null || typeof client !== 'object' || client.platform !== 'web') return undefined;
|
|
153
|
+
const exported = manifest.exports?.['./client'];
|
|
154
|
+
const relative = typeof exported === 'string' ? exported : (exported?.default ?? exported?.import ?? 'lib/client.js');
|
|
155
|
+
const file = path.resolve(root, relative);
|
|
156
|
+
let stat;
|
|
157
|
+
try {
|
|
158
|
+
stat = fs.statSync(file);
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
id: manifest.name,
|
|
164
|
+
file,
|
|
165
|
+
// dsh's `rev` changes whenever the bundle does; mtime and size say so.
|
|
166
|
+
rev: `${Math.trunc(stat.mtimeMs)}-${stat.size}`,
|
|
167
|
+
inject: Array.isArray(client.inject) ? [...client.inject] : [],
|
|
168
|
+
immediately: client.immediately === true,
|
|
169
|
+
external: Array.isArray(client.external) ? [...client.external] : [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// The package an entry's module belongs to: the nearest directory above
|
|
174
|
+
/// it holding a `package.json`, the module's own package as Node reads it.
|
|
175
|
+
function packageRoot(entry) {
|
|
176
|
+
if (typeof entry.root !== 'string' || typeof entry.module !== 'string') return undefined;
|
|
177
|
+
let dir = path.dirname(path.resolve(entry.root, entry.module));
|
|
178
|
+
const top = path.resolve(entry.root);
|
|
179
|
+
for (;;) {
|
|
180
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
181
|
+
if (dir === top || path.dirname(dir) === dir) return undefined;
|
|
182
|
+
dir = path.dirname(dir);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clientModules() {
|
|
187
|
+
const rows = [];
|
|
188
|
+
for (const entry of entries.values()) {
|
|
189
|
+
const root = packageRoot(entry);
|
|
190
|
+
if (root === undefined) continue;
|
|
191
|
+
const half = clientHalf(root);
|
|
192
|
+
if (half !== undefined && !rows.some((row) => row.id === half.id)) rows.push({ entryId: entry.id, ...half });
|
|
193
|
+
}
|
|
194
|
+
return { entries: rows };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const ENDPOINTS = {
|
|
198
|
+
'settings/describe': () => {
|
|
199
|
+
const settings = settingsService();
|
|
200
|
+
return {
|
|
201
|
+
writable: settings.writable,
|
|
202
|
+
hasDocument: settings.documentPath !== undefined,
|
|
203
|
+
namespaces: settings.describe({ redactSecrets: true }).map(namespaceView),
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
'settings/update': (args) => settingsWrite('update', args),
|
|
207
|
+
'settings/replace': (args) => settingsWrite('replace', args),
|
|
208
|
+
'settings/mutate': (args) => settingsWrite('mutate', args),
|
|
209
|
+
'pluginInventory/list': inventory,
|
|
210
|
+
'rebon/clientModules': clientModules,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/// Answers one remote call in dsh's wire shape. Never throws: a refusal is a
|
|
214
|
+
/// result the client reads, as dsh's gateway answers.
|
|
215
|
+
export async function remote(endpoint, args = {}) {
|
|
216
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) {
|
|
217
|
+
return { ok: false, error: { code: 'gateway/bad-request', message: 'remote calls expect named arguments', details: {} } };
|
|
218
|
+
}
|
|
219
|
+
const handler = Object.hasOwn(ENDPOINTS, endpoint) ? ENDPOINTS[endpoint] : undefined;
|
|
220
|
+
if (handler === undefined) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: { code: 'gateway/method-unavailable', message: `no live service answers ${JSON.stringify(String(endpoint))} in this composition`, details: {} },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
return { ok: true, value: (await handler(args ?? {})) ?? null };
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const code = error instanceof RemoteError ? error.code : 'gateway/internal';
|
|
230
|
+
return { ok: false, error: { code, message: String(error?.message ?? error), details: error?.details ?? {} } };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Whether this realm answers `endpoint`, so the plane routes to it.
|
|
235
|
+
export function answers(endpoint) {
|
|
236
|
+
return Object.hasOwn(ENDPOINTS, endpoint);
|
|
237
|
+
}
|
|
@@ -26,10 +26,22 @@ import { pathToFileURL } from 'node:url';
|
|
|
26
26
|
import { payloadDir } from './payload.mjs';
|
|
27
27
|
import provided from './provided-modules.json' with { type: 'json' };
|
|
28
28
|
|
|
29
|
-
/// Payload-relative module map, keyed by bare specifier
|
|
30
|
-
///
|
|
31
|
-
/// Mirrors `build_import_map` entry for entry, minus the `node:` shims.
|
|
29
|
+
/// Payload-relative module map, keyed by bare specifier: what rebon owns and
|
|
30
|
+
/// what its vendored payload is built from, together.
|
|
32
31
|
export function payloadModules(dir) {
|
|
32
|
+
return new Map([...ownedModules(dir), ...vendoredModules(dir)]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The specifiers rebon answers for every importer.
|
|
36
|
+
export function ownedModules(dir) {
|
|
37
|
+
const at = (relative) => pathToFileURL(path.join(dir, relative)).href;
|
|
38
|
+
return new Map(Object.entries(provided.owned).map(([name, relative]) => [name, at(relative)]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The vendored packages rebon's own payload imports. A package installed
|
|
42
|
+
/// from npm brings its own copy of these and gets it; only one that has none
|
|
43
|
+
/// falls back to the vendored build.
|
|
44
|
+
export function vendoredModules(dir) {
|
|
33
45
|
const at = (relative) => pathToFileURL(path.join(dir, relative)).href;
|
|
34
46
|
return new Map(Object.entries(provided.payload).map(([name, relative]) => [name, at(relative)]));
|
|
35
47
|
}
|
|
@@ -51,20 +63,25 @@ let installed;
|
|
|
51
63
|
/// Returns the resolved map so callers can report what a specifier became
|
|
52
64
|
/// without repeating the lookup rules.
|
|
53
65
|
export function installModuleResolution({ dir = payloadDir(), extraModules = {} } = {}) {
|
|
54
|
-
const map = new Map([...
|
|
66
|
+
const map = new Map([...ownedModules(dir), ...localModules()]);
|
|
55
67
|
for (const [name, file] of Object.entries(extraModules)) {
|
|
56
68
|
map.set(name, pathToFileURL(path.resolve(file)).href);
|
|
57
69
|
}
|
|
70
|
+
const vendored = vendoredModules(dir);
|
|
58
71
|
if (installed) {
|
|
59
72
|
// Hooks are process-wide and cannot be unregistered per call; re-running
|
|
60
73
|
// with a different map would leave two resolvers disagreeing about the
|
|
61
74
|
// same specifier, which is a debugging trap rather than a feature.
|
|
62
75
|
for (const [name, url] of map) installed.map.set(name, url);
|
|
63
|
-
return installed.map;
|
|
76
|
+
return new Map([...installed.vendored, ...installed.map]);
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
const payloadPrefix = pathToFileURL(dir.endsWith(path.sep) ? dir : dir + path.sep).href;
|
|
67
|
-
const
|
|
80
|
+
const runtimePrefix = new URL('./', import.meta.url).href;
|
|
81
|
+
// Rebon's own code — the payload and this runtime — always gets the
|
|
82
|
+
// vendored build it was written against.
|
|
83
|
+
const ours = (parent) => parent === undefined || parent.startsWith(payloadPrefix) || parent.startsWith(runtimePrefix);
|
|
84
|
+
const state = { map, vendored };
|
|
68
85
|
registerHooks({
|
|
69
86
|
resolve(specifier, context, next) {
|
|
70
87
|
// The native Cordis, where the host defines it (cordis-rs on Verve's
|
|
@@ -75,7 +92,16 @@ export function installModuleResolution({ dir = payloadDir(), extraModules = {}
|
|
|
75
92
|
}
|
|
76
93
|
const mapped = state.map.get(specifier);
|
|
77
94
|
if (mapped !== undefined) return { url: mapped, format: 'module', shortCircuit: true };
|
|
78
|
-
|
|
95
|
+
const fallback = state.vendored.get(specifier);
|
|
96
|
+
if (fallback === undefined) return next(specifier, context);
|
|
97
|
+
if (ours(context?.parentURL)) return { url: fallback, format: 'module', shortCircuit: true };
|
|
98
|
+
// A package from npm: its own copy if it installed one — dsh's current
|
|
99
|
+
// packages need their own versions of these — else the vendored one.
|
|
100
|
+
try {
|
|
101
|
+
return next(specifier, context);
|
|
102
|
+
} catch {
|
|
103
|
+
return { url: fallback, format: 'module', shortCircuit: true };
|
|
104
|
+
}
|
|
79
105
|
},
|
|
80
106
|
// The payload's ESM is `.js` under no `package.json`, so Node's own rules
|
|
81
107
|
// read it as CommonJS and every `import` in it is a syntax error. The
|
|
@@ -87,5 +113,5 @@ export function installModuleResolution({ dir = payloadDir(), extraModules = {}
|
|
|
87
113
|
},
|
|
88
114
|
});
|
|
89
115
|
installed = state;
|
|
90
|
-
return
|
|
116
|
+
return new Map([...vendored, ...map]);
|
|
91
117
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The composition's `ctx.subprocess` seat: dsh's subprocess seam, with rebon
|
|
2
|
+
// starting the program.
|
|
3
|
+
//
|
|
4
|
+
// DeepSeek Harness's shell executors (`dsh-bash-local`, `dsh-pwsh-local`)
|
|
5
|
+
// run commands through `ctx.subprocess.spawn(spec)`. dsh's own backend,
|
|
6
|
+
// `dsh-subprocess-local`, is `child_process` plus native addons (node-pty,
|
|
7
|
+
// and koffi for Windows job objects), none of which a plugin on Verve has.
|
|
8
|
+
// So the realm answers the seam itself, and the program is started by rebon
|
|
9
|
+
// through the `process` seat (`crates/rebon-plugin-host/src/process_seat.rs`):
|
|
10
|
+
// on the shared host only, inside a composition tool call the person
|
|
11
|
+
// allowed.
|
|
12
|
+
//
|
|
13
|
+
// The contract kept is dsh's (`@deepseek-ai/dsh-subprocess`): `spawn`
|
|
14
|
+
// returns a live handle synchronously; `done` settles with
|
|
15
|
+
// `{ exitCode, signal }`; collected streams are read by offset with
|
|
16
|
+
// `readFrom`, keep their tail within `maxBytes`, and say when a read was
|
|
17
|
+
// lossy; `terminate` (and the spec's abort signal) ends the program;
|
|
18
|
+
// `waitForExit` waits for it. What is not offered is refused by name rather
|
|
19
|
+
// than half-done: raw `pipe`/`inherit` streams, a control channel, spill
|
|
20
|
+
// files and terminals.
|
|
21
|
+
import { Service } from 'cordis';
|
|
22
|
+
import { seat } from './bridge.mjs';
|
|
23
|
+
|
|
24
|
+
class SubprocessError extends Error {
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(`${code} ${message}`);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = 'SubprocessError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// How long one `wait` holds on rebon's side before answering with nothing.
|
|
33
|
+
const WAIT_SLICE_MS = 10_000;
|
|
34
|
+
|
|
35
|
+
/// One collect-mode stream: the retained tail, by whole-stream byte offset.
|
|
36
|
+
class Collected {
|
|
37
|
+
constructor(maxBytes) {
|
|
38
|
+
this.maxBytes = Math.max(0, Math.floor(maxBytes));
|
|
39
|
+
this.bytes = Buffer.alloc(0);
|
|
40
|
+
/** Whole-stream offset of `bytes[0]`. */
|
|
41
|
+
this.start = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
push(text, dropped = 0) {
|
|
45
|
+
if (dropped > 0) {
|
|
46
|
+
// 丢失发生在旧尾部之后;只保留新连续区间,否则 readFrom 会返回不存在的拼接。
|
|
47
|
+
this.start += this.bytes.length + dropped;
|
|
48
|
+
this.bytes = Buffer.alloc(0);
|
|
49
|
+
}
|
|
50
|
+
if (text.length === 0) return;
|
|
51
|
+
this.bytes = Buffer.concat([this.bytes, Buffer.from(text, 'utf8')]);
|
|
52
|
+
if (this.bytes.length > this.maxBytes) {
|
|
53
|
+
const excess = this.bytes.length - this.maxBytes;
|
|
54
|
+
this.bytes = this.bytes.subarray(excess);
|
|
55
|
+
this.start += excess;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// dsh's `SubprocessOutputReader.readFrom`.
|
|
60
|
+
readFrom(fromByte) {
|
|
61
|
+
const end = this.start + this.bytes.length;
|
|
62
|
+
if (fromByte < this.start) {
|
|
63
|
+
return { text: this.bytes.toString('utf8'), nextOffset: end, lossy: true };
|
|
64
|
+
}
|
|
65
|
+
const at = Math.min(fromByte, end) - this.start;
|
|
66
|
+
return { text: this.bytes.subarray(at).toString('utf8'), nextOffset: end, lossy: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectMode(mode, stream) {
|
|
71
|
+
if (mode !== null && typeof mode === 'object' && Number.isFinite(mode.maxBytes)) return mode;
|
|
72
|
+
throw new SubprocessError(
|
|
73
|
+
'[UNSUPPORTED]',
|
|
74
|
+
`${stream} ${JSON.stringify(mode)} is not offered: rebon's subprocess seam collects output (\`{ maxBytes }\`)`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default class RebonSubprocess extends Service {
|
|
79
|
+
constructor(ctx) {
|
|
80
|
+
super(ctx, 'subprocess');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// dsh `SubprocessRuntime.spawn`.
|
|
84
|
+
spawn(spec) {
|
|
85
|
+
const argv = spec?.argv;
|
|
86
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv.every((part) => typeof part === 'string')) {
|
|
87
|
+
throw new TypeError('subprocess.spawn needs a non-empty argv of strings');
|
|
88
|
+
}
|
|
89
|
+
const stdio = spec.stdio ?? {};
|
|
90
|
+
if (stdio.control !== undefined) {
|
|
91
|
+
throw new SubprocessError('[UNSUPPORTED]', 'a control channel is not offered by rebon\'s subprocess seam');
|
|
92
|
+
}
|
|
93
|
+
const stdin = stdio.stdin;
|
|
94
|
+
if (stdin !== 'ignore' && !(stdin !== null && typeof stdin === 'object' && typeof stdin.data === 'string')) {
|
|
95
|
+
throw new SubprocessError('[UNSUPPORTED]', `stdin ${JSON.stringify(stdin)} is not offered: use 'ignore' or { data }`);
|
|
96
|
+
}
|
|
97
|
+
const stdout = new Collected(collectMode(stdio.stdout, 'stdout').maxBytes);
|
|
98
|
+
const stderr = new Collected(collectMode(stdio.stderr, 'stderr').maxBytes);
|
|
99
|
+
// `undefined` is dsh's tombstone for an ambient entry; JSON says null.
|
|
100
|
+
const env = Object.fromEntries(Object.entries(spec.env ?? {}).map(([name, value]) => [name, value ?? null]));
|
|
101
|
+
const via = this.ctx;
|
|
102
|
+
let id;
|
|
103
|
+
let terminated = false;
|
|
104
|
+
let exited = false;
|
|
105
|
+
const exitWaiters = new Set();
|
|
106
|
+
const kill = () => {
|
|
107
|
+
if (id === undefined || exited) return;
|
|
108
|
+
void seat('process', 'kill', { id }, { via }).catch(() => {});
|
|
109
|
+
};
|
|
110
|
+
const onAbort = () => terminate();
|
|
111
|
+
const done = (async () => {
|
|
112
|
+
const started = await seat('process', 'spawn', {
|
|
113
|
+
argv: [...argv],
|
|
114
|
+
...(typeof spec.cwd === 'string' ? { cwd: spec.cwd } : {}),
|
|
115
|
+
env,
|
|
116
|
+
...(typeof stdin === 'object' ? { stdin: stdin.data } : {}),
|
|
117
|
+
}, { via });
|
|
118
|
+
id = started.id;
|
|
119
|
+
if (terminated) kill();
|
|
120
|
+
for (;;) {
|
|
121
|
+
const answer = await seat('process', 'wait', { id, timeoutMs: WAIT_SLICE_MS }, { via });
|
|
122
|
+
stdout.push(answer.stdout?.text ?? '', answer.stdout?.dropped ?? 0);
|
|
123
|
+
stderr.push(answer.stderr?.text ?? '', answer.stderr?.dropped ?? 0);
|
|
124
|
+
if (answer.exit !== null && answer.exit !== undefined) {
|
|
125
|
+
return { exitCode: answer.exit.exitCode ?? null, signal: answer.exit.signal ?? null };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})().finally(() => {
|
|
129
|
+
exited = true;
|
|
130
|
+
spec.signal?.removeEventListener?.('abort', onAbort);
|
|
131
|
+
for (const resolve of exitWaiters) resolve(true);
|
|
132
|
+
exitWaiters.clear();
|
|
133
|
+
});
|
|
134
|
+
const terminate = () => {
|
|
135
|
+
terminated = true;
|
|
136
|
+
kill();
|
|
137
|
+
};
|
|
138
|
+
if (spec.signal?.aborted) terminate();
|
|
139
|
+
spec.signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
140
|
+
return {
|
|
141
|
+
stdin: undefined,
|
|
142
|
+
stdout: undefined,
|
|
143
|
+
stderr: undefined,
|
|
144
|
+
control: undefined,
|
|
145
|
+
collected: { stdout, stderr },
|
|
146
|
+
done,
|
|
147
|
+
terminate,
|
|
148
|
+
waitForExit(signal) {
|
|
149
|
+
if (exited) return Promise.resolve(true);
|
|
150
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const finish = (value) => {
|
|
153
|
+
exitWaiters.delete(finish);
|
|
154
|
+
signal?.removeEventListener?.('abort', abort);
|
|
155
|
+
resolve(value);
|
|
156
|
+
};
|
|
157
|
+
const abort = () => finish(false);
|
|
158
|
+
exitWaiters.add(finish);
|
|
159
|
+
signal?.addEventListener?.('abort', abort, { once: true });
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// dsh's terminal primitive, which rebon does not offer.
|
|
166
|
+
spawnTerminal() {
|
|
167
|
+
throw new SubprocessError('[UNSUPPORTED]', 'terminals are not offered by rebon\'s subprocess seam');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -13,15 +13,77 @@
|
|
|
13
13
|
// to rebon with the rest of the load report, and withdrawn when the plugin
|
|
14
14
|
// unloads.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
16
|
+
// Provider-function `text` is resolved once, when rebon asks the entry what it
|
|
17
|
+
// registered — after the entry finished loading, so a section that says
|
|
18
|
+
// something about the entry's own tools (`ctx.tools.get('read', scope)`) sees
|
|
19
|
+
// them. It is resolved with an empty AssembleContext: rebon assembles one
|
|
20
|
+
// prompt from what a composition reported, so the global, subject-less
|
|
21
|
+
// answer is the one there is. A section whose text resolves empty
|
|
22
|
+
// contributes nothing, as in dsh.
|
|
23
|
+
//
|
|
24
|
+
// Deliberately NOT provided (v1 honesty boundary, unchanged): `complete`
|
|
25
|
+
// sections (replacing rebon's own prompt belongs to the loop seat, not a side
|
|
26
|
+
// door), `context()` / variables / tool-order surfaces. All fail loudly rather
|
|
27
|
+
// than silently degrade.
|
|
22
28
|
import { Service } from 'cordis';
|
|
23
29
|
import { sinkOf } from './registry.mjs';
|
|
24
30
|
|
|
31
|
+
/** dsh's centrally allocated section positions (dsh-system-prompt
|
|
32
|
+
* `SECTION_ORDERS`), so a package placing itself by name lands where it
|
|
33
|
+
* would in dsh. */
|
|
34
|
+
const SECTION_ORDERS = Object.freeze({
|
|
35
|
+
HARNESS_IDENTITY: -1000,
|
|
36
|
+
DEPLOYMENT_PERSONA_PREFIX: 0,
|
|
37
|
+
PLAN_POLICY: 500,
|
|
38
|
+
TEAM_POLICY: 600,
|
|
39
|
+
PTC_ONLY: 800,
|
|
40
|
+
FILE_REFERENCE: 900,
|
|
41
|
+
TOOL_BASH: 1000,
|
|
42
|
+
TOOL_PWSH: 1010,
|
|
43
|
+
TOOL_READ: 1100,
|
|
44
|
+
TOOL_WRITE: 1200,
|
|
45
|
+
TOOL_EDIT: 1300,
|
|
46
|
+
TOOL_GLOB: 1400,
|
|
47
|
+
TOOL_GREP: 1500,
|
|
48
|
+
TOOL_JOBS: 1600,
|
|
49
|
+
TOOL_PTY: 1700,
|
|
50
|
+
TOOL_WEB_SEARCH: 2000,
|
|
51
|
+
TOOL_WEB_FETCH: 2100,
|
|
52
|
+
TOOL_LSP: 2200,
|
|
53
|
+
TOOL_SESSION_QUERY: 2300,
|
|
54
|
+
TOOL_GOAL: 2400,
|
|
55
|
+
TOOL_WORKFLOW: 2600,
|
|
56
|
+
TOOL_RALPH: 2700,
|
|
57
|
+
TOOL_SUBAGENT: 2800,
|
|
58
|
+
TOOL_REPORT: 2900,
|
|
59
|
+
TOOL_COMPUTER_USE: 3000,
|
|
60
|
+
MCP_SERVERS: 3100,
|
|
61
|
+
TOOLS_SDK: 5000,
|
|
62
|
+
DELIVERABLE_FILE_REFERENCES: 9000,
|
|
63
|
+
STRUCTURED_OUTPUT: 9900,
|
|
64
|
+
HARNESS_SOURCE: 10000,
|
|
65
|
+
WEB_SURFACE: 10100,
|
|
66
|
+
DEPLOYMENT_PERSONA_SUFFIX: 10200,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** dsh's runtime-context positions (`CONTEXT_ORDERS`). */
|
|
70
|
+
const CONTEXT_ORDERS = Object.freeze({
|
|
71
|
+
WORKING_DIRECTORY: 100,
|
|
72
|
+
SANDBOX_POLICY: 110,
|
|
73
|
+
APPROVAL_POLICY: 115,
|
|
74
|
+
SUBAGENT_DELEGATION: 120,
|
|
75
|
+
TEAM_IDENTITY: 125,
|
|
76
|
+
HARNESS_SOURCE: 200,
|
|
77
|
+
WEB_SURFACE: 210,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function placement(table, kind, name) {
|
|
81
|
+
if (!Object.hasOwn(table, name)) {
|
|
82
|
+
throw new TypeError(`unknown prompt ${kind} placement ${JSON.stringify(name)}`);
|
|
83
|
+
}
|
|
84
|
+
return table[name];
|
|
85
|
+
}
|
|
86
|
+
|
|
25
87
|
export default class RebonSystemPromptRuntime extends Service {
|
|
26
88
|
constructor(ctx) {
|
|
27
89
|
super(ctx, 'systemPrompt');
|
|
@@ -37,13 +99,8 @@ export default class RebonSystemPromptRuntime extends Service {
|
|
|
37
99
|
if (!Number.isFinite(entry.order)) {
|
|
38
100
|
throw new TypeError(`prompt section "${name}" order must be a finite number`);
|
|
39
101
|
}
|
|
40
|
-
if (typeof entry.text
|
|
41
|
-
throw new
|
|
42
|
-
`prompt section "${name}": provider-function text is not supported in the composition (register static text)`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (typeof entry.text !== 'string') {
|
|
46
|
-
throw new TypeError(`prompt section "${name}" text must be a string`);
|
|
102
|
+
if (typeof entry.text !== 'string' && typeof entry.text !== 'function') {
|
|
103
|
+
throw new TypeError(`prompt section "${name}" text must be a string or a provider`);
|
|
47
104
|
}
|
|
48
105
|
if (entry.complete) {
|
|
49
106
|
throw new Error(
|
|
@@ -64,4 +121,14 @@ export default class RebonSystemPromptRuntime extends Service {
|
|
|
64
121
|
};
|
|
65
122
|
});
|
|
66
123
|
}
|
|
124
|
+
|
|
125
|
+
/** dsh `getSectionOrder`: a section's centrally owned position. */
|
|
126
|
+
getSectionOrder(name) {
|
|
127
|
+
return placement(SECTION_ORDERS, 'section', name);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** dsh `getContextOrder`: a runtime context's centrally owned position. */
|
|
131
|
+
getContextOrder(name) {
|
|
132
|
+
return placement(CONTEXT_ORDERS, 'context', name);
|
|
133
|
+
}
|
|
67
134
|
}
|
|
@@ -148,6 +148,17 @@ export default class RebonToolsRuntime extends Service {
|
|
|
148
148
|
return { kind: 'exclusive' };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* dsh `ToolRuntime.get`: the definition registered under `name` in this
|
|
153
|
+
* composition, or undefined. The scope is accepted and not consulted:
|
|
154
|
+
* rebon offers a composition's tools to every session alike, so the
|
|
155
|
+
* global answer is the scoped one. A prompt section that mentions a tool
|
|
156
|
+
* only while it exists asks this.
|
|
157
|
+
*/
|
|
158
|
+
get(name, _scope) {
|
|
159
|
+
return this.defs.get(name);
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
/**
|
|
152
163
|
* Register one dsh-shaped tool definition. Mirrors dsh
|
|
153
164
|
* `ToolRuntime.register` validation; returns the exact disposer.
|
|
@@ -222,7 +233,17 @@ export default class RebonToolsRuntime extends Service {
|
|
|
222
233
|
// On the plane that is a published event: the tool is telling rebon
|
|
223
234
|
// something happened, and rebon decides who hears it.
|
|
224
235
|
agent: {
|
|
236
|
+
// The session's scope is the agent's identity here: rebon's engine
|
|
237
|
+
// runs the turn, and the scope is what rebon calls its session.
|
|
238
|
+
id: callCtx.scopeId,
|
|
225
239
|
session: {
|
|
240
|
+
id: callCtx.scopeId,
|
|
241
|
+
// What a dsh tool reads off the session's header: its id (the
|
|
242
|
+
// shell exports it as `DSH_SESSION_ID`) and where it started.
|
|
243
|
+
header: Object.freeze({ id: callCtx.scopeId, cwd: callCtx.workspaceRoot }),
|
|
244
|
+
// A session's model request settings are rebon's own; a tool that
|
|
245
|
+
// asks finds none and takes its default.
|
|
246
|
+
requestHeader: () => undefined,
|
|
226
247
|
append(type, data) {
|
|
227
248
|
void publish(SESSION_APPEND_TOPIC, { type, data, tool: name, callId })
|
|
228
249
|
.catch(() => {});
|