@ctrl-spc/cs 0.7.14 → 0.7.16
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 +50 -3
- package/dist/agents.js +175 -1
- package/dist/autostart.js +103 -122
- package/dist/codex-home.js +14 -11
- package/dist/companion-ui.js +54 -6
- package/dist/companion.js +86 -169
- package/dist/config.js +199 -17
- package/dist/daemon-lifecycle.js +575 -0
- package/dist/daemon-lock.js +149 -42
- package/dist/daemon-processes.js +860 -0
- package/dist/daemon.js +14 -46
- package/dist/darwin-coalition.js +340 -0
- package/dist/failure-reason.js +76 -16
- package/dist/index.js +75 -76
- package/dist/login.js +9 -9
- package/dist/mcp.js +55 -56
- package/dist/native/darwin-coalition +0 -0
- package/dist/native/darwin-coalition.build.json +1 -0
- package/dist/native/darwin-coalition.c +145 -0
- package/dist/orchestrator.js +892 -575
- package/dist/panel3/coordinator.js +3 -1
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/run.js +996 -558
- package/dist/panel3/spawn.js +85 -23
- package/dist/panel3/tools.js +5 -0
- package/dist/presence-heartbeat.js +3 -0
- package/dist/presence.js +271 -135
- package/dist/supabase.js +173 -37
- package/dist/win-shell.js +464 -1
- package/dist/windows-job.js +312 -0
- package/package.json +4 -3
package/dist/config.js
CHANGED
|
@@ -1,12 +1,63 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, openSync, closeSync, fsyncSync, renameSync } from 'node:fs';
|
|
2
2
|
import { homedir, hostname, platform } from 'node:os';
|
|
3
|
-
import { randomBytes, createHash } from 'node:crypto';
|
|
4
|
-
import { execSync } from 'node:child_process';
|
|
3
|
+
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
|
4
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
/** Own config dir, isolated from the v1 CLI: `~/.config/ctrl-spc-v2`. */
|
|
7
7
|
export function configDir() {
|
|
8
8
|
return process.env.CTRL_SPC_V2_CONFIG_DIR || join(homedir(), '.config', 'ctrl-spc-v2');
|
|
9
9
|
}
|
|
10
|
+
/** Local control and process records are never hosted or shared with harness MCP. */
|
|
11
|
+
export function lifecycleDir() {
|
|
12
|
+
return join(configDir(), 'lifecycle');
|
|
13
|
+
}
|
|
14
|
+
let securedLifecycleDir = null;
|
|
15
|
+
export function ensureLifecycleDir() {
|
|
16
|
+
const path = lifecycleDir();
|
|
17
|
+
if (securedLifecycleDir === path && existsSync(path))
|
|
18
|
+
return path;
|
|
19
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
// Set an exact current-user ACL; removing inheritance alone leaves any
|
|
22
|
+
// explicit grants from an existing directory in place.
|
|
23
|
+
const script = "$ErrorActionPreference='Stop'; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User; $acl=New-Object System.Security.AccessControl.DirectorySecurity; $acl.SetOwner($sid); $acl.SetAccessRuleProtection($true,$false); $rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','ContainerInherit,ObjectInherit','None','Allow'); $acl.AddAccessRule($rule); Set-Acl -LiteralPath $env:CTRL_SPC_LIFECYCLE_ACL_PATH -AclObject $acl";
|
|
24
|
+
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
25
|
+
stdio: 'ignore', windowsHide: true, timeout: 3000, env: { ...process.env, CTRL_SPC_LIFECYCLE_ACL_PATH: path },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
chmodSync(path, 0o700);
|
|
30
|
+
securedLifecycleDir = path;
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
export function readLifecycleToken() {
|
|
34
|
+
try {
|
|
35
|
+
return readFileSync(join(lifecycleDir(), 'token'), 'utf8').trim() || null;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === 'ENOENT')
|
|
39
|
+
return null;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function lifecycleToken() {
|
|
44
|
+
const prior = readLifecycleToken();
|
|
45
|
+
if (prior)
|
|
46
|
+
return prior;
|
|
47
|
+
const path = join(ensureLifecycleDir(), 'token');
|
|
48
|
+
const token = randomBytes(32).toString('hex');
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(path, token, { flag: 'wx', mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== 'EEXIST')
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
const result = readLifecycleToken();
|
|
57
|
+
if (!result)
|
|
58
|
+
throw new Error('Local service credential could not be created.');
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
10
61
|
function filePath(name) {
|
|
11
62
|
return join(configDir(), name);
|
|
12
63
|
}
|
|
@@ -145,26 +196,157 @@ export function clearSupersededMachineIds() {
|
|
|
145
196
|
}
|
|
146
197
|
catch { /* leave as-is */ }
|
|
147
198
|
}
|
|
199
|
+
/** Missing, intentionally signed out, and unreadable storage are different facts. */
|
|
200
|
+
export function readSessionRecord() {
|
|
201
|
+
let text;
|
|
202
|
+
try {
|
|
203
|
+
text = readFileSync(filePath('session.json'), 'utf8');
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code === 'ENOENT')
|
|
207
|
+
return null;
|
|
208
|
+
throw new Error('Local sign-in could not be read.', { cause: error });
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const value = JSON.parse(text);
|
|
212
|
+
if (!value || typeof value !== 'object')
|
|
213
|
+
throw new Error('Invalid record');
|
|
214
|
+
if (value.schema === undefined && typeof value.access_token === 'string' && value.access_token) {
|
|
215
|
+
if (value.refresh_token !== undefined && typeof value.refresh_token !== 'string')
|
|
216
|
+
throw new Error('Invalid credential');
|
|
217
|
+
// A stable legacy identity permits the first refresh to upgrade in place.
|
|
218
|
+
return { schema: 1, generation: createHash('sha256').update(text).digest('hex'), accountId: null,
|
|
219
|
+
state: 'signed-in', tokens: { access_token: value.access_token, refresh_token: value.refresh_token ?? '' } };
|
|
220
|
+
}
|
|
221
|
+
if (value.schema !== 1 || typeof value.generation !== 'string' || !value.generation
|
|
222
|
+
|| !(value.accountId === null || (typeof value.accountId === 'string' && value.accountId)))
|
|
223
|
+
throw new Error('Invalid identity');
|
|
224
|
+
if (value.state === 'signed-out' || value.state === 'rejected') {
|
|
225
|
+
if ('tokens' in value)
|
|
226
|
+
throw new Error('Inactive record contains credentials');
|
|
227
|
+
return { schema: 1, generation: value.generation, accountId: value.accountId, state: value.state };
|
|
228
|
+
}
|
|
229
|
+
if (value.state !== 'signed-in' || typeof value.tokens?.access_token !== 'string' || !value.tokens.access_token
|
|
230
|
+
|| typeof value.tokens.refresh_token !== 'string')
|
|
231
|
+
throw new Error('Invalid credentials');
|
|
232
|
+
return { schema: 1, generation: value.generation, accountId: value.accountId, state: 'signed-in',
|
|
233
|
+
tokens: { access_token: value.tokens.access_token, refresh_token: value.tokens.refresh_token } };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw new Error('Local sign-in storage is incomplete or invalid.', { cause: error });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Compatibility for passive consumers: inactive records never expose tokens. */
|
|
148
240
|
export function readSession() {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
241
|
+
const record = readSessionRecord();
|
|
242
|
+
return record?.state === 'signed-in' ? record.tokens : null;
|
|
243
|
+
}
|
|
244
|
+
async function changeSession(change) {
|
|
245
|
+
// Dynamic import avoids config -> process helpers -> config initialization.
|
|
246
|
+
const { inspectProcess, processIdentityMatches } = await import('./win-shell.js');
|
|
247
|
+
const deadline = Date.now() + 5000;
|
|
248
|
+
const owner = await inspectProcess(process.pid, deadline);
|
|
249
|
+
if (!owner)
|
|
250
|
+
throw new Error('Cannot verify the sign-in writer process.');
|
|
251
|
+
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
252
|
+
const lock = filePath('session.lock');
|
|
253
|
+
const nonce = randomUUID();
|
|
254
|
+
let held = false;
|
|
255
|
+
while (Date.now() < deadline) {
|
|
256
|
+
try {
|
|
257
|
+
const fd = openSync(lock, 'wx', 0o600);
|
|
258
|
+
try {
|
|
259
|
+
writeFileSync(fd, JSON.stringify({ nonce, owner }));
|
|
260
|
+
fsyncSync(fd);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
closeSync(fd);
|
|
264
|
+
}
|
|
265
|
+
held = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (error.code !== 'EEXIST')
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
let prior;
|
|
273
|
+
try {
|
|
274
|
+
prior = JSON.parse(readFileSync(lock, 'utf8'));
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (!(error instanceof SyntaxError) && error.code !== 'ENOENT')
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
if (prior && typeof prior.nonce === 'string' && Number.isInteger(prior.owner?.pid)) {
|
|
281
|
+
const live = await inspectProcess(prior.owner.pid, deadline);
|
|
282
|
+
if (!live || !processIdentityMatches(prior.owner, live)) {
|
|
283
|
+
// Recheck the resource identity after asynchronous process inspection.
|
|
284
|
+
try {
|
|
285
|
+
if (JSON.parse(readFileSync(lock, 'utf8')).nonce === prior.nonce)
|
|
286
|
+
rmSync(lock);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (error.code !== 'ENOENT')
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
295
|
+
}
|
|
296
|
+
if (!held)
|
|
297
|
+
throw new Error('Local sign-in is busy or its writer cannot be verified. Try again.');
|
|
298
|
+
const temp = filePath('session.' + nonce + '.tmp');
|
|
152
299
|
try {
|
|
153
|
-
|
|
300
|
+
const record = change(readSessionRecord());
|
|
301
|
+
if (record) {
|
|
302
|
+
const fd = openSync(temp, 'wx', 0o600);
|
|
303
|
+
try {
|
|
304
|
+
writeFileSync(fd, JSON.stringify(record));
|
|
305
|
+
fsyncSync(fd);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
closeSync(fd);
|
|
309
|
+
}
|
|
310
|
+
renameSync(temp, filePath('session.json'));
|
|
311
|
+
}
|
|
312
|
+
return record;
|
|
154
313
|
}
|
|
155
|
-
|
|
156
|
-
|
|
314
|
+
finally {
|
|
315
|
+
rmSync(temp, { force: true });
|
|
316
|
+
if (JSON.parse(readFileSync(lock, 'utf8')).nonce === nonce)
|
|
317
|
+
rmSync(lock);
|
|
157
318
|
}
|
|
158
319
|
}
|
|
159
|
-
export function writeSession(session) {
|
|
160
|
-
|
|
320
|
+
export async function writeSession(session, accountId = null, expectedGeneration) {
|
|
321
|
+
if (!session.access_token || !session.refresh_token)
|
|
322
|
+
throw new Error('Sign-in did not return complete credentials.');
|
|
323
|
+
return (await changeSession(prior => {
|
|
324
|
+
if (expectedGeneration !== undefined && (prior?.generation ?? null) !== expectedGeneration)
|
|
325
|
+
throw new Error('The local sign-in changed while this request was being verified. Try again.');
|
|
326
|
+
return { schema: 1, generation: randomUUID(), accountId, state: 'signed-in', tokens: session };
|
|
327
|
+
}));
|
|
161
328
|
}
|
|
162
|
-
export function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
329
|
+
export async function rotateSession(generation, session, accountId) {
|
|
330
|
+
let changed = false;
|
|
331
|
+
await changeSession(prior => {
|
|
332
|
+
if (prior?.state !== 'signed-in' || prior.generation !== generation || (prior.accountId !== null && prior.accountId !== accountId))
|
|
333
|
+
return null;
|
|
334
|
+
changed = true;
|
|
335
|
+
return { ...prior, accountId, tokens: session };
|
|
336
|
+
});
|
|
337
|
+
return changed;
|
|
338
|
+
}
|
|
339
|
+
export async function rejectSession(generation) {
|
|
340
|
+
await changeSession(prior => prior?.state === 'signed-in' && prior.generation === generation
|
|
341
|
+
? { schema: 1, generation: randomUUID(), accountId: prior.accountId, state: 'rejected' } : null);
|
|
342
|
+
}
|
|
343
|
+
export async function clearSession() {
|
|
344
|
+
let wasSignedIn = false;
|
|
345
|
+
await changeSession(prior => {
|
|
346
|
+
wasSignedIn = prior?.state === 'signed-in';
|
|
347
|
+
return { schema: 1, generation: randomUUID(), accountId: prior?.accountId ?? null, state: 'signed-out' };
|
|
348
|
+
});
|
|
349
|
+
return wasSignedIn;
|
|
168
350
|
}
|
|
169
351
|
/** This machine's project mappings, stored locally (not in the cloud) so
|
|
170
352
|
* absolute paths never leave the machine. Keyed by project id. */
|