@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.
@@ -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([...payloadModules(dir), ...localModules()]);
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 state = { map };
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
- return next(specifier, context);
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 state.map;
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
- // Deliberately NOT provided (v1 honesty boundary, unchanged): provider-function
17
- // `text` (dsh resolves it per assembly with an AssembleContext this side does
18
- // not have — a static composition registers static text), `complete` sections
19
- // (replacing rebon's own prompt belongs to the loop seat, not a side door),
20
- // `context()` / variables / tool-order surfaces. All fail loudly rather than
21
- // silently degrade.
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 === 'function') {
41
- throw new Error(
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(() => {});