borgmcp 2.11.0 → 2.13.0
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/README.md +7 -3
- package/dist/assimilate-cmd.d.ts +5 -2
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +11 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +23 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +49 -29
- package/dist/claude.js.map +1 -1
- package/dist/cli-platform.d.ts +1 -0
- package/dist/cli-platform.d.ts.map +1 -1
- package/dist/cli-platform.js +10 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/cli-tool-approval.d.ts +3 -1
- package/dist/cli-tool-approval.d.ts.map +1 -1
- package/dist/cli-tool-approval.js +8 -14
- package/dist/cli-tool-approval.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -27
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +8 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +46 -0
- package/dist/opencode-drone.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +5 -2
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/remote-client.d.ts +5 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +36 -11
- package/dist/remote-client.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +4 -4
- package/docs/LOCAL_SERVER.md +1 -1
- package/docs/RELEASING.md +4 -4
- package/package.json +3 -3
- package/src/assimilate-cmd.ts +18 -7
- package/src/assimilate-deps.ts +3 -2
- package/src/claude.ts +72 -30
- package/src/cli-platform.ts +16 -2
- package/src/cli-tool-approval.ts +9 -16
- package/src/index.ts +65 -26
- package/src/opencode-drone.ts +57 -0
- package/src/parse-assimilate-args.ts +4 -2
- package/src/remote-client.ts +41 -14
package/src/claude.ts
CHANGED
|
@@ -81,13 +81,63 @@ import {
|
|
|
81
81
|
} from './config-utils.js';
|
|
82
82
|
import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
83
83
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
84
|
-
import {
|
|
84
|
+
import { allocateOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff, openCodeLaunchBinding } from './opencode-drone.js';
|
|
85
85
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
86
86
|
import { isClientOwnedCubeInitArgv, runEarlyServerFacade } from './server-facade.js';
|
|
87
87
|
import { runEarlyUpdate } from './update-cmd.js';
|
|
88
88
|
|
|
89
89
|
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
90
90
|
|
|
91
|
+
export function createOpenCodeLaunchPlan(
|
|
92
|
+
cwd: string,
|
|
93
|
+
port: number,
|
|
94
|
+
prompt: string,
|
|
95
|
+
passthroughArgs: string[] = [],
|
|
96
|
+
): { launchArgs: string[]; envPort: string; serverUrl: string } {
|
|
97
|
+
const binding = openCodeLaunchBinding(port);
|
|
98
|
+
return {
|
|
99
|
+
launchArgs: buildOpenCodeLaunchArgs(cwd, Number(binding.cliPort), prompt, passthroughArgs),
|
|
100
|
+
envPort: binding.envPort,
|
|
101
|
+
serverUrl: binding.serverUrl,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function launchOpenCodeProcess(options: {
|
|
106
|
+
cwd: string;
|
|
107
|
+
port: number;
|
|
108
|
+
prompt: string;
|
|
109
|
+
passthroughArgs: string[];
|
|
110
|
+
env: NodeJS.ProcessEnv;
|
|
111
|
+
droneLabel: string;
|
|
112
|
+
cubeName: string;
|
|
113
|
+
kickoff: ReturnType<typeof createOpenCodeLaunchKickoff>;
|
|
114
|
+
spawnProcess?: typeof spawn;
|
|
115
|
+
connect?: typeof connectOpenCodeDrone;
|
|
116
|
+
}): {
|
|
117
|
+
launchArgs: string[];
|
|
118
|
+
launchEnv: NodeJS.ProcessEnv;
|
|
119
|
+
process: ReturnType<typeof spawn>;
|
|
120
|
+
} {
|
|
121
|
+
const plan = createOpenCodeLaunchPlan(options.cwd, options.port, options.prompt, options.passthroughArgs);
|
|
122
|
+
const launchEnv = { ...options.env, BORG_OPENCODE_PORT: plan.envPort };
|
|
123
|
+
// OpenCode's bind can still race this allocation; client#298 tracks the
|
|
124
|
+
// residual pre-bind window outside this slice.
|
|
125
|
+
const child = (options.spawnProcess ?? spawn)('opencode', plan.launchArgs, {
|
|
126
|
+
stdio: 'inherit',
|
|
127
|
+
shell: false,
|
|
128
|
+
env: launchEnv,
|
|
129
|
+
});
|
|
130
|
+
(options.connect ?? connectOpenCodeDrone)({
|
|
131
|
+
serverUrl: plan.serverUrl,
|
|
132
|
+
directory: options.cwd,
|
|
133
|
+
droneLabel: options.droneLabel,
|
|
134
|
+
cubeName: options.cubeName,
|
|
135
|
+
})
|
|
136
|
+
.then(() => injectInitialKickoff(options.kickoff))
|
|
137
|
+
.catch(() => {});
|
|
138
|
+
return { launchArgs: plan.launchArgs, launchEnv, process: child };
|
|
139
|
+
}
|
|
140
|
+
|
|
91
141
|
export async function runAssimilateEntry(
|
|
92
142
|
args: readonly string[],
|
|
93
143
|
buildDeps: AssimilateDepsBuilder = buildDefaultAssimilateDeps,
|
|
@@ -346,8 +396,8 @@ async function main() {
|
|
|
346
396
|
}
|
|
347
397
|
|
|
348
398
|
// client#20: inspect only the SELECTED harness after the one-shot launch
|
|
349
|
-
// menu choice.
|
|
350
|
-
//
|
|
399
|
+
// menu choice. A narrow per-process override is applied by default; Borg
|
|
400
|
+
// never rewrites the user's approval policy here.
|
|
351
401
|
const approvalCwd = cli === 'codex'
|
|
352
402
|
? resolveCodexLaunchCwd(parsedCli.rest, process.cwd())
|
|
353
403
|
: process.cwd();
|
|
@@ -357,7 +407,8 @@ async function main() {
|
|
|
357
407
|
cwd: approvalCwd,
|
|
358
408
|
env: process.env,
|
|
359
409
|
codexArgs: parsedCli.rest,
|
|
360
|
-
})
|
|
410
|
+
}),
|
|
411
|
+
{ skipOverride: parsedCli.noBorgApprovalOverride }
|
|
361
412
|
);
|
|
362
413
|
if (launchApproval.warning) {
|
|
363
414
|
console.error(`${consolePrefix()}${chalk.yellow(`warning: ${launchApproval.warning}`)}`);
|
|
@@ -464,6 +515,7 @@ async function main() {
|
|
|
464
515
|
// their existing launch prompts. OpenCode records this nonce-bearing copy
|
|
465
516
|
// and later uses the nonce to bind its separately spawned MCP child.
|
|
466
517
|
let openCodeKickoff: ReturnType<typeof createOpenCodeLaunchKickoff> | null = null;
|
|
518
|
+
let openCodePort: number | undefined;
|
|
467
519
|
let launchArgs: string[];
|
|
468
520
|
if (cli === 'codex') {
|
|
469
521
|
// gh#673 P1-codex: codex MCP children only see the pinned
|
|
@@ -485,14 +537,12 @@ async function main() {
|
|
|
485
537
|
} else if (cli === 'opencode') {
|
|
486
538
|
// OpenCode launch: start TUI with the kickoff passed via --prompt
|
|
487
539
|
// (auto-submits it as the first message). BORG_SESSION is pinned in
|
|
488
|
-
// opencode.json.
|
|
489
|
-
// to OpenCode's local HTTP API
|
|
490
|
-
|
|
491
|
-
? computeOpenCodePort(active.droneId)
|
|
492
|
-
: 14096;
|
|
540
|
+
// opencode.json. The OS-selected loopback port lets the MCP child connect
|
|
541
|
+
// to OpenCode's local HTTP API without a shared deterministic collision space.
|
|
542
|
+
openCodePort = await allocateOpenCodePort();
|
|
493
543
|
installBorgPlugin();
|
|
494
544
|
openCodeKickoff = createOpenCodeLaunchKickoff(kickoff);
|
|
495
|
-
launchArgs =
|
|
545
|
+
launchArgs = [];
|
|
496
546
|
} else {
|
|
497
547
|
// gh#702: borg-launched claude drones auto-allow ONLY mcp__borg__* so they
|
|
498
548
|
// never prompt on borg coordination calls; Bash/file/web still prompt.
|
|
@@ -502,26 +552,18 @@ async function main() {
|
|
|
502
552
|
const cliDisplayName = cli === 'claude' ? 'Claude Code' : cli === 'codex' ? 'Codex' : 'OpenCode';
|
|
503
553
|
console.error(`${consolePrefix()}${chalk.blue(`◼ Launching ${cliDisplayName}…`)}`);
|
|
504
554
|
|
|
505
|
-
const agentProcess =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
? computeOpenCodePort(active.droneId)
|
|
518
|
-
: 14096;
|
|
519
|
-
const serverUrl = `http://127.0.0.1:${dronePort}`;
|
|
520
|
-
// Fire-and-forget; never delay the launch or crash on failure.
|
|
521
|
-
connectOpenCodeDrone({ serverUrl, directory: process.cwd(), droneLabel: active?.droneLabel ?? 'opencode', cubeName: active?.name ?? 'borg' })
|
|
522
|
-
.then(() => injectInitialKickoff(launchKickoff))
|
|
523
|
-
.catch(() => {});
|
|
524
|
-
}
|
|
555
|
+
const agentProcess = cli === 'opencode' && openCodeKickoff && openCodePort !== undefined
|
|
556
|
+
? launchOpenCodeProcess({
|
|
557
|
+
cwd: process.cwd(),
|
|
558
|
+
port: openCodePort,
|
|
559
|
+
prompt: openCodeKickoff.prompt,
|
|
560
|
+
passthroughArgs,
|
|
561
|
+
env: launchEnv,
|
|
562
|
+
droneLabel: active?.droneLabel ?? 'opencode',
|
|
563
|
+
cubeName: active?.name ?? 'borg',
|
|
564
|
+
kickoff: openCodeKickoff,
|
|
565
|
+
}).process
|
|
566
|
+
: spawn(cli, launchArgs, { stdio: 'inherit', shell: false, env: launchEnv });
|
|
525
567
|
|
|
526
568
|
// gh#857 WI-2: wake-target recording is codex-only (app-server bridge).
|
|
527
569
|
// OpenCode uses HTTP entry injection; Claude uses the inbox Monitor.
|
package/src/cli-platform.ts
CHANGED
|
@@ -101,10 +101,17 @@ export function defaultCliChoiceDeps(prompt: (message: string) => Promise<string
|
|
|
101
101
|
|
|
102
102
|
const VALID_CLIS: readonly BorgCli[] = ['claude', 'codex', 'opencode'];
|
|
103
103
|
|
|
104
|
-
export function parseCliFlag(args: string[]): {
|
|
104
|
+
export function parseCliFlag(args: string[]): {
|
|
105
|
+
cli?: BorgCli;
|
|
106
|
+
force?: boolean;
|
|
107
|
+
noBorgApprovalOverride?: boolean;
|
|
108
|
+
rest: string[];
|
|
109
|
+
error?: string;
|
|
110
|
+
} {
|
|
105
111
|
const rest: string[] = [];
|
|
106
112
|
let cli: BorgCli | undefined;
|
|
107
113
|
let force = false;
|
|
114
|
+
let noBorgApprovalOverride = false;
|
|
108
115
|
for (let i = 0; i < args.length; i++) {
|
|
109
116
|
const arg = args[i];
|
|
110
117
|
if (arg === '--cli') {
|
|
@@ -122,9 +129,16 @@ export function parseCliFlag(args: string[]): { cli?: BorgCli; force?: boolean;
|
|
|
122
129
|
cli = value as BorgCli;
|
|
123
130
|
} else if (arg === '--force') {
|
|
124
131
|
force = true;
|
|
132
|
+
} else if (arg === '--no-borg-approval-override') {
|
|
133
|
+
noBorgApprovalOverride = true;
|
|
125
134
|
} else {
|
|
126
135
|
rest.push(arg);
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
...(cli ? { cli } : {}),
|
|
140
|
+
...(force ? { force: true } : {}),
|
|
141
|
+
...(noBorgApprovalOverride ? { noBorgApprovalOverride: true } : {}),
|
|
142
|
+
rest,
|
|
143
|
+
};
|
|
130
144
|
}
|
package/src/cli-tool-approval.ts
CHANGED
|
@@ -581,13 +581,10 @@ export function defaultApprovalIo(
|
|
|
581
581
|
};
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
function accepted(answer: string): boolean {
|
|
585
|
-
return /^(?:y|yes)$/i.test(answer.trim());
|
|
586
|
-
}
|
|
587
|
-
|
|
588
584
|
export async function resolveLaunchBorgApprovals(
|
|
589
585
|
cli: BorgCli,
|
|
590
|
-
io: ApprovalIo
|
|
586
|
+
io: ApprovalIo,
|
|
587
|
+
options: { skipOverride?: boolean } = {}
|
|
591
588
|
): Promise<LaunchApprovalDecision> {
|
|
592
589
|
if (cli === 'claude') return { codexArgs: [] };
|
|
593
590
|
|
|
@@ -611,18 +608,10 @@ export async function resolveLaunchBorgApprovals(
|
|
|
611
608
|
const intro =
|
|
612
609
|
`${cli === 'codex' ? 'Codex' : 'OpenCode'} requires approval for ${inspection.restrictiveTools.length} Borg tool${inspection.restrictiveTools.length === 1 ? '' : 's'}. ` +
|
|
613
610
|
BORG_DISPATCHER_APPROVAL_DISCLOSURE;
|
|
614
|
-
if (
|
|
611
|
+
if (options.skipOverride) {
|
|
615
612
|
return {
|
|
616
613
|
codexArgs: [],
|
|
617
|
-
warning: `${intro}
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const answer = await io.confirm(`${intro} Apply this launch-only Borg approval set? [y/N] `);
|
|
622
|
-
if (!accepted(answer)) {
|
|
623
|
-
return {
|
|
624
|
-
codexArgs: [],
|
|
625
|
-
warning: `${intro} Continuing without the launch-only fix. To repair it globally, add:\n${inspection.repairSnippet}`,
|
|
614
|
+
warning: `${intro} Continuing without the launch-only fix because --no-borg-approval-override was supplied. To repair it globally, add:\n${inspection.repairSnippet}`,
|
|
626
615
|
};
|
|
627
616
|
}
|
|
628
617
|
|
|
@@ -645,7 +634,10 @@ export async function resolveLaunchBorgApprovals(
|
|
|
645
634
|
warning: `Codex managed policy prevents the launch-only Borg approval override. Ask your Codex administrator to allow these tools:\n${effectiveWithOverride.repairSnippet}`,
|
|
646
635
|
};
|
|
647
636
|
}
|
|
648
|
-
return {
|
|
637
|
+
return {
|
|
638
|
+
codexArgs,
|
|
639
|
+
warning: `${intro} Applied a launch-only approval override. Pass --no-borg-approval-override to opt out.`,
|
|
640
|
+
};
|
|
649
641
|
}
|
|
650
642
|
const openCodePermission = JSON.stringify(mergeOpenCodePermission(
|
|
651
643
|
openCodeConfig && typeof openCodeConfig === 'object'
|
|
@@ -673,6 +665,7 @@ export async function resolveLaunchBorgApprovals(
|
|
|
673
665
|
return {
|
|
674
666
|
codexArgs: [],
|
|
675
667
|
openCodePermission,
|
|
668
|
+
warning: `${intro} Applied a launch-only approval override. Pass --no-borg-approval-override to opt out.`,
|
|
676
669
|
};
|
|
677
670
|
}
|
|
678
671
|
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
createRole,
|
|
42
42
|
updateRole,
|
|
43
43
|
patchRoleSection,
|
|
44
|
+
sanitizeServerAdvisory,
|
|
44
45
|
patchTaxonomyClass,
|
|
45
46
|
deleteRole,
|
|
46
47
|
getCube,
|
|
@@ -132,7 +133,9 @@ import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
|
132
133
|
import {
|
|
133
134
|
connectOpenCodeDrone,
|
|
134
135
|
injectOpenCodeEntry,
|
|
135
|
-
|
|
136
|
+
configuredOpenCodePort,
|
|
137
|
+
OPEN_CODE_PORT_MISSING_DIAGNOSTIC,
|
|
138
|
+
openCodeLaunchBinding,
|
|
136
139
|
} from './opencode-drone.js';
|
|
137
140
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
138
141
|
import { setModuleInjectOpenCode } from './log-stream.js';
|
|
@@ -216,6 +219,57 @@ async function requireActiveCube() {
|
|
|
216
219
|
return active;
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
export function appendServerAdvisory(text: string, advisory: unknown): string {
|
|
223
|
+
const sanitized = sanitizeServerAdvisory(advisory);
|
|
224
|
+
return sanitized === undefined ? text : `${text}\n\nAdvisory: ${sanitized}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function formatUpdatedCubeResult(cube: { name: string; id: string }, advisory?: unknown): string {
|
|
228
|
+
return appendServerAdvisory(`Updated cube **${cube.name}** (id: ${cube.id}).`, advisory);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function formatUpdatedRoleResult(role: { name: string; id: string; role_class?: string; is_human_seat?: boolean; is_default?: boolean; is_mandatory?: boolean }, advisory?: unknown): string {
|
|
232
|
+
const tags = [
|
|
233
|
+
role.role_class === 'queen' ? 'Queen' : null,
|
|
234
|
+
role.is_human_seat ? 'human-seat' : null,
|
|
235
|
+
role.is_default ? 'default' : null,
|
|
236
|
+
role.is_mandatory ? 'mandatory' : null,
|
|
237
|
+
].filter(Boolean).join(', ');
|
|
238
|
+
const tag = tags ? ` (${tags})` : '';
|
|
239
|
+
return appendServerAdvisory(`Updated role **${role.name}**${tag} (id: ${role.id}).`, advisory);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function formatPatchedRoleSectionResult(action: 'replace' | 'insert' | 'delete', heading: string, role: { name: string; id: string }, advisory?: unknown): string {
|
|
243
|
+
const verb = action === 'replace' ? 'Replaced' : action === 'insert' ? 'Inserted' : 'Deleted';
|
|
244
|
+
return appendServerAdvisory(`${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).`, advisory);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function connectOpenCodeRuntime(
|
|
248
|
+
active: {
|
|
249
|
+
worktree?: string;
|
|
250
|
+
droneLabel: string;
|
|
251
|
+
name: string;
|
|
252
|
+
},
|
|
253
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
254
|
+
deps: {
|
|
255
|
+
connect?: typeof connectOpenCodeDrone;
|
|
256
|
+
} = {},
|
|
257
|
+
): Promise<boolean> {
|
|
258
|
+
const configuredPort = configuredOpenCodePort(env);
|
|
259
|
+
if (configuredPort === null) {
|
|
260
|
+
console.error(OPEN_CODE_PORT_MISSING_DIAGNOSTIC);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const binding = openCodeLaunchBinding(configuredPort);
|
|
264
|
+
await (deps.connect ?? connectOpenCodeDrone)({
|
|
265
|
+
serverUrl: binding.serverUrl,
|
|
266
|
+
directory: active.worktree ?? findProjectRoot(),
|
|
267
|
+
droneLabel: active.droneLabel,
|
|
268
|
+
cubeName: active.name,
|
|
269
|
+
});
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
219
273
|
/**
|
|
220
274
|
* Main entry point - MCP stdio server
|
|
221
275
|
*/
|
|
@@ -266,15 +320,7 @@ export async function main() {
|
|
|
266
320
|
installBorgPlugin();
|
|
267
321
|
const active = await getActiveCube();
|
|
268
322
|
if (active && openCodeRuntime) {
|
|
269
|
-
|
|
270
|
-
const serverUrl = `http://127.0.0.1:${port}`;
|
|
271
|
-
await connectOpenCodeDrone({
|
|
272
|
-
serverUrl,
|
|
273
|
-
directory: active.worktree ?? findProjectRoot(),
|
|
274
|
-
droneLabel: active.droneLabel,
|
|
275
|
-
cubeName: active.name,
|
|
276
|
-
});
|
|
277
|
-
setModuleInjectOpenCode(injectOpenCodeEntry);
|
|
323
|
+
if (await connectOpenCodeRuntime(active)) setModuleInjectOpenCode(injectOpenCodeEntry);
|
|
278
324
|
}
|
|
279
325
|
},
|
|
280
326
|
};
|
|
@@ -1018,8 +1064,8 @@ export async function main() {
|
|
|
1018
1064
|
if (typeof args?.cube_directive === 'string') updates.cube_directive = args.cube_directive as string;
|
|
1019
1065
|
if (Array.isArray(args?.message_taxonomy)) updates.message_taxonomy = args.message_taxonomy as MessageTaxonomy;
|
|
1020
1066
|
if (Object.keys(updates).length === 0) throw new Error('Pass at least one of: cube_directive, message_taxonomy.');
|
|
1021
|
-
const { cube } = await updateCube(cubeId, updates);
|
|
1022
|
-
return { content: [{ type: 'text', text:
|
|
1067
|
+
const { cube, advisory } = await updateCube(cubeId, updates);
|
|
1068
|
+
return { content: [{ type: 'text', text: formatUpdatedCubeResult(cube, advisory) }] };
|
|
1023
1069
|
}
|
|
1024
1070
|
|
|
1025
1071
|
case 'borg_patch-taxonomy-class': {
|
|
@@ -1108,15 +1154,8 @@ export async function main() {
|
|
|
1108
1154
|
if (typeof args?.receives_all_direct === 'boolean') updates.receives_all_direct = args.receives_all_direct as boolean;
|
|
1109
1155
|
if (typeof args?.default_model === 'string') updates.default_model = args.default_model as string;
|
|
1110
1156
|
if (Object.keys(updates).length === 0) throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_mandatory, is_human_seat, can_broadcast, receives_all_direct.');
|
|
1111
|
-
const { role } = await updateRole(roleId, updates);
|
|
1112
|
-
|
|
1113
|
-
role.role_class === 'queen' ? 'Queen' : null,
|
|
1114
|
-
role.is_human_seat ? 'human-seat' : null,
|
|
1115
|
-
role.is_default ? 'default' : null,
|
|
1116
|
-
role.is_mandatory ? 'mandatory' : null,
|
|
1117
|
-
].filter(Boolean).join(', ');
|
|
1118
|
-
const tag = tags ? ` (${tags})` : '';
|
|
1119
|
-
return { content: [{ type: 'text', text: `Updated role **${role.name}**${tag} (id: ${role.id}).` }] };
|
|
1157
|
+
const { role, advisory } = await updateRole(roleId, updates);
|
|
1158
|
+
return { content: [{ type: 'text', text: formatUpdatedRoleResult(role, advisory) }] };
|
|
1120
1159
|
}
|
|
1121
1160
|
|
|
1122
1161
|
case 'borg_patch-role-section': {
|
|
@@ -1129,8 +1168,9 @@ export async function main() {
|
|
|
1129
1168
|
const heading = args?.heading as string;
|
|
1130
1169
|
if (!heading) throw new Error('heading is required');
|
|
1131
1170
|
let role: any;
|
|
1171
|
+
let advisory: unknown;
|
|
1132
1172
|
if (action === 'delete') {
|
|
1133
|
-
({ role } = await patchRoleSection(roleId, { action, heading }));
|
|
1173
|
+
({ role, advisory } = await patchRoleSection(roleId, { action, heading }));
|
|
1134
1174
|
} else {
|
|
1135
1175
|
const body = args?.body as string;
|
|
1136
1176
|
if (typeof body !== 'string') {
|
|
@@ -1138,13 +1178,12 @@ export async function main() {
|
|
|
1138
1178
|
}
|
|
1139
1179
|
if (action === 'insert') {
|
|
1140
1180
|
const after = (typeof args?.after === 'string' ? args.after : null) as string | null;
|
|
1141
|
-
({ role } = await patchRoleSection(roleId, { action, heading, body, after }));
|
|
1181
|
+
({ role, advisory } = await patchRoleSection(roleId, { action, heading, body, after }));
|
|
1142
1182
|
} else {
|
|
1143
|
-
({ role } = await patchRoleSection(roleId, { action, heading, body }));
|
|
1183
|
+
({ role, advisory } = await patchRoleSection(roleId, { action, heading, body }));
|
|
1144
1184
|
}
|
|
1145
1185
|
}
|
|
1146
|
-
|
|
1147
|
-
return { content: [{ type: 'text', text: `${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).` }] };
|
|
1186
|
+
return { content: [{ type: 'text', text: formatPatchedRoleSectionResult(action, heading, role, advisory) }] };
|
|
1148
1187
|
}
|
|
1149
1188
|
|
|
1150
1189
|
case 'borg_delete-role': {
|
package/src/opencode-drone.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
2
|
import { createHash, randomUUID } from 'crypto';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
3
4
|
import { join } from 'path';
|
|
4
5
|
import { tmpdir } from 'os';
|
|
5
6
|
|
|
@@ -749,6 +750,62 @@ export function computeOpenCodePort(droneId: string, base: number = 14096): numb
|
|
|
749
750
|
return base + (Math.abs(hash) % 1024);
|
|
750
751
|
}
|
|
751
752
|
|
|
753
|
+
/**
|
|
754
|
+
* Ask the OS for an available loopback port. The old deterministic hash is
|
|
755
|
+
* retained above only for compatibility fixtures; launch paths must not use a
|
|
756
|
+
* bounded shared port space where two drones can collide.
|
|
757
|
+
*/
|
|
758
|
+
async function canBindOpenCodePort(port: number): Promise<boolean> {
|
|
759
|
+
return new Promise((resolve) => {
|
|
760
|
+
const probe = createServer();
|
|
761
|
+
probe.once('error', () => resolve(false));
|
|
762
|
+
probe.listen(port, '127.0.0.1', () => {
|
|
763
|
+
probe.close(() => resolve(true));
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export function configuredOpenCodePort(env: NodeJS.ProcessEnv = process.env): number | null {
|
|
769
|
+
const port = Number(env.BORG_OPENCODE_PORT);
|
|
770
|
+
return Number.isInteger(port) && port > 0 && port <= 65_535 ? port : null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export const OPEN_CODE_PORT_MISSING_DIAGNOSTIC =
|
|
774
|
+
'OpenCode launch port is missing; skipping OpenCode entry injection. Relaunch through borg.';
|
|
775
|
+
|
|
776
|
+
export function openCodeLaunchBinding(port: number): {
|
|
777
|
+
cliPort: string;
|
|
778
|
+
envPort: string;
|
|
779
|
+
serverUrl: string;
|
|
780
|
+
} {
|
|
781
|
+
const value = String(port);
|
|
782
|
+
return { cliPort: value, envPort: value, serverUrl: `http://127.0.0.1:${value}` };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function allocateOpenCodePort(
|
|
786
|
+
isPortAvailable: (port: number) => Promise<boolean> = canBindOpenCodePort,
|
|
787
|
+
): Promise<number> {
|
|
788
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
789
|
+
const port = await new Promise<number>((resolve, reject) => {
|
|
790
|
+
const probe = createServer();
|
|
791
|
+
const fail = (error: Error) => {
|
|
792
|
+
probe.close(() => reject(error));
|
|
793
|
+
};
|
|
794
|
+
probe.once('error', fail);
|
|
795
|
+
probe.listen(0, '127.0.0.1', () => {
|
|
796
|
+
const address = probe.address();
|
|
797
|
+
if (address === null || typeof address === 'string') {
|
|
798
|
+
fail(new Error('OpenCode port allocation returned no TCP address'));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
probe.close((error) => error ? reject(error) : resolve(address.port));
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
if (await isPortAvailable(port)) return port;
|
|
805
|
+
}
|
|
806
|
+
throw new Error('OpenCode port allocation could not claim an available loopback port');
|
|
807
|
+
}
|
|
808
|
+
|
|
752
809
|
/** Test-only cleanup for module state and the local cross-process binding. */
|
|
753
810
|
export function __resetOpenCodeDroneForTests(): void {
|
|
754
811
|
abandonOpenCodeDeliveries(state);
|
|
@@ -16,7 +16,7 @@ export type ParseResult =
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Parse argv for `borg assimilate [role] [--worktree <n>] [--template <n>]
|
|
19
|
-
* [--no-template] [--cube-name <n>] [--host <host>] [--enroll] [--here] [--force] [--yes]`. The `assimilate`
|
|
19
|
+
* [--no-template] [--cube-name <n>] [--host <host>] [--enroll] [--here] [--force] [--yes] [--no-borg-approval-override]`. The `assimilate`
|
|
20
20
|
* subcommand token must already be stripped by the caller.
|
|
21
21
|
*/
|
|
22
22
|
export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
|
|
@@ -53,6 +53,8 @@ export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
|
|
|
53
53
|
flags.here = true;
|
|
54
54
|
} else if (arg === '--force') {
|
|
55
55
|
flags.force = true;
|
|
56
|
+
} else if (arg === '--no-borg-approval-override') {
|
|
57
|
+
flags.noBorgApprovalOverride = true;
|
|
56
58
|
} else if (arg === '--host') {
|
|
57
59
|
const next = rawArgs[i + 1];
|
|
58
60
|
if (typeof next !== 'string' || next.length === 0 || next.startsWith('-')) {
|
|
@@ -106,7 +108,7 @@ export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
|
|
|
106
108
|
} else if (arg.startsWith('--')) {
|
|
107
109
|
return {
|
|
108
110
|
ok: false,
|
|
109
|
-
error: `unknown flag: ${arg}. Supported: --worktree, --template, --no-template, --cube-name, --host, --enroll, --here, --force, --yes, --cli, --model`,
|
|
111
|
+
error: `unknown flag: ${arg}. Supported: --worktree, --template, --no-template, --cube-name, --host, --enroll, --here, --force, --yes, --cli, --model, --no-borg-approval-override`,
|
|
110
112
|
};
|
|
111
113
|
} else {
|
|
112
114
|
if (role !== undefined) {
|
package/src/remote-client.ts
CHANGED
|
@@ -85,10 +85,27 @@ export const LOCAL_SERVER_RESPONSE_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
|
85
85
|
// bounded read throws → the 401 fails closed to non-destructive CREDENTIAL_REJECTED.
|
|
86
86
|
const AUTH_ERROR_ENVELOPE_LIMIT_BYTES = 64 * 1024;
|
|
87
87
|
const ROLE_SECTION_CONFLICT_CODE = 'ROLE_SECTION_CONFLICT';
|
|
88
|
+
const CAPACITY_EXCEEDED_CODE = 'CAPACITY_EXCEEDED';
|
|
88
89
|
export const LOCAL_SERVER_REQUEST_TIMEOUT_MS = 5_000;
|
|
89
90
|
const LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE =
|
|
90
91
|
'Local Borg server response exceeded the response limit';
|
|
91
92
|
|
|
93
|
+
function sanitizeServerMessage(message: string): string {
|
|
94
|
+
return message
|
|
95
|
+
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '')
|
|
96
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
97
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
|
98
|
+
.replace(/\\u(?:000[0-9a-f]|001[0-9a-f]|007f|008[0-9a-f]|009[0-9a-f])/gi, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const SERVER_ADVISORY_MAX_CHARS = 512;
|
|
102
|
+
|
|
103
|
+
export function sanitizeServerAdvisory(value: unknown): string | undefined {
|
|
104
|
+
if (typeof value !== 'string') return undefined;
|
|
105
|
+
const sanitized = sanitizeServerMessage(value).trim();
|
|
106
|
+
return sanitized.length > 0 ? sanitized.slice(0, SERVER_ADVISORY_MAX_CHARS) : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
/**
|
|
93
110
|
* Parse a `Retry-After` header (delta-seconds form, which the worker
|
|
94
111
|
* emits — mcp-server.ts:382/583) into milliseconds. Returns null when
|
|
@@ -725,11 +742,11 @@ async function authedFetch(
|
|
|
725
742
|
}
|
|
726
743
|
|
|
727
744
|
if (!response.ok) {
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
// branching; never surface the server-provided message or details.
|
|
745
|
+
// Decode only the bounded protocol error envelope. Its message is the
|
|
746
|
+
// server's operator-facing action guidance; details and unrecognized bodies
|
|
747
|
+
// remain excluded from the client error.
|
|
732
748
|
let code: ErrorCode | undefined;
|
|
749
|
+
let serverMessage: string | undefined;
|
|
733
750
|
let protocolMismatch = false;
|
|
734
751
|
try {
|
|
735
752
|
const body = await readBoundedResponseBody(
|
|
@@ -739,7 +756,11 @@ async function authedFetch(
|
|
|
739
756
|
);
|
|
740
757
|
const parsed = JSON.parse(body);
|
|
741
758
|
try {
|
|
742
|
-
|
|
759
|
+
const decoded = decodeProtocolErrorEnvelope(parsed);
|
|
760
|
+
code = decoded.error.code;
|
|
761
|
+
serverMessage = response.status === 404
|
|
762
|
+
? undefined
|
|
763
|
+
: sanitizeServerMessage(decoded.error.message);
|
|
743
764
|
} catch (error) {
|
|
744
765
|
if (
|
|
745
766
|
error instanceof ProtocolContractError &&
|
|
@@ -750,21 +771,25 @@ async function authedFetch(
|
|
|
750
771
|
if (
|
|
751
772
|
parsed !== null && typeof parsed === 'object' &&
|
|
752
773
|
parsed.error !== null && typeof parsed.error === 'object' &&
|
|
753
|
-
parsed.error.code === ROLE_SECTION_CONFLICT_CODE
|
|
774
|
+
(parsed.error.code === ROLE_SECTION_CONFLICT_CODE
|
|
775
|
+
|| parsed.error.code === CAPACITY_EXCEEDED_CODE)
|
|
754
776
|
) {
|
|
755
777
|
// The shared protocol intentionally omits this server-local code. Re-validate the whole
|
|
756
778
|
// envelope through the strict shared decoder with only the recognized
|
|
757
|
-
// code substituted
|
|
758
|
-
|
|
779
|
+
// code substituted. The original message remains in place so the
|
|
780
|
+
// shared decoder validates its length and diagnostic shape.
|
|
781
|
+
const decoded = decodeProtocolErrorEnvelope({
|
|
759
782
|
...parsed,
|
|
760
783
|
error: {
|
|
761
784
|
...parsed.error,
|
|
762
785
|
code: ErrorCode.INVALID_INPUT,
|
|
763
|
-
message: 'Role section conflict.',
|
|
764
786
|
...(Object.hasOwn(parsed.error, 'details') ? { details: 'Redacted.' } : {}),
|
|
765
787
|
},
|
|
766
788
|
});
|
|
767
|
-
code =
|
|
789
|
+
code = parsed.error.code as ErrorCode;
|
|
790
|
+
serverMessage = response.status === 404
|
|
791
|
+
? undefined
|
|
792
|
+
: sanitizeServerMessage(decoded.error.message);
|
|
768
793
|
}
|
|
769
794
|
}
|
|
770
795
|
} catch {
|
|
@@ -782,7 +807,9 @@ async function authedFetch(
|
|
|
782
807
|
}
|
|
783
808
|
throw new BorgServerHttpError(
|
|
784
809
|
response.status,
|
|
785
|
-
|
|
810
|
+
serverMessage
|
|
811
|
+
? `Borg server request failed (HTTP ${response.status}): ${serverMessage}`
|
|
812
|
+
: `Borg server request failed (HTTP ${response.status})`,
|
|
786
813
|
code,
|
|
787
814
|
);
|
|
788
815
|
}
|
|
@@ -1302,7 +1329,7 @@ export async function updateCube(
|
|
|
1302
1329
|
updates: { name?: string; cube_directive?: string; message_taxonomy?: MessageTaxonomy | null },
|
|
1303
1330
|
activeOverride?: ActiveCube,
|
|
1304
1331
|
connectionOverride?: RemoteConnection,
|
|
1305
|
-
): Promise<{ cube: any }> {
|
|
1332
|
+
): Promise<{ cube: any; advisory?: unknown }> {
|
|
1306
1333
|
assertUuidShape(cubeId, 'cube_id');
|
|
1307
1334
|
const active = activeOverride ?? await getActiveCube();
|
|
1308
1335
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
@@ -1438,7 +1465,7 @@ export async function updateRole(
|
|
|
1438
1465
|
targetCubeId?: string,
|
|
1439
1466
|
activeOverride?: ActiveCube,
|
|
1440
1467
|
connectionOverride?: RemoteConnection,
|
|
1441
|
-
): Promise<{ role: any }> {
|
|
1468
|
+
): Promise<{ role: any; advisory?: unknown }> {
|
|
1442
1469
|
assertUuidShape(roleId, 'role_id');
|
|
1443
1470
|
const active = activeOverride ?? await getActiveCube();
|
|
1444
1471
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
@@ -1514,7 +1541,7 @@ export async function patchRoleSection(
|
|
|
1514
1541
|
targetCubeId?: string,
|
|
1515
1542
|
activeOverride?: ActiveCube,
|
|
1516
1543
|
connectionOverride?: RemoteConnection,
|
|
1517
|
-
): Promise<{ role: any }> {
|
|
1544
|
+
): Promise<{ role: any; advisory?: unknown }> {
|
|
1518
1545
|
assertUuidShape(roleId, 'role_id');
|
|
1519
1546
|
const active = activeOverride ?? await getActiveCube();
|
|
1520
1547
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|