borgmcp 4.4.0 → 4.5.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/dist/agent-integration-health.d.ts +2 -0
- package/dist/agent-integration-health.d.ts.map +1 -1
- package/dist/agent-integration-health.js +18 -1
- package/dist/agent-integration-health.js.map +1 -1
- package/dist/backends/launch-all-terminals.d.ts.map +1 -1
- package/dist/backends/launch-all-terminals.js +6 -3
- package/dist/backends/launch-all-terminals.js.map +1 -1
- package/dist/cli-help.js +2 -2
- package/dist/cli-help.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +2 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +86 -19
- package/dist/opencode-drone.js.map +1 -1
- package/dist/private-root.d.ts +2 -0
- package/dist/private-root.d.ts.map +1 -1
- package/dist/private-root.js +35 -1
- package/dist/private-root.js.map +1 -1
- package/dist/terminal-title.d.ts.map +1 -1
- package/dist/terminal-title.js +3 -2
- package/dist/terminal-title.js.map +1 -1
- package/docs/LOCAL_SERVER.md +7 -5
- package/package.json +1 -1
- package/src/agent-integration-health.ts +20 -1
- package/src/backends/launch-all-terminals.ts +6 -3
- package/src/cli-help.ts +2 -2
- package/src/index.ts +8 -1
- package/src/opencode-drone.ts +116 -19
- package/src/private-root.ts +38 -1
- package/src/terminal-title.ts +4 -2
package/src/opencode-drone.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
existsSync,
|
|
5
|
+
fchmodSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
writeSync,
|
|
14
|
+
} from 'fs';
|
|
15
|
+
import { createHash, randomUUID } from 'crypto';
|
|
3
16
|
import { createServer } from 'node:net';
|
|
4
17
|
import { join } from 'path';
|
|
5
18
|
import { tmpdir } from 'os';
|
|
19
|
+
import {
|
|
20
|
+
borgConfigRoot,
|
|
21
|
+
ensurePrivateBorgConfigRoot,
|
|
22
|
+
ensurePrivateBorgConfigRootSync,
|
|
23
|
+
} from './private-root.js';
|
|
6
24
|
import {
|
|
7
25
|
OPENCODE_INJECTED_ENTRY_METADATA_KEY,
|
|
8
26
|
OPENCODE_WAKE_IDENTITY_METADATA_KEY,
|
|
@@ -30,37 +48,115 @@ function stateIdentityDigest(current: OpenCodeDroneState): string {
|
|
|
30
48
|
return createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
function
|
|
34
|
-
|
|
51
|
+
export function openCodeStartupDiagnosticLogPath(): string {
|
|
52
|
+
return join(borgConfigRoot(), 'opencode-drone-startup.log');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function diagnosticLogPath(owner: OpenCodeDroneState | null): string {
|
|
56
|
+
const root = borgConfigRoot();
|
|
57
|
+
const path = owner
|
|
58
|
+
? join(root, `opencode-drone-${stateIdentityDigest(owner)}.log`)
|
|
59
|
+
: openCodeStartupDiagnosticLogPath();
|
|
35
60
|
diagnosticLogPathsForTests.add(path);
|
|
36
61
|
return path;
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
function log(msg: string, owner: OpenCodeDroneState | null = state) {
|
|
64
|
+
function log(msg: string, owner: OpenCodeDroneState | null = state, throwOnFailure = false) {
|
|
40
65
|
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
66
|
+
let descriptor: number | null = null;
|
|
67
|
+
let temporaryDescriptor: number | null = null;
|
|
68
|
+
let temporary: string | null = null;
|
|
45
69
|
try {
|
|
70
|
+
ensurePrivateBorgConfigRootSync(borgConfigRoot());
|
|
46
71
|
const path = diagnosticLogPath(owner);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
// The private root is the primary boundary. Where available, no-follow also
|
|
73
|
+
// closes the final replacement gap between root verification and this open.
|
|
74
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
75
|
+
descriptor = openSync(
|
|
76
|
+
path,
|
|
77
|
+
constants.O_RDWR |
|
|
78
|
+
constants.O_APPEND |
|
|
79
|
+
constants.O_CREAT |
|
|
80
|
+
noFollow,
|
|
81
|
+
0o600,
|
|
82
|
+
);
|
|
83
|
+
if (!fstatSync(descriptor).isFile()) {
|
|
84
|
+
throw Object.assign(new Error('OpenCode diagnostic log is not a regular file'), { code: 'EINVAL' });
|
|
85
|
+
}
|
|
86
|
+
fchmodSync(descriptor, 0o600);
|
|
87
|
+
writeSync(descriptor, line, null, 'utf8');
|
|
88
|
+
|
|
89
|
+
const size = fstatSync(descriptor).size;
|
|
90
|
+
if (size <= OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES) return;
|
|
91
|
+
const tail = Buffer.allocUnsafe(OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES);
|
|
92
|
+
let bytesRead = 0;
|
|
93
|
+
while (bytesRead < tail.length) {
|
|
94
|
+
const count = readSync(
|
|
95
|
+
descriptor,
|
|
96
|
+
tail,
|
|
97
|
+
bytesRead,
|
|
98
|
+
tail.length - bytesRead,
|
|
99
|
+
size - tail.length + bytesRead,
|
|
100
|
+
);
|
|
101
|
+
if (count === 0) break;
|
|
102
|
+
bytesRead += count;
|
|
103
|
+
}
|
|
104
|
+
const completeTail = tail.subarray(0, bytesRead);
|
|
105
|
+
const firstNewline = completeTail.indexOf(0x0a);
|
|
106
|
+
const bounded = firstNewline >= 0 ? completeTail.subarray(firstNewline + 1) : completeTail;
|
|
107
|
+
|
|
108
|
+
temporary = `${path}.${randomUUID()}.tmp`;
|
|
109
|
+
temporaryDescriptor = openSync(
|
|
110
|
+
temporary,
|
|
111
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow,
|
|
112
|
+
0o600,
|
|
113
|
+
);
|
|
114
|
+
if (!fstatSync(temporaryDescriptor).isFile()) {
|
|
115
|
+
throw Object.assign(new Error('OpenCode diagnostic temporary is not a regular file'), { code: 'EINVAL' });
|
|
116
|
+
}
|
|
117
|
+
fchmodSync(temporaryDescriptor, 0o600);
|
|
118
|
+
let bytesWritten = 0;
|
|
119
|
+
while (bytesWritten < bounded.length) {
|
|
120
|
+
const count = writeSync(
|
|
121
|
+
temporaryDescriptor,
|
|
122
|
+
bounded,
|
|
123
|
+
bytesWritten,
|
|
124
|
+
bounded.length - bytesWritten,
|
|
125
|
+
);
|
|
126
|
+
if (count === 0) throw Object.assign(new Error('OpenCode diagnostic temporary write stalled'), { code: 'EIO' });
|
|
127
|
+
bytesWritten += count;
|
|
128
|
+
}
|
|
129
|
+
closeSync(temporaryDescriptor);
|
|
130
|
+
temporaryDescriptor = null;
|
|
131
|
+
closeSync(descriptor);
|
|
132
|
+
descriptor = null;
|
|
57
133
|
renameSync(temporary, path);
|
|
134
|
+
temporary = null;
|
|
58
135
|
} catch (error) {
|
|
59
136
|
const code = (error as NodeJS.ErrnoException | null)?.code ?? 'unknown';
|
|
60
137
|
process.stderr.write(`OpenCode diagnostic log write failed (${code})\n`);
|
|
138
|
+
if (throwOnFailure) throw error;
|
|
139
|
+
} finally {
|
|
140
|
+
if (temporaryDescriptor !== null) {
|
|
141
|
+
try { closeSync(temporaryDescriptor); } catch { /* The primary write error is already reported. */ }
|
|
142
|
+
}
|
|
143
|
+
if (descriptor !== null) {
|
|
144
|
+
try { closeSync(descriptor); } catch { /* The primary write error is already reported. */ }
|
|
145
|
+
}
|
|
146
|
+
if (temporary !== null) {
|
|
147
|
+
try {
|
|
148
|
+
unlinkSync(temporary);
|
|
149
|
+
} catch {
|
|
150
|
+
// Already absent or inaccessible; the randomized name cannot be reused.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
61
153
|
}
|
|
62
154
|
}
|
|
63
155
|
|
|
156
|
+
export function writeOpenCodeStartupDiagnostic(message: string): void {
|
|
157
|
+
log(message, null, true);
|
|
158
|
+
}
|
|
159
|
+
|
|
64
160
|
interface OpenCodeDroneState {
|
|
65
161
|
serverUrl: string;
|
|
66
162
|
apiPassword: string;
|
|
@@ -229,6 +325,7 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
229
325
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
230
326
|
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
231
327
|
}
|
|
328
|
+
await ensurePrivateBorgConfigRoot(borgConfigRoot());
|
|
232
329
|
abandonOpenCodeDeliveries(state);
|
|
233
330
|
state = {
|
|
234
331
|
serverUrl: deps.serverUrl,
|
package/src/private-root.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstatSync, realpathSync } from 'node:fs';
|
|
1
|
+
import fs, { lstatSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { chmod, lstat, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -100,3 +100,40 @@ export async function ensurePrivateBorgConfigRoot(root = borgConfigRoot()): Prom
|
|
|
100
100
|
throw new Error('Borg private-state directory is not private');
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/** Synchronous equivalent for startup-failure paths that must never await. */
|
|
105
|
+
export function ensurePrivateBorgConfigRootSync(root = borgConfigRoot()): void {
|
|
106
|
+
if (!isAbsolute(root) || resolve(root) !== root) {
|
|
107
|
+
throw new Error('Borg private-state directory path is not canonical');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let metadata: fs.Stats;
|
|
111
|
+
try {
|
|
112
|
+
metadata = fs.lstatSync(root);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
115
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
116
|
+
metadata = fs.lstatSync(root);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
120
|
+
throw new Error('Borg private-state directory must be a real directory');
|
|
121
|
+
}
|
|
122
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
123
|
+
if (uid !== null && metadata.uid !== uid) {
|
|
124
|
+
throw new Error('Borg private-state directory is not owned by the current user');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const mode = metadata.mode & 0o777;
|
|
128
|
+
if ((mode & 0o022) !== 0) {
|
|
129
|
+
throw new Error('Borg private-state directory is writable by other users');
|
|
130
|
+
}
|
|
131
|
+
if (mode !== 0o700) {
|
|
132
|
+
fs.chmodSync(root, 0o700);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const final = fs.lstatSync(root);
|
|
136
|
+
if (!final.isDirectory() || (final.mode & 0o777) !== 0o700) {
|
|
137
|
+
throw new Error('Borg private-state directory is not private');
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/terminal-title.ts
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
* populated," so the assimilated path is the common case.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import { escapeSyncDisplay } from './sync-roles-render.js';
|
|
36
|
+
|
|
35
37
|
/**
|
|
36
38
|
* Pure: compose the title string for a session. Exported so tests can
|
|
37
39
|
* exercise every branch without TTY / process / fs dependencies.
|
|
@@ -46,9 +48,9 @@ export function composeTerminalTitle(
|
|
|
46
48
|
repoBasename: string
|
|
47
49
|
): string {
|
|
48
50
|
if (activeDrone) {
|
|
49
|
-
return `borg · ${activeDrone.label} · ${activeDrone.cubeName}`;
|
|
51
|
+
return `borg · ${escapeSyncDisplay(activeDrone.label)} · ${escapeSyncDisplay(activeDrone.cubeName)}`;
|
|
50
52
|
}
|
|
51
|
-
return `borg · ${repoBasename}`;
|
|
53
|
+
return `borg · ${escapeSyncDisplay(repoBasename)}`;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/**
|