@dharma-ai-labs/agent-fabric 0.2.102 → 0.2.104
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/demoPackage.d.ts +2 -2
- package/dist/demoPackage.d.ts.map +1 -1
- package/dist/demoPackage.js +4 -1
- package/dist/demoPackage.js.map +1 -1
- package/dist/demoPackage.test.js +42 -0
- package/dist/demoPackage.test.js.map +1 -1
- package/dist/demoStatus.d.ts +14 -0
- package/dist/demoStatus.d.ts.map +1 -0
- package/dist/demoStatus.js +11 -0
- package/dist/demoStatus.js.map +1 -0
- package/dist/demoSupervisor.d.ts +34 -0
- package/dist/demoSupervisor.d.ts.map +1 -0
- package/dist/demoSupervisor.js +164 -0
- package/dist/demoSupervisor.js.map +1 -0
- package/dist/demoSupervisor.test.d.ts +2 -0
- package/dist/demoSupervisor.test.d.ts.map +1 -0
- package/dist/demoSupervisor.test.js +249 -0
- package/dist/demoSupervisor.test.js.map +1 -0
- package/dist/demoWatchControl.d.ts +60 -0
- package/dist/demoWatchControl.d.ts.map +1 -0
- package/dist/demoWatchControl.js +128 -0
- package/dist/demoWatchControl.js.map +1 -0
- package/dist/demoWatchControl.test.d.ts +2 -0
- package/dist/demoWatchControl.test.d.ts.map +1 -0
- package/dist/demoWatchControl.test.js +184 -0
- package/dist/demoWatchControl.test.js.map +1 -0
- package/dist/demoWatchHealth.d.ts +26 -0
- package/dist/demoWatchHealth.d.ts.map +1 -0
- package/dist/demoWatchHealth.js +177 -0
- package/dist/demoWatchHealth.js.map +1 -0
- package/dist/demoWatchHealth.test.d.ts +2 -0
- package/dist/demoWatchHealth.test.d.ts.map +1 -0
- package/dist/demoWatchHealth.test.js +175 -0
- package/dist/demoWatchHealth.test.js.map +1 -0
- package/dist/demoWatchRegistry.d.ts +17 -0
- package/dist/demoWatchRegistry.d.ts.map +1 -0
- package/dist/demoWatchRegistry.js +170 -0
- package/dist/demoWatchRegistry.js.map +1 -0
- package/dist/demoWatchRegistry.test.d.ts +2 -0
- package/dist/demoWatchRegistry.test.d.ts.map +1 -0
- package/dist/demoWatchRegistry.test.js +146 -0
- package/dist/demoWatchRegistry.test.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +248 -82
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +69 -5
- package/dist/index.test.js.map +1 -1
- package/dist/pidLock.test.d.ts +2 -0
- package/dist/pidLock.test.d.ts.map +1 -0
- package/dist/pidLock.test.js +172 -0
- package/dist/pidLock.test.js.map +1 -0
- package/dist/relayAutostart.d.ts +17 -6
- package/dist/relayAutostart.d.ts.map +1 -1
- package/dist/relayAutostart.js +124 -19
- package/dist/relayAutostart.js.map +1 -1
- package/dist/relayAutostart.test.js +158 -1
- package/dist/relayAutostart.test.js.map +1 -1
- package/dist/relaySupervisor.test.js +75 -1
- package/dist/relaySupervisor.test.js.map +1 -1
- package/dist/repositoryOnboardingCaller.test.js +21 -0
- package/dist/repositoryOnboardingCaller.test.js.map +1 -1
- package/dist/schemas/demo-watch-control.v1.schema.json +27 -0
- package/dist/schemas/demo-watch-health.v1.schema.json +24 -0
- package/dist/schemas/demo-watch-registration.v1.schema.json +18 -0
- package/dist/schemas/relay-autostart.v2.schema.json +23 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +1 -1
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:
|
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
5
|
import { access, chmod, link, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
|
-
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
7
|
+
import { basename, dirname, isAbsolute, posix, relative, resolve, win32 } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { canonicalize, createActionDecisionPublicKeyResolver, sha256, validateContract, validateTrustedServerSigningKeysetContract, verifyCanonicalObject, verifyInitialServerSigningKeyset, } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
@@ -17,7 +17,7 @@ import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAu
|
|
|
17
17
|
import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
|
|
18
18
|
import { CLI_USAGE } from './usage.js';
|
|
19
19
|
import { superviseRelay } from './relaySupervisor.js';
|
|
20
|
-
import { disableRelayAutostart, enableRelayAutostart, relayAutostartStatus } from './relayAutostart.js';
|
|
20
|
+
import { disableRelayAutostart, enableRelayAutostart, relayAutostartStatus, startRelayAutostart } from './relayAutostart.js';
|
|
21
21
|
import { initializeRepositoryKnowledge, readRepositoryKnowledgeSource } from './repositoryKnowledge.js';
|
|
22
22
|
import { inventoryRepositoryPackage, readRepositoryPackageSnapshot, readRepositorySourceBaselineSnapshot, writeRepositoryPackageSnapshot } from './repositoryPackage.js';
|
|
23
23
|
import { validateRepositorySourceAuthorization } from './repositorySourceAuthorization.js';
|
|
@@ -38,8 +38,13 @@ import { waitForRepositoryReadiness } from './repositoryReadinessWait.js';
|
|
|
38
38
|
import { connectDemoDevice, verifyDemoDevice } from './demoEnrollment.js';
|
|
39
39
|
import { performDemoPeerAction, withDemoDeviceLock } from './demoPeer.js';
|
|
40
40
|
import { demoRepositoryPackage } from './demoPackage.js';
|
|
41
|
+
import { demoDeviceAndPackageStatus } from './demoStatus.js';
|
|
41
42
|
import { runDemoWatch } from './demoWatch.js';
|
|
42
|
-
|
|
43
|
+
import { runDemoSupervisor } from './demoSupervisor.js';
|
|
44
|
+
import { demoWatchRegistrationKey, listDemoWatchRegistrations } from './demoWatchRegistry.js';
|
|
45
|
+
import { createDemoWatchHealthRecorder } from './demoWatchHealth.js';
|
|
46
|
+
import { demoWatchStatus, disableDemoWatch, enableDemoWatch, inspectDemoWatchSupervisor } from './demoWatchControl.js';
|
|
47
|
+
const VERSION = '0.2.104';
|
|
43
48
|
const USAGE = CLI_USAGE;
|
|
44
49
|
const execFileAsync = promisify(execFile);
|
|
45
50
|
const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
|
|
@@ -576,10 +581,40 @@ async function acquirePidLock(lockPath, timeoutMs, timeoutMessage) {
|
|
|
576
581
|
throw error;
|
|
577
582
|
const recoveryPath = `${lockPath}.recovery`;
|
|
578
583
|
const recoveryCandidate = `${recoveryPath}.${process.pid}.${randomUUID()}.candidate`;
|
|
584
|
+
let windowsRecoveryOwner;
|
|
579
585
|
try {
|
|
580
586
|
await mkdir(recoveryCandidate);
|
|
581
587
|
await writeFile(resolve(recoveryCandidate, 'owner'), `${process.pid}\n`, { mode: 0o600 });
|
|
582
|
-
|
|
588
|
+
for (let attempt = 0;; attempt++) {
|
|
589
|
+
try {
|
|
590
|
+
await rename(recoveryCandidate, recoveryPath);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
catch (renameError) {
|
|
594
|
+
if (process.platform !== 'win32' || renameError.code !== 'EPERM')
|
|
595
|
+
throw renameError;
|
|
596
|
+
// Windows uses EPERM for an existing destination. Its owner can
|
|
597
|
+
// finish during inspection; retry publication, never ignore denial.
|
|
598
|
+
try {
|
|
599
|
+
const directory = await lstat(recoveryPath);
|
|
600
|
+
const ownerPath = resolve(recoveryPath, 'owner');
|
|
601
|
+
const ownerFile = await lstat(ownerPath);
|
|
602
|
+
const ownerText = (await readFile(ownerPath, 'utf8')).trim();
|
|
603
|
+
const owner = Number(ownerText);
|
|
604
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
605
|
+
|| !ownerFile.isFile() || ownerFile.isSymbolicLink()
|
|
606
|
+
|| !/^[1-9][0-9]*$/.test(ownerText) || !Number.isSafeInteger(owner))
|
|
607
|
+
throw renameError;
|
|
608
|
+
windowsRecoveryOwner = owner;
|
|
609
|
+
}
|
|
610
|
+
catch (inspectionError) {
|
|
611
|
+
if (inspectionError.code === 'ENOENT' && attempt < 3)
|
|
612
|
+
continue;
|
|
613
|
+
throw renameError;
|
|
614
|
+
}
|
|
615
|
+
throw renameError;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
583
618
|
try {
|
|
584
619
|
let ownerPid = 0;
|
|
585
620
|
try {
|
|
@@ -617,13 +652,21 @@ async function acquirePidLock(lockPath, timeoutMs, timeoutMessage) {
|
|
|
617
652
|
}
|
|
618
653
|
catch (recoveryError) {
|
|
619
654
|
await rm(recoveryCandidate, { recursive: true, force: true });
|
|
620
|
-
|
|
621
|
-
throw recoveryError;
|
|
655
|
+
const recoveryCode = recoveryError.code || '';
|
|
622
656
|
let recoveryOwner = 0;
|
|
623
|
-
|
|
624
|
-
|
|
657
|
+
if (process.platform === 'win32' && recoveryCode === 'EPERM') {
|
|
658
|
+
if (windowsRecoveryOwner === undefined)
|
|
659
|
+
throw recoveryError;
|
|
660
|
+
recoveryOwner = windowsRecoveryOwner;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(recoveryCode))
|
|
664
|
+
throw recoveryError;
|
|
665
|
+
try {
|
|
666
|
+
recoveryOwner = Number((await readFile(resolve(recoveryPath, 'owner'), 'utf8')).trim());
|
|
667
|
+
}
|
|
668
|
+
catch { }
|
|
625
669
|
}
|
|
626
|
-
catch { }
|
|
627
670
|
let recoveryOwnerAlive = Number.isSafeInteger(recoveryOwner) && recoveryOwner > 0;
|
|
628
671
|
if (recoveryOwnerAlive) {
|
|
629
672
|
try {
|
|
@@ -900,6 +943,15 @@ async function withFileLock(lockPath, operation, timeoutMessage = 'Timed out wai
|
|
|
900
943
|
await release();
|
|
901
944
|
}
|
|
902
945
|
}
|
|
946
|
+
export async function withRelayStartupMutation(operation, home = dharmaHome()) {
|
|
947
|
+
const release = await acquirePidLock(resolve(home, 'autostart-mutation.lock'), 30_000, 'demo_watch_startup_busy: another startup operation is still running.');
|
|
948
|
+
try {
|
|
949
|
+
return await operation();
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
await release();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
903
955
|
export async function withWorkspacePolicyRefreshLock(workspaceId, operation) {
|
|
904
956
|
if (!workspaceId)
|
|
905
957
|
throw new Error('Content transmission requires a registered workspace ID.');
|
|
@@ -1370,64 +1422,89 @@ export function requireCompletedBootstrapEvidence(result, discovered) {
|
|
|
1370
1422
|
+ 'Enrollment and repository setup were preserved; rerun bounded evidence synchronization after the service is available.');
|
|
1371
1423
|
}
|
|
1372
1424
|
async function archiveEnrollmentForAuthorizedRebind(existing) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
relayPid = Number((await readFile(pidPath, 'utf8')).trim());
|
|
1377
|
-
}
|
|
1378
|
-
catch { }
|
|
1379
|
-
if (Number.isSafeInteger(relayPid) && relayPid > 0) {
|
|
1380
|
-
try {
|
|
1381
|
-
process.kill(relayPid, 'SIGTERM');
|
|
1425
|
+
return withRelayStartupMutation(async () => {
|
|
1426
|
+
if ((await listDemoWatchRegistrations(dharmaHome())).length) {
|
|
1427
|
+
throw new Error('demo_watch_standard_rebind_conflict: preserve Demo scopes; standard rebind needs a scoped recovery path.');
|
|
1382
1428
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1429
|
+
const pidPath = resolve(dharmaHome(), 'relay', 'relay.pid');
|
|
1430
|
+
let relayPid = 0;
|
|
1431
|
+
try {
|
|
1432
|
+
relayPid = Number((await readFile(pidPath, 'utf8')).trim());
|
|
1386
1433
|
}
|
|
1387
|
-
|
|
1434
|
+
catch { }
|
|
1435
|
+
if (Number.isSafeInteger(relayPid) && relayPid > 0) {
|
|
1436
|
+
try {
|
|
1437
|
+
process.kill(relayPid, 'SIGTERM');
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
if (error.code !== 'ESRCH')
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1443
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
1444
|
+
try {
|
|
1445
|
+
process.kill(relayPid, 0);
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
if (error.code === 'ESRCH')
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
1452
|
+
}
|
|
1388
1453
|
try {
|
|
1389
1454
|
process.kill(relayPid, 0);
|
|
1455
|
+
throw new Error('The previous organization relay did not stop; enrollment was not changed.');
|
|
1390
1456
|
}
|
|
1391
1457
|
catch (error) {
|
|
1392
|
-
if (error.code
|
|
1393
|
-
|
|
1458
|
+
if (error.code !== 'ESRCH')
|
|
1459
|
+
throw error;
|
|
1394
1460
|
}
|
|
1395
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
1396
|
-
}
|
|
1397
|
-
try {
|
|
1398
|
-
process.kill(relayPid, 0);
|
|
1399
|
-
throw new Error('The previous organization relay did not stop; enrollment was not changed.');
|
|
1400
1461
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1462
|
+
const backupId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${createHash('sha256')
|
|
1463
|
+
.update(`${existing.hqUrl}\0${existing.organizationId}`)
|
|
1464
|
+
.digest('hex').slice(0, 12)}`;
|
|
1465
|
+
const backupRoot = resolve(dirname(dharmaHome()), '.dharma-rebind-backups', backupId);
|
|
1466
|
+
await disableRelayAutostart({ home: dharmaHome() });
|
|
1467
|
+
await mkdir(backupRoot, { recursive: true, mode: 0o700 });
|
|
1468
|
+
for (const relativePath of ['device.json', 'pending-enrollment.json', 'registry', 'relay', 'vault']) {
|
|
1469
|
+
const source = resolve(dharmaHome(), relativePath);
|
|
1470
|
+
if (!await pathExists(source))
|
|
1471
|
+
continue;
|
|
1472
|
+
await rename(source, resolve(backupRoot, relativePath));
|
|
1404
1473
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
for (const relativePath of ['device.json', 'pending-enrollment.json', 'registry', 'relay', 'vault']) {
|
|
1413
|
-
const source = resolve(dharmaHome(), relativePath);
|
|
1414
|
-
if (!await pathExists(source))
|
|
1415
|
-
continue;
|
|
1416
|
-
await rename(source, resolve(backupRoot, relativePath));
|
|
1417
|
-
}
|
|
1418
|
-
await writeJsonAtomic(resolve(backupRoot, 'RECEIPT.json'), {
|
|
1419
|
-
schema: 'dharma.local-enrollment-rebind-backup/v1',
|
|
1420
|
-
previousOrganizationId: existing.organizationId,
|
|
1421
|
-
previousPortalOrigin: existing.hqUrl,
|
|
1422
|
-
createdAt: new Date().toISOString(),
|
|
1474
|
+
await writeJsonAtomic(resolve(backupRoot, 'RECEIPT.json'), {
|
|
1475
|
+
schema: 'dharma.local-enrollment-rebind-backup/v1',
|
|
1476
|
+
previousOrganizationId: existing.organizationId,
|
|
1477
|
+
previousPortalOrigin: existing.hqUrl,
|
|
1478
|
+
createdAt: new Date().toISOString(),
|
|
1479
|
+
});
|
|
1480
|
+
return { changed: true, backupId, previousOrganizationId: existing.organizationId };
|
|
1423
1481
|
});
|
|
1424
|
-
return { changed: true, backupId, previousOrganizationId: existing.organizationId };
|
|
1425
1482
|
}
|
|
1426
|
-
export function stableRepositoryLauncherContents(version = VERSION) {
|
|
1483
|
+
export function stableRepositoryLauncherContents(version = VERSION, runtime) {
|
|
1484
|
+
if (!/^(?=.{1,64}$)\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(version)) {
|
|
1485
|
+
throw new Error('Managed launcher version is invalid.');
|
|
1486
|
+
}
|
|
1487
|
+
let shellEnvironment = '';
|
|
1488
|
+
let windowsEnvironment = '';
|
|
1489
|
+
if (runtime) {
|
|
1490
|
+
const directory = runtime.nodeDirectory;
|
|
1491
|
+
if (!directory || directory.length > 4096 || /[\r\n\0]/.test(directory)
|
|
1492
|
+
|| !(runtime.platform === 'win32' ? win32 : posix).isAbsolute(directory)) {
|
|
1493
|
+
throw new Error('Managed launcher runtime directory is invalid.');
|
|
1494
|
+
}
|
|
1495
|
+
if (runtime.platform === 'win32') {
|
|
1496
|
+
if (directory.includes('"'))
|
|
1497
|
+
throw new Error('Managed launcher runtime directory is invalid.');
|
|
1498
|
+
windowsEnvironment = `setlocal DisableDelayedExpansion\r\nset "PATH=${directory.replace(/%/g, '%%')};%PATH%"\r\n`;
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
shellEnvironment = `export PATH='${directory.replace(/'/g, "'\\''")}':"$PATH"\n`;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1427
1504
|
const invocation = `npm exec --yes -- @dharma-ai-labs/agent-fabric@${version}`;
|
|
1428
1505
|
return {
|
|
1429
|
-
shell: `#!/bin/sh\
|
|
1430
|
-
windows: `@echo off\r\n${invocation} %*\r\n`,
|
|
1506
|
+
shell: `#!/bin/sh\n${shellEnvironment}exec ${invocation} "$@"\n`,
|
|
1507
|
+
windows: `@echo off\r\n${windowsEnvironment}${invocation} %*\r\n`,
|
|
1431
1508
|
};
|
|
1432
1509
|
}
|
|
1433
1510
|
async function installStableRepositoryLauncher(workspace) {
|
|
@@ -1435,7 +1512,7 @@ async function installStableRepositoryLauncher(workspace) {
|
|
|
1435
1512
|
await mkdir(launcherRoot, { recursive: true, mode: 0o700 });
|
|
1436
1513
|
const shellPath = resolve(launcherRoot, 'dharma');
|
|
1437
1514
|
const cmdPath = resolve(launcherRoot, 'dharma.cmd');
|
|
1438
|
-
const contents = stableRepositoryLauncherContents();
|
|
1515
|
+
const contents = stableRepositoryLauncherContents(VERSION, { platform: process.platform, nodeDirectory: dirname(process.execPath) });
|
|
1439
1516
|
await writeFile(shellPath, contents.shell, { mode: 0o700 });
|
|
1440
1517
|
await chmod(shellPath, 0o700);
|
|
1441
1518
|
await writeFile(cmdPath, contents.windows, { mode: 0o600 });
|
|
@@ -1510,32 +1587,88 @@ async function startRelayDaemon(policyPath) {
|
|
|
1510
1587
|
const readiness = await waitForRelayReadiness({ expectedVersion: VERSION });
|
|
1511
1588
|
return { started: !alreadyRunning, supervisor: 'running', ...readiness };
|
|
1512
1589
|
}
|
|
1590
|
+
async function supervisedDemoCycle(registration, signal) {
|
|
1591
|
+
signal.throwIfAborted();
|
|
1592
|
+
const workspace = await realpath(registration.workspace);
|
|
1593
|
+
if (workspace !== registration.workspace)
|
|
1594
|
+
throw new Error('Demo watch checkout is no longer canonical.');
|
|
1595
|
+
const root = await gitValue(workspace, ['rev-parse', '--show-toplevel']);
|
|
1596
|
+
const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
|
|
1597
|
+
if (!root || !remote || normalizeGitRemoteIdentity(remote) !== registration.normalizedRepository) {
|
|
1598
|
+
throw Object.assign(new Error('Demo watch repository binding changed.'), { code: 'demo_watch_repository_changed' });
|
|
1599
|
+
}
|
|
1600
|
+
const scope = { hqUrl: registration.hqUrl, organizationId: registration.organizationId,
|
|
1601
|
+
repositoryId: registration.repositoryId, normalizedRepository: registration.normalizedRepository,
|
|
1602
|
+
installationId: await loadOrCreateInstallationId(), stateRoot: dharmaHome() };
|
|
1603
|
+
const nativeDirectory = registration.provider === 'codex' ? resolve(workspace, '.agents', 'skills')
|
|
1604
|
+
: registration.provider === 'claude' ? resolve(workspace, '.claude', 'skills')
|
|
1605
|
+
: nativeSkillDirectory(registration.provider);
|
|
1606
|
+
return withDemoDeviceLock(scope, () => {
|
|
1607
|
+
signal.throwIfAborted();
|
|
1608
|
+
return demoRepositoryPackage({ scope, workspace, provider: registration.provider,
|
|
1609
|
+
nativeSkillDirectory: nativeDirectory }, { fetcher: (url, options) => {
|
|
1610
|
+
signal.throwIfAborted();
|
|
1611
|
+
return fetch(url, { ...options,
|
|
1612
|
+
signal: options?.signal ? AbortSignal.any([options.signal, signal]) : signal });
|
|
1613
|
+
} });
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1513
1616
|
async function relaySupervise(flags) {
|
|
1514
|
-
const
|
|
1515
|
-
if (
|
|
1516
|
-
throw new Error('
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1617
|
+
const demoOnly = flags.has('demo-only');
|
|
1618
|
+
if (demoOnly && flags.has('policy'))
|
|
1619
|
+
throw new Error('Demo-only supervision cannot select a standard policy.');
|
|
1620
|
+
const policyPath = demoOnly ? null : resolve(required(flags, 'policy'));
|
|
1621
|
+
const selectedWorkspace = policyPath
|
|
1622
|
+
? (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath) : null;
|
|
1623
|
+
if (policyPath) {
|
|
1624
|
+
if (!await readDeviceConfig())
|
|
1625
|
+
throw new Error('Relay supervision requires an enrolled device.');
|
|
1626
|
+
if (!selectedWorkspace)
|
|
1627
|
+
throw new Error('Relay policy must be the canonical policy of one registered workspace.');
|
|
1628
|
+
}
|
|
1629
|
+
else if (!(await listDemoWatchRegistrations(dharmaHome())).length) {
|
|
1630
|
+
throw new Error('Relay supervision requires at least one registered Demo repository.');
|
|
1631
|
+
}
|
|
1520
1632
|
const releaseLease = await acquireRelaySupervisorLease();
|
|
1521
1633
|
const bindingPath = resolve(dharmaHome(), 'relay', 'supervisor-workspace.json');
|
|
1522
1634
|
const controller = new AbortController();
|
|
1523
1635
|
const stop = () => controller.abort();
|
|
1524
1636
|
process.on('SIGINT', stop);
|
|
1525
1637
|
process.on('SIGTERM', stop);
|
|
1638
|
+
const services = [];
|
|
1639
|
+
const health = createDemoWatchHealthRecorder({ home: dharmaHome(), pid: process.pid, version: VERSION });
|
|
1526
1640
|
try {
|
|
1527
|
-
await writeJsonAtomic(bindingPath, { pid: process.pid, workspaceId: selectedWorkspace
|
|
1528
|
-
|
|
1641
|
+
await writeJsonAtomic(bindingPath, { pid: process.pid, workspaceId: selectedWorkspace?.workspaceId ?? null,
|
|
1642
|
+
policyPath, demoWatches: true, version: VERSION });
|
|
1643
|
+
const demo = runDemoSupervisor({ signal: controller.signal,
|
|
1644
|
+
list: () => listDemoWatchRegistrations(dharmaHome()), cycle: (registration, signal) => {
|
|
1645
|
+
if (!health.available(demoWatchRegistrationKey(registration))) {
|
|
1646
|
+
throw Object.assign(new Error('Demo watch health storage is unavailable.'), { code: 'demo_watch_health_unavailable' });
|
|
1647
|
+
}
|
|
1648
|
+
return supervisedDemoCycle(registration, signal);
|
|
1649
|
+
},
|
|
1650
|
+
onObservation: observation => {
|
|
1651
|
+
health.record(observation);
|
|
1652
|
+
process.stderr.write(`${JSON.stringify({ event: 'demo_watch_cycle', ...observation })}\n`);
|
|
1653
|
+
},
|
|
1654
|
+
});
|
|
1655
|
+
services.push(demo);
|
|
1656
|
+
const standard = policyPath ? superviseRelay({
|
|
1529
1657
|
signal: controller.signal,
|
|
1530
1658
|
start: () => spawn(process.execPath, [fileURLToPath(import.meta.url), 'relay', 'start', '--policy', policyPath], {
|
|
1531
1659
|
cwd: dirname(dirname(policyPath)),
|
|
1532
1660
|
stdio: 'ignore',
|
|
1533
1661
|
env: process.env,
|
|
1534
1662
|
}),
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1663
|
+
}) : Promise.resolve({ restarts: 0 });
|
|
1664
|
+
services.push(standard);
|
|
1665
|
+
const [demoResult, standardResult] = await Promise.all([demo, standard]);
|
|
1666
|
+
return { ok: true, stopped: true, ...standardResult, demo: demoResult };
|
|
1537
1667
|
}
|
|
1538
1668
|
finally {
|
|
1669
|
+
controller.abort();
|
|
1670
|
+
await Promise.allSettled(services);
|
|
1671
|
+
await health.drain();
|
|
1539
1672
|
process.removeListener('SIGINT', stop);
|
|
1540
1673
|
process.removeListener('SIGTERM', stop);
|
|
1541
1674
|
const binding = await readFile(bindingPath, 'utf8')
|
|
@@ -1812,11 +1945,11 @@ async function bootstrap(flags) {
|
|
|
1812
1945
|
const launcher = await installStableRepositoryLauncher(workspace);
|
|
1813
1946
|
const autostart = flags.has('no-relay-daemon')
|
|
1814
1947
|
? { state: 'disabled', backend: null }
|
|
1815
|
-
: await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => enableRelayAutostart({
|
|
1948
|
+
: await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(() => enableRelayAutostart({
|
|
1816
1949
|
home: dharmaHome(), workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
|
|
1817
1950
|
launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
|
|
1818
1951
|
version: VERSION,
|
|
1819
|
-
}));
|
|
1952
|
+
})));
|
|
1820
1953
|
let sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
|
|
1821
1954
|
const completionRequested = flags.has('complete');
|
|
1822
1955
|
if (!completionRequested) {
|
|
@@ -5762,7 +5895,7 @@ export async function run(argv) {
|
|
|
5762
5895
|
if (flags.has('version') || command === 'version')
|
|
5763
5896
|
return { version: VERSION };
|
|
5764
5897
|
if (command === 'demo' && ['connect', 'status', 'role', 'peers', 'ask', 'reply', 'inbox', 'ack', 'resume',
|
|
5765
|
-
'package', 'package-status', 'watch']
|
|
5898
|
+
'package', 'package-status', 'watch', 'watch-enable', 'watch-status', 'watch-disable']
|
|
5766
5899
|
.includes(String(subcommand))) {
|
|
5767
5900
|
const hqUrl = normalizeHqUrl(portalUrl(flags));
|
|
5768
5901
|
const organizationId = required(flags, 'organization-id');
|
|
@@ -5776,8 +5909,12 @@ export async function run(argv) {
|
|
|
5776
5909
|
if (!remote || normalizeGitRemoteIdentity(remote) !== normalizedRepository) {
|
|
5777
5910
|
throw new Error('Local Git remote does not match the private Demo repository binding.');
|
|
5778
5911
|
}
|
|
5912
|
+
const watchControl = ['watch-enable', 'watch-status', 'watch-disable'].includes(String(subcommand));
|
|
5913
|
+
if (watchControl && await realpath(root) !== workspace) {
|
|
5914
|
+
throw new Error('Demo watch setup must select the exact Git repository root.');
|
|
5915
|
+
}
|
|
5779
5916
|
if (flags.has('dry-run'))
|
|
5780
|
-
return { ok: true, stage: 'demo_device_plan',
|
|
5917
|
+
return { ok: true, stage: watchControl ? 'demo_watch_plan' : 'demo_device_plan',
|
|
5781
5918
|
organizationId, repositoryId, normalizedRepository, workspaceVerified: true,
|
|
5782
5919
|
repositoryPackageState: 'not_connected' };
|
|
5783
5920
|
const grant = subcommand === 'connect' ? required(flags, 'grant') : null;
|
|
@@ -5785,6 +5922,35 @@ export async function run(argv) {
|
|
|
5785
5922
|
hqUrl, organizationId, repositoryId, normalizedRepository,
|
|
5786
5923
|
installationId: await loadOrCreateInstallationId(), stateRoot: dharmaHome(),
|
|
5787
5924
|
};
|
|
5925
|
+
if (watchControl) {
|
|
5926
|
+
const existing = (await listDemoWatchRegistrations(dharmaHome()))
|
|
5927
|
+
.find(row => row.hqUrl === hqUrl && row.organizationId === organizationId && row.repositoryId === repositoryId);
|
|
5928
|
+
const requestedProvider = String(flags.get('provider') || 'auto').trim().toLowerCase();
|
|
5929
|
+
const provider = requestedProvider !== 'auto' ? requestedProvider : existing?.provider
|
|
5930
|
+
?? (subcommand === 'watch-enable' ? await detectBootstrapProvider(workspace) : 'codex');
|
|
5931
|
+
if (!isLocalProviderId(provider))
|
|
5932
|
+
throw new Error('Demo watch provider must be auto, codex, claude, agy, or hermes.');
|
|
5933
|
+
const registration = { schema: 'dharma.demo-watch/v1',
|
|
5934
|
+
hqUrl, organizationId, repositoryId, normalizedRepository, provider, workspace };
|
|
5935
|
+
const input = { home: dharmaHome(), registration, version: VERSION };
|
|
5936
|
+
const deps = {
|
|
5937
|
+
verify: async () => { await withDemoDeviceLock(scope, () => verifyDemoDevice(scope)); },
|
|
5938
|
+
exclusive: operation => withRelayStartupMutation(operation),
|
|
5939
|
+
prepareStartup: async () => {
|
|
5940
|
+
const launcher = await installStableRepositoryLauncher(workspace);
|
|
5941
|
+
await enableRelayAutostart({ home: dharmaHome(), workspace, policy: null, version: VERSION,
|
|
5942
|
+
launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell) });
|
|
5943
|
+
},
|
|
5944
|
+
start: async () => { await startRelayAutostart({ home: dharmaHome() }); },
|
|
5945
|
+
autostart: async () => (await relayAutostartStatus({ home: dharmaHome() })).state,
|
|
5946
|
+
supervisor: () => inspectDemoWatchSupervisor(dharmaHome(), VERSION, async () => {
|
|
5947
|
+
const supervisor = await relaySupervisorProcessState();
|
|
5948
|
+
return supervisor === 'stopped' && await relayProcessState() !== 'stopped' ? 'unknown' : supervisor;
|
|
5949
|
+
}),
|
|
5950
|
+
};
|
|
5951
|
+
return subcommand === 'watch-enable' ? enableDemoWatch(input, deps)
|
|
5952
|
+
: subcommand === 'watch-disable' ? disableDemoWatch(input, deps) : demoWatchStatus(input, deps);
|
|
5953
|
+
}
|
|
5788
5954
|
if (['package', 'package-status', 'watch'].includes(String(subcommand))) {
|
|
5789
5955
|
const requestedProvider = String(flags.get('provider') || 'auto').trim().toLowerCase();
|
|
5790
5956
|
const provider = subcommand === 'package-status' && requestedProvider === 'auto' ? 'codex'
|
|
@@ -5821,18 +5987,18 @@ export async function run(argv) {
|
|
|
5821
5987
|
}
|
|
5822
5988
|
}
|
|
5823
5989
|
return withDemoDeviceLock(scope, async () => {
|
|
5824
|
-
if (subcommand === '
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5990
|
+
if (subcommand === 'status')
|
|
5991
|
+
return demoDeviceAndPackageStatus(scope, workspace);
|
|
5992
|
+
if (subcommand === 'connect') {
|
|
5993
|
+
const connected = await connectDemoDevice({
|
|
5994
|
+
...scope, grant: grant,
|
|
5995
|
+
deviceName: String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`),
|
|
5996
|
+
platform: await platform(),
|
|
5997
|
+
}, { onApprovalRequired: async (url) => {
|
|
5998
|
+
if (!flags.has('no-browser'))
|
|
5999
|
+
await openVerificationUri(url);
|
|
6000
|
+
process.stderr.write(`Approve this exact Demo device in your signed-in Dharma session: ${url}\n`);
|
|
6001
|
+
} });
|
|
5836
6002
|
const { configPath: _configPath, ...receipt } = connected;
|
|
5837
6003
|
return receipt;
|
|
5838
6004
|
}
|
|
@@ -5970,7 +6136,7 @@ export async function run(argv) {
|
|
|
5970
6136
|
if (positional[2] === 'status')
|
|
5971
6137
|
return relayAutostartStatus({ home: dharmaHome() });
|
|
5972
6138
|
if (positional[2] === 'disable')
|
|
5973
|
-
return disableRelayAutostart({ home: dharmaHome() });
|
|
6139
|
+
return withRelayStartupMutation(() => disableRelayAutostart({ home: dharmaHome() }));
|
|
5974
6140
|
throw new Error('Use dharma relay autostart status or dharma relay autostart disable.');
|
|
5975
6141
|
}
|
|
5976
6142
|
if (command === 'relay' && subcommand === 'supervise')
|