@dharma-ai-labs/agent-fabric 0.1.13 → 0.1.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 +39 -4
- package/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +501 -43
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +140 -3
- package/dist/index.test.js.map +1 -1
- package/dist/usage.d.ts +2 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +2 -0
- package/dist/usage.js.map +1 -0
- package/package.json +31 -11
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
|
-
import { access, link, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { access, link, mkdir, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -12,34 +12,40 @@ import { buildTrajectoryCapsule, redactValue, referencesExcludedPath } from '@dh
|
|
|
12
12
|
import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } from '@dharma-ai-labs/agent-fabric-policy';
|
|
13
13
|
import { agyAdapter, claudeAdapter, codexAdapter, providerAdapters } from '@dharma-ai-labs/agent-fabric-provider-adapters';
|
|
14
14
|
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
15
|
+
import { AgentFabricClient as AgentFabricApiClient } from '@dharma-ai-labs/agent-fabric-sdk';
|
|
15
16
|
import { getActiveSkillBundleId, installSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
|
|
16
17
|
import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
|
|
17
|
-
|
|
18
|
-
const
|
|
18
|
+
import { CLI_USAGE } from './usage.js';
|
|
19
|
+
const VERSION = '0.1.16';
|
|
20
|
+
const USAGE = CLI_USAGE;
|
|
19
21
|
const execFileAsync = promisify(execFile);
|
|
20
|
-
function
|
|
22
|
+
export function parseCliOptions(args) {
|
|
21
23
|
const positional = [];
|
|
22
24
|
const flags = new Map();
|
|
25
|
+
const repeated = new Map();
|
|
23
26
|
for (let index = 0; index < args.length; index += 1) {
|
|
24
27
|
const value = args[index];
|
|
25
28
|
if (!value.startsWith('--')) {
|
|
26
29
|
positional.push(value);
|
|
27
30
|
continue;
|
|
28
31
|
}
|
|
29
|
-
const
|
|
32
|
+
const rawOption = value.slice(2);
|
|
33
|
+
const separator = rawOption.indexOf('=');
|
|
34
|
+
const rawKey = separator < 0 ? rawOption : rawOption.slice(0, separator);
|
|
35
|
+
const inline = separator < 0 ? undefined : rawOption.slice(separator + 1);
|
|
30
36
|
if (inline !== undefined) {
|
|
31
37
|
flags.set(rawKey, inline);
|
|
38
|
+
repeated.set(rawKey, [...(repeated.get(rawKey) || []), inline]);
|
|
32
39
|
continue;
|
|
33
40
|
}
|
|
34
41
|
const next = args[index + 1];
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const parsed = next && !next.startsWith('--') ? next : true;
|
|
43
|
+
flags.set(rawKey, parsed);
|
|
44
|
+
repeated.set(rawKey, [...(repeated.get(rawKey) || []), parsed]);
|
|
45
|
+
if (parsed !== true)
|
|
37
46
|
index += 1;
|
|
38
|
-
}
|
|
39
|
-
else
|
|
40
|
-
flags.set(rawKey, true);
|
|
41
47
|
}
|
|
42
|
-
return { positional, flags };
|
|
48
|
+
return { positional, flags, repeated };
|
|
43
49
|
}
|
|
44
50
|
function required(flags, name) {
|
|
45
51
|
const value = flags.get(name);
|
|
@@ -47,6 +53,9 @@ function required(flags, name) {
|
|
|
47
53
|
throw new Error(`Missing required option --${name}.`);
|
|
48
54
|
return value;
|
|
49
55
|
}
|
|
56
|
+
export function portalUrl(flags, fallback = 'https://www.dharma-ai.io') {
|
|
57
|
+
return String(flags.get('portal-url') || flags.get('hq-url') || fallback);
|
|
58
|
+
}
|
|
50
59
|
function print(value) {
|
|
51
60
|
process.stdout.write(typeof value === 'string' ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`);
|
|
52
61
|
}
|
|
@@ -808,6 +817,62 @@ function deterministicUuid(value) {
|
|
|
808
817
|
const hex = bytes.toString('hex');
|
|
809
818
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
810
819
|
}
|
|
820
|
+
export function normalizeGitRemoteIdentity(value) {
|
|
821
|
+
const raw = value.trim();
|
|
822
|
+
if (!raw || /[\u0000-\u001f\u007f]/.test(raw))
|
|
823
|
+
throw new Error('Git remote is empty or invalid.');
|
|
824
|
+
let host;
|
|
825
|
+
let repositoryPath;
|
|
826
|
+
const scp = raw.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
|
|
827
|
+
if (!raw.includes('://') && scp && !/^[A-Za-z]:[\\/]/.test(raw)) {
|
|
828
|
+
host = scp[1].toLowerCase();
|
|
829
|
+
repositoryPath = scp[2];
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
let remote;
|
|
833
|
+
try {
|
|
834
|
+
remote = new URL(raw);
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
throw new Error('Git remote must be a hosted URL or use --repository-key.');
|
|
838
|
+
}
|
|
839
|
+
if (!['https:', 'http:', 'ssh:', 'git:'].includes(remote.protocol) || !remote.hostname) {
|
|
840
|
+
throw new Error('Local and file Git remotes require an explicit stable --repository-key.');
|
|
841
|
+
}
|
|
842
|
+
host = remote.hostname.toLowerCase();
|
|
843
|
+
const port = remote.port && !((remote.protocol === 'https:' && remote.port === '443') || (remote.protocol === 'http:' && remote.port === '80'))
|
|
844
|
+
? `:${remote.port}` : '';
|
|
845
|
+
host += port;
|
|
846
|
+
repositoryPath = remote.pathname;
|
|
847
|
+
}
|
|
848
|
+
repositoryPath = repositoryPath.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
|
|
849
|
+
if (!repositoryPath || repositoryPath.includes('..') || /\s/.test(repositoryPath)) {
|
|
850
|
+
throw new Error('Git remote repository path is invalid.');
|
|
851
|
+
}
|
|
852
|
+
if (host === 'github.com' || host === 'gitlab.com')
|
|
853
|
+
repositoryPath = repositoryPath.toLowerCase();
|
|
854
|
+
return `${host}/${repositoryPath}`;
|
|
855
|
+
}
|
|
856
|
+
export function sourceRepositoryFingerprint(remote, explicitKey) {
|
|
857
|
+
const key = explicitKey?.trim();
|
|
858
|
+
let identity;
|
|
859
|
+
let source;
|
|
860
|
+
if (key) {
|
|
861
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(key) || key.includes('..')) {
|
|
862
|
+
throw new Error('--repository-key must be a stable 1-160 character identifier without secrets or path traversal.');
|
|
863
|
+
}
|
|
864
|
+
identity = `key:${key}`;
|
|
865
|
+
source = 'explicit_key';
|
|
866
|
+
}
|
|
867
|
+
else if (remote) {
|
|
868
|
+
identity = `remote:${normalizeGitRemoteIdentity(remote)}`;
|
|
869
|
+
source = 'remote';
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
throw new Error('Repository has no hosted Git remote. Supply a stable --repository-key for this repository.');
|
|
873
|
+
}
|
|
874
|
+
return { fingerprint: `sha256:${createHash('sha256').update(identity).digest('hex')}`, source };
|
|
875
|
+
}
|
|
811
876
|
function responseTextFromEvent(value) {
|
|
812
877
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
813
878
|
return null;
|
|
@@ -901,6 +966,15 @@ async function registry() {
|
|
|
901
966
|
return [];
|
|
902
967
|
}
|
|
903
968
|
}
|
|
969
|
+
async function saveRegistry(items) {
|
|
970
|
+
await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
|
|
971
|
+
await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
|
|
972
|
+
}
|
|
973
|
+
async function saveWorkspaceRecord(entry) {
|
|
974
|
+
const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
|
|
975
|
+
items.push(entry);
|
|
976
|
+
await saveRegistry(items);
|
|
977
|
+
}
|
|
904
978
|
async function gitValue(workspace, argv) {
|
|
905
979
|
try {
|
|
906
980
|
return (await execFileAsync('git', ['-C', workspace, ...argv], { timeout: 10_000 })).stdout.trim() || null;
|
|
@@ -914,6 +988,121 @@ async function client() {
|
|
|
914
988
|
await instance.openSession(VERSION);
|
|
915
989
|
return instance;
|
|
916
990
|
}
|
|
991
|
+
async function organizationApi(flags) {
|
|
992
|
+
let enrolled = null;
|
|
993
|
+
try {
|
|
994
|
+
enrolled = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
995
|
+
}
|
|
996
|
+
catch { }
|
|
997
|
+
const organizationId = String(flags.get('organization-id') || enrolled?.organizationId || '').trim();
|
|
998
|
+
if (!organizationId)
|
|
999
|
+
throw new Error('Organization command requires --organization-id or an enrolled device.');
|
|
1000
|
+
const token = String(process.env.DHARMA_ORG_API_TOKEN || '').trim();
|
|
1001
|
+
if (!token)
|
|
1002
|
+
throw new Error('Organization command requires DHARMA_ORG_API_TOKEN. Tokens are not accepted on the command line.');
|
|
1003
|
+
return new AgentFabricApiClient({
|
|
1004
|
+
organizationId,
|
|
1005
|
+
token,
|
|
1006
|
+
baseUrl: portalUrl(flags, enrolled?.hqUrl || 'https://www.dharma-ai.io'),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
async function commandJsonBody(flags) {
|
|
1010
|
+
const inline = flags.get('json-body');
|
|
1011
|
+
const path = flags.get('body-file');
|
|
1012
|
+
if (typeof inline === 'string' && typeof path === 'string') {
|
|
1013
|
+
throw new Error('Use either --json-body or --body-file, not both.');
|
|
1014
|
+
}
|
|
1015
|
+
const serialized = typeof inline === 'string'
|
|
1016
|
+
? inline
|
|
1017
|
+
: typeof path === 'string'
|
|
1018
|
+
? await readFile(resolve(path), 'utf8')
|
|
1019
|
+
: '{}';
|
|
1020
|
+
const parsed = JSON.parse(serialized);
|
|
1021
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1022
|
+
throw new Error('Command body must be a JSON object.');
|
|
1023
|
+
}
|
|
1024
|
+
return parsed;
|
|
1025
|
+
}
|
|
1026
|
+
export function requireExplicitConfirmation(flags, action) {
|
|
1027
|
+
if (flags.get('confirm') !== true) {
|
|
1028
|
+
throw new Error(`${action} requires --confirm after reviewing organization scope, cost, and authority.`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async function runOrganizationCommand(command, subcommand, flags) {
|
|
1032
|
+
const api = await organizationApi(flags);
|
|
1033
|
+
if (command === 'organization' && subcommand === 'status')
|
|
1034
|
+
return api.instructions();
|
|
1035
|
+
if (command === 'agents' && subcommand === 'list')
|
|
1036
|
+
return api.listAgents();
|
|
1037
|
+
if (command === 'agents' && subcommand === 'bind-runtime') {
|
|
1038
|
+
requireExplicitConfirmation(flags, 'Binding a managed or cloud BYOK runtime endpoint');
|
|
1039
|
+
const agentId = required(flags, 'agent-id');
|
|
1040
|
+
const body = await commandJsonBody(flags);
|
|
1041
|
+
const endpointKind = String(body.endpointKind || '');
|
|
1042
|
+
if (!['managed_runtime', 'cloud_byok'].includes(endpointKind)) {
|
|
1043
|
+
throw new Error('Runtime endpoint kind must be managed_runtime or cloud_byok.');
|
|
1044
|
+
}
|
|
1045
|
+
return api.bindRuntimeEndpoint(agentId, {
|
|
1046
|
+
endpointKind: endpointKind,
|
|
1047
|
+
managedAgentId: String(body.managedAgentId || ''),
|
|
1048
|
+
runtimeBindingId: String(body.runtimeBindingId || ''),
|
|
1049
|
+
...(body.priority === undefined ? {} : { priority: Number(body.priority) }),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (command === 'experiments' && subcommand === 'list')
|
|
1053
|
+
return api.listAnalysisWindows();
|
|
1054
|
+
if (command === 'experiments' && subcommand === 'run') {
|
|
1055
|
+
requireExplicitConfirmation(flags, 'Running an experiment');
|
|
1056
|
+
return api.requestAnalysis(await commandJsonBody(flags));
|
|
1057
|
+
}
|
|
1058
|
+
if (command === 'failures' && subcommand === 'list')
|
|
1059
|
+
return api.listFailures();
|
|
1060
|
+
if (command === 'remediations' && subcommand === 'list')
|
|
1061
|
+
return api.listRemediations();
|
|
1062
|
+
if (command === 'remediations' && subcommand === 'act') {
|
|
1063
|
+
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1064
|
+
const targetId = required(flags, 'target-id');
|
|
1065
|
+
const body = await commandJsonBody(flags);
|
|
1066
|
+
const action = String(flags.get('action') || body.action || '');
|
|
1067
|
+
if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1068
|
+
throw new Error('Remediation action must be run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1069
|
+
}
|
|
1070
|
+
return api.transitionRemediationTarget(targetId, {
|
|
1071
|
+
...body,
|
|
1072
|
+
action: action,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (command === 'skills' && subcommand === 'list')
|
|
1076
|
+
return api.listSkills();
|
|
1077
|
+
if (command === 'skills' && subcommand === 'release') {
|
|
1078
|
+
requireExplicitConfirmation(flags, 'Releasing a skill');
|
|
1079
|
+
return api.releaseSkill(await commandJsonBody(flags));
|
|
1080
|
+
}
|
|
1081
|
+
if (command === 'skills' && (subcommand === 'rollout' || subcommand === 'rollback')) {
|
|
1082
|
+
requireExplicitConfirmation(flags, `${subcommand === 'rollback' ? 'Rolling back' : 'Rolling out'} a skill`);
|
|
1083
|
+
const bundleId = required(flags, 'bundle-id');
|
|
1084
|
+
const body = await commandJsonBody(flags);
|
|
1085
|
+
return api.transitionSkillRollout(bundleId, {
|
|
1086
|
+
action: subcommand === 'rollback' ? 'rollback' : String(body.action || 'start'),
|
|
1087
|
+
...(body.canaryPercent === undefined ? {} : { canaryPercent: Number(body.canaryPercent) }),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (command === 'tasks' && subcommand === 'list')
|
|
1091
|
+
return api.listTasks();
|
|
1092
|
+
if (command === 'tasks' && subcommand === 'dispatch') {
|
|
1093
|
+
requireExplicitConfirmation(flags, 'Dispatching a task');
|
|
1094
|
+
return api.dispatchTask(await commandJsonBody(flags));
|
|
1095
|
+
}
|
|
1096
|
+
if (command === 'handoffs' && subcommand === 'list')
|
|
1097
|
+
return api.listHandoffs();
|
|
1098
|
+
if (command === 'handoffs' && subcommand === 'dispatch') {
|
|
1099
|
+
requireExplicitConfirmation(flags, 'Dispatching an A2A handoff');
|
|
1100
|
+
return api.dispatchHandoff(await commandJsonBody(flags));
|
|
1101
|
+
}
|
|
1102
|
+
if (command === 'usage' && subcommand === 'list')
|
|
1103
|
+
return api.usage();
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
917
1106
|
async function openVerificationUri(url) {
|
|
918
1107
|
const attempts = process.platform === 'darwin'
|
|
919
1108
|
? [['open', [url]]]
|
|
@@ -939,7 +1128,7 @@ async function login(flags) {
|
|
|
939
1128
|
pending = JSON.parse(await readFile(pendingEnrollmentPath(), 'utf8'));
|
|
940
1129
|
}
|
|
941
1130
|
else {
|
|
942
|
-
const hqUrl = normalizeHqUrl(
|
|
1131
|
+
const hqUrl = normalizeHqUrl(portalUrl(flags));
|
|
943
1132
|
const organizationId = required(flags, 'organization-id');
|
|
944
1133
|
const name = String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`);
|
|
945
1134
|
const devicePlatform = await platform();
|
|
@@ -1213,19 +1402,207 @@ async function workspaceAdd(flags, positional) {
|
|
|
1213
1402
|
const organizationId = String(flags.get('organization-id') || device.organizationId);
|
|
1214
1403
|
if (organizationId !== device.organizationId)
|
|
1215
1404
|
throw new Error('Workspace organization must match the enrolled device.');
|
|
1216
|
-
await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
|
|
1217
1405
|
const workspaceId = deterministicUuid(`${organizationId}:${device.deviceId}:${path}`);
|
|
1218
1406
|
const remote = await gitValue(path, ['config', '--get', 'remote.origin.url']);
|
|
1407
|
+
const identity = sourceRepositoryFingerprint(remote, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
|
|
1219
1408
|
const entry = {
|
|
1220
1409
|
workspaceId, organizationId, name: String(flags.get('name') || basename(path)), path,
|
|
1221
1410
|
routeHash: `sha256:${createHash('sha256').update(path).digest('hex')}`,
|
|
1222
|
-
repositoryRemoteHash:
|
|
1411
|
+
repositoryRemoteHash: identity.fingerprint,
|
|
1412
|
+
repositoryIdentityVersion: 'normalized-v1',
|
|
1413
|
+
repositoryAgentId: null,
|
|
1414
|
+
repositoryBindingId: null,
|
|
1415
|
+
repositoryAgentKey: null,
|
|
1416
|
+
controlBranch: null,
|
|
1223
1417
|
defaultBranch: await gitValue(path, ['branch', '--show-current']), status: 'active',
|
|
1224
1418
|
};
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1419
|
+
await saveWorkspaceRecord(entry);
|
|
1420
|
+
return {
|
|
1421
|
+
ok: true,
|
|
1422
|
+
workspaceId,
|
|
1423
|
+
organizationId,
|
|
1424
|
+
sourceFingerprint: identity.fingerprint,
|
|
1425
|
+
repositoryIdentitySource: identity.source,
|
|
1426
|
+
pathStoredLocally: true,
|
|
1427
|
+
pathDisclosedToServer: false,
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
async function discoverRepositoryAt(path) {
|
|
1431
|
+
const canonical = await realpath(path);
|
|
1432
|
+
const topLevel = await gitValue(canonical, ['rev-parse', '--show-toplevel']);
|
|
1433
|
+
if (!topLevel)
|
|
1434
|
+
return null;
|
|
1435
|
+
const workspace = await realpath(topLevel);
|
|
1436
|
+
const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
|
|
1437
|
+
let normalizedRemote = null;
|
|
1438
|
+
let fingerprint = null;
|
|
1439
|
+
if (remote) {
|
|
1440
|
+
normalizedRemote = normalizeGitRemoteIdentity(remote);
|
|
1441
|
+
fingerprint = sourceRepositoryFingerprint(remote).fingerprint;
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
name: basename(workspace),
|
|
1445
|
+
path: workspace,
|
|
1446
|
+
remote: normalizedRemote,
|
|
1447
|
+
sourceFingerprint: fingerprint,
|
|
1448
|
+
defaultBranch: await gitValue(workspace, ['branch', '--show-current']),
|
|
1449
|
+
connectable: Boolean(fingerprint),
|
|
1450
|
+
requiredAction: fingerprint ? null : 'Supply --repository-key when connecting this repository.',
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
async function repositoriesDiscover(flags, positional, roots) {
|
|
1454
|
+
const requested = [...positional, ...roots.filter((value) => typeof value === 'string')];
|
|
1455
|
+
if (requested.length === 0)
|
|
1456
|
+
requested.push(String(flags.get('root') || '.'));
|
|
1457
|
+
const discovered = new Map();
|
|
1458
|
+
for (const requestedRoot of requested) {
|
|
1459
|
+
const root = await realpath(requestedRoot);
|
|
1460
|
+
const direct = await discoverRepositoryAt(root);
|
|
1461
|
+
if (direct) {
|
|
1462
|
+
discovered.set(direct.path, direct);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
1466
|
+
if (!entry.isDirectory())
|
|
1467
|
+
continue;
|
|
1468
|
+
const candidate = await discoverRepositoryAt(resolve(root, entry.name));
|
|
1469
|
+
if (candidate)
|
|
1470
|
+
discovered.set(candidate.path, candidate);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
ok: true,
|
|
1475
|
+
scanBoundary: 'explicit_roots_and_immediate_children',
|
|
1476
|
+
repositories: [...discovered.values()].filter(Boolean),
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function repositoryKeyAssignments(paths, values) {
|
|
1480
|
+
const keys = values.filter((value) => typeof value === 'string');
|
|
1481
|
+
const assignments = new Map();
|
|
1482
|
+
if (keys.length === 0)
|
|
1483
|
+
return assignments;
|
|
1484
|
+
const named = keys.filter((value) => value.includes('='));
|
|
1485
|
+
if (named.length > 0 && named.length !== keys.length) {
|
|
1486
|
+
throw new Error('Use either ordered --repository-key values or path=key assignments, not both.');
|
|
1487
|
+
}
|
|
1488
|
+
if (named.length > 0) {
|
|
1489
|
+
for (const value of named) {
|
|
1490
|
+
const split = value.indexOf('=');
|
|
1491
|
+
const path = resolve(value.slice(0, split));
|
|
1492
|
+
const index = paths.findIndex((candidate) => resolve(candidate) === path);
|
|
1493
|
+
if (index < 0)
|
|
1494
|
+
throw new Error(`Repository key assignment does not match a selected repository: ${value.slice(0, split)}`);
|
|
1495
|
+
assignments.set(index, value.slice(split + 1));
|
|
1496
|
+
}
|
|
1497
|
+
return assignments;
|
|
1498
|
+
}
|
|
1499
|
+
if (keys.length !== paths.length) {
|
|
1500
|
+
if (paths.length === 1 && keys.length === 1)
|
|
1501
|
+
return new Map([[0, keys[0]]]);
|
|
1502
|
+
throw new Error('Provide one ordered --repository-key per selected repository without a hosted remote.');
|
|
1503
|
+
}
|
|
1504
|
+
keys.forEach((key, index) => assignments.set(index, key));
|
|
1505
|
+
return assignments;
|
|
1506
|
+
}
|
|
1507
|
+
async function repositoriesConnect(flags, positional, repeatedRepos, repeatedKeys, repeatedProviders) {
|
|
1508
|
+
const selected = [...positional, ...repeatedRepos.filter((value) => typeof value === 'string')];
|
|
1509
|
+
if (selected.length === 0)
|
|
1510
|
+
selected.push(String(flags.get('repo') || '.'));
|
|
1511
|
+
const canonical = await Promise.all(selected.map((path) => realpath(path)));
|
|
1512
|
+
const unique = [...new Set(canonical)];
|
|
1513
|
+
const keys = repositoryKeyAssignments(unique, repeatedKeys.length ? repeatedKeys : (typeof flags.get('repository-key') === 'string' ? [String(flags.get('repository-key'))] : []));
|
|
1514
|
+
const providerIds = parseSelectedProviderIds(repeatedProviders.length ? repeatedProviders : (typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []));
|
|
1515
|
+
const results = [];
|
|
1516
|
+
for (let index = 0; index < unique.length; index += 1) {
|
|
1517
|
+
const connectFlags = new Map(flags);
|
|
1518
|
+
connectFlags.set('workspace', unique[index]);
|
|
1519
|
+
const repositoryKey = keys.get(index);
|
|
1520
|
+
if (repositoryKey)
|
|
1521
|
+
connectFlags.set('repository-key', repositoryKey);
|
|
1522
|
+
else
|
|
1523
|
+
connectFlags.delete('repository-key');
|
|
1524
|
+
if (providerIds)
|
|
1525
|
+
connectFlags.set('providers', providerIds.join(','));
|
|
1526
|
+
const result = await onboard(connectFlags);
|
|
1527
|
+
results.push(result);
|
|
1528
|
+
if (result?.stage === 'approve_device')
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
return { ok: true, requested: unique.length, connected: results.length, repositories: results };
|
|
1532
|
+
}
|
|
1533
|
+
export function parseSelectedProviderIds(values) {
|
|
1534
|
+
const selected = [...new Set(values
|
|
1535
|
+
.filter((value) => typeof value === 'string')
|
|
1536
|
+
.flatMap((value) => value.split(','))
|
|
1537
|
+
.map((value) => value.trim())
|
|
1538
|
+
.filter(Boolean))];
|
|
1539
|
+
if (selected.length === 0)
|
|
1540
|
+
return null;
|
|
1541
|
+
if (selected.some((value) => !['codex', 'claude', 'agy'].includes(value))) {
|
|
1542
|
+
throw new Error('Repository providers must be codex, claude, or agy. Repeat --provider to select more than one.');
|
|
1543
|
+
}
|
|
1544
|
+
return selected;
|
|
1545
|
+
}
|
|
1546
|
+
function selectedProviderAdapters(providerIds) {
|
|
1547
|
+
return providerIds
|
|
1548
|
+
? providerAdapters.filter((adapter) => providerIds.includes(adapter.providerId))
|
|
1549
|
+
: providerAdapters;
|
|
1550
|
+
}
|
|
1551
|
+
async function repositoriesList(flags) {
|
|
1552
|
+
const verbose = flags.has('verbose') || flags.has('diagnostic');
|
|
1553
|
+
return {
|
|
1554
|
+
ok: true,
|
|
1555
|
+
repositories: (await registry()).map((item) => ({
|
|
1556
|
+
workspaceId: item.workspaceId,
|
|
1557
|
+
name: item.name,
|
|
1558
|
+
repositoryAgentId: item.repositoryAgentId || null,
|
|
1559
|
+
repositoryAgentKey: item.repositoryAgentKey || null,
|
|
1560
|
+
controlBranch: item.controlBranch || null,
|
|
1561
|
+
connected: Boolean(item.repositoryAgentId && item.controlBranch),
|
|
1562
|
+
...(verbose ? { localPath: item.path, sourceFingerprint: item.repositoryRemoteHash } : {}),
|
|
1563
|
+
})),
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async function bindRepositoryAgent(fabric, item) {
|
|
1567
|
+
if (item.repositoryAgentId && item.repositoryBindingId && item.repositoryAgentKey && item.controlBranch)
|
|
1568
|
+
return item;
|
|
1569
|
+
const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
|
|
1570
|
+
const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
|
|
1571
|
+
? item.repositoryRemoteHash
|
|
1572
|
+
: remote ? sourceRepositoryFingerprint(remote).fingerprint : item.repositoryRemoteHash;
|
|
1573
|
+
if (!currentIdentity) {
|
|
1574
|
+
throw new Error('Repository identity is missing. Re-run dharma workspace add with --repository-key.');
|
|
1575
|
+
}
|
|
1576
|
+
const response = await fabric.connectRepositoryAgent({
|
|
1577
|
+
sourceFingerprint: currentIdentity,
|
|
1578
|
+
displayName: item.name,
|
|
1579
|
+
defaultSourceRef: item.defaultBranch,
|
|
1580
|
+
workspaceId: item.workspaceId,
|
|
1581
|
+
legacySourceFingerprint: item.repositoryIdentityVersion === 'normalized-v1' ? null : item.repositoryRemoteHash,
|
|
1582
|
+
});
|
|
1583
|
+
const repositoryAgent = response.repositoryAgent && typeof response.repositoryAgent === 'object'
|
|
1584
|
+
? response.repositoryAgent
|
|
1585
|
+
: null;
|
|
1586
|
+
const branch = repositoryAgent?.branch && typeof repositoryAgent.branch === 'object'
|
|
1587
|
+
? repositoryAgent.branch
|
|
1588
|
+
: null;
|
|
1589
|
+
const updated = {
|
|
1590
|
+
...item,
|
|
1591
|
+
repositoryRemoteHash: currentIdentity,
|
|
1592
|
+
repositoryIdentityVersion: 'normalized-v1',
|
|
1593
|
+
repositoryAgentId: String(repositoryAgent?.organization_agent_id || ''),
|
|
1594
|
+
repositoryBindingId: String(repositoryAgent?.id || ''),
|
|
1595
|
+
repositoryAgentKey: String(repositoryAgent?.agent_key || ''),
|
|
1596
|
+
controlBranch: String(repositoryAgent?.control_branch || branch?.branch || ''),
|
|
1597
|
+
};
|
|
1598
|
+
if (!/^[0-9a-f-]{36}$/i.test(updated.repositoryAgentId || '')
|
|
1599
|
+
|| !/^[0-9a-f-]{36}$/i.test(updated.repositoryBindingId || '')
|
|
1600
|
+
|| !/^repo:[a-f0-9]{24}$/.test(updated.repositoryAgentKey || '')
|
|
1601
|
+
|| !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
|
|
1602
|
+
throw new Error('Dharma HQ returned an invalid repository-agent binding.');
|
|
1603
|
+
}
|
|
1604
|
+
await saveWorkspaceRecord(updated);
|
|
1605
|
+
return updated;
|
|
1229
1606
|
}
|
|
1230
1607
|
async function workspaceSync(flags, positional) {
|
|
1231
1608
|
const workspaceId = positional[0] || required(flags, 'workspace-id');
|
|
@@ -1239,13 +1616,16 @@ async function workspaceSync(flags, positional) {
|
|
|
1239
1616
|
next: `dharma workspace sync ${workspaceId} --policy-revision ${policyRevision} --apply`,
|
|
1240
1617
|
};
|
|
1241
1618
|
}
|
|
1242
|
-
|
|
1619
|
+
const fabric = await client();
|
|
1620
|
+
const providerIds = parseSelectedProviderIds(typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
|
|
1621
|
+
return syncWorkspacePolicy(fabric, await bindRepositoryAgent(fabric, item), policyRevision, true, providerIds);
|
|
1243
1622
|
}
|
|
1244
|
-
async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true) {
|
|
1245
|
-
const providers = await Promise.all(
|
|
1623
|
+
async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true, providerIds = null) {
|
|
1624
|
+
const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
|
|
1246
1625
|
const response = await fabric.registerWorkspace({
|
|
1247
1626
|
workspaceId: item.workspaceId, name: item.name, routeHash: item.routeHash,
|
|
1248
1627
|
repositoryRemoteHash: item.repositoryRemoteHash, defaultBranch: item.defaultBranch,
|
|
1628
|
+
repositoryAgentId: item.repositoryAgentId,
|
|
1249
1629
|
policyRevision, providers,
|
|
1250
1630
|
});
|
|
1251
1631
|
const generated = await materializeWorkspacePolicy({
|
|
@@ -1334,46 +1714,68 @@ Use the installed \`dharma\` CLI for organization-scoped agent work. Never print
|
|
|
1334
1714
|
6. Use only signed tasks whose organization, device, workspace, authority, budget, and skill pin pass local validation.
|
|
1335
1715
|
7. For cross-agent help, ask the control plane for a structured, task-bound handoff. Do not open arbitrary chat, shell, file, merge, deploy, or secret authority.
|
|
1336
1716
|
8. Install only signed skill bundles. Preserve the active bundle receipt and automatic rollback result.
|
|
1717
|
+
9. Use the organization MCP connection for role-scoped status, experiments, failures, remediations, rollouts, and profile administration. Reads may run directly. Paid evals, task dispatch, GitHub writes, approvals, rollout, rollback, and profile mutation require the granted scope and explicit confirmation.
|
|
1337
1718
|
|
|
1338
|
-
The organization contract and API origin are recorded in \`.dharma/agent-fabric.json\`. API calls must use the published SDK and a scoped organization token supplied at runtime, never a credential committed to this repository.
|
|
1719
|
+
The organization contract and API origin are recorded in \`.dharma/agent-fabric.json\`; the logical repository-agent identity is recorded in \`.dharma/repository-agent.json\`. API calls must use the published SDK and a scoped organization token supplied at runtime, never a credential committed to this repository.
|
|
1339
1720
|
`;
|
|
1340
1721
|
const reference = `# Organization connection
|
|
1341
1722
|
|
|
1342
1723
|
- HQ API: ${input.hqUrl}
|
|
1343
1724
|
- Organization: ${input.organizationId}
|
|
1344
1725
|
- Workspace: ${input.workspaceId}
|
|
1726
|
+
- Repository agent: ${input.repositoryAgentKey || 'pending'}
|
|
1727
|
+
- Permanent control branch: ${input.controlBranch || 'pending'}
|
|
1345
1728
|
- Policy revision: ${input.policyRevision}
|
|
1346
1729
|
- OpenAPI: ${input.hqUrl}/api/v1/agent-fabric/openapi.json
|
|
1730
|
+
- Organization instructions: ${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions
|
|
1347
1731
|
|
|
1348
1732
|
The CLI enrolls this device through browser-confirmed Clerk organization consent. Local provider credentials remain on this device. Managed and cloud BYOK execution are brokered by Dharma HQ and expose neither private runtime URLs nor cloud credentials.
|
|
1349
1733
|
`;
|
|
1350
1734
|
const connection = {
|
|
1351
|
-
schema: 'dharma.repository-connection/
|
|
1735
|
+
schema: 'dharma.repository-connection/v2',
|
|
1352
1736
|
hqUrl: input.hqUrl,
|
|
1353
1737
|
organizationId: input.organizationId,
|
|
1354
1738
|
workspaceId: input.workspaceId,
|
|
1739
|
+
repositoryAgentId: input.repositoryAgentId || null,
|
|
1740
|
+
repositoryAgentKey: input.repositoryAgentKey || null,
|
|
1741
|
+
controlBranch: input.controlBranch || null,
|
|
1355
1742
|
policyRevision: input.policyRevision,
|
|
1356
1743
|
openapiUrl: `${input.hqUrl}/api/v1/agent-fabric/openapi.json`,
|
|
1744
|
+
instructionsUrl: `${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions`,
|
|
1745
|
+
};
|
|
1746
|
+
const repositoryAgent = {
|
|
1747
|
+
schema: 'dharma.repository-agent/v1',
|
|
1748
|
+
organizationId: input.organizationId,
|
|
1749
|
+
organizationAgentId: input.repositoryAgentId || null,
|
|
1750
|
+
agentKey: input.repositoryAgentKey || null,
|
|
1751
|
+
controlBranch: input.controlBranch || null,
|
|
1752
|
+
workspaceId: input.workspaceId,
|
|
1357
1753
|
};
|
|
1358
1754
|
await writeFile(resolve(skillRoot, 'SKILL.md'), skill, { mode: 0o600 });
|
|
1359
1755
|
await writeFile(resolve(skillRoot, 'references', 'organization.md'), reference, { mode: 0o600 });
|
|
1360
1756
|
await writeFile(marker, `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`, { mode: 0o600 });
|
|
1361
1757
|
await writeFile(resolve(input.workspace, '.dharma', 'agent-fabric.json'), `${JSON.stringify(connection, null, 2)}\n`, { mode: 0o600 });
|
|
1758
|
+
await writeFile(resolve(input.workspace, '.dharma', 'repository-agent.json'), `${JSON.stringify(repositoryAgent, null, 2)}\n`, { mode: 0o600 });
|
|
1362
1759
|
return {
|
|
1363
1760
|
skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
|
|
1364
1761
|
connectionPath: '.dharma/agent-fabric.json',
|
|
1762
|
+
repositoryAgentPath: '.dharma/repository-agent.json',
|
|
1365
1763
|
};
|
|
1366
1764
|
}
|
|
1367
1765
|
async function onboard(flags) {
|
|
1368
1766
|
const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
|
|
1369
1767
|
const organizationId = required(flags, 'organization-id');
|
|
1370
1768
|
const policyRevision = required(flags, 'policy-revision');
|
|
1371
|
-
const requestedHqUrl = normalizeHqUrl(
|
|
1769
|
+
const requestedHqUrl = normalizeHqUrl(portalUrl(flags));
|
|
1372
1770
|
let config = await readDeviceConfig();
|
|
1373
1771
|
if (!config) {
|
|
1374
1772
|
const loginFlags = new Map(flags);
|
|
1375
1773
|
loginFlags.set('hq-url', requestedHqUrl);
|
|
1376
1774
|
loginFlags.set('organization-id', organizationId);
|
|
1775
|
+
if (flags.has('non-interactive')) {
|
|
1776
|
+
loginFlags.set('no-browser', true);
|
|
1777
|
+
loginFlags.set('no-wait', true);
|
|
1778
|
+
}
|
|
1377
1779
|
const enrollment = await login(loginFlags);
|
|
1378
1780
|
if (enrollment.status !== 'approved') {
|
|
1379
1781
|
return {
|
|
@@ -1390,24 +1792,30 @@ async function onboard(flags) {
|
|
|
1390
1792
|
if (config.organizationId !== organizationId) {
|
|
1391
1793
|
throw new Error('This DHARMA_HOME is enrolled to a different organization. Use a separate DHARMA_HOME for each organization.');
|
|
1392
1794
|
}
|
|
1393
|
-
if (flags.has('hq-url') && config.hqUrl !== requestedHqUrl) {
|
|
1394
|
-
throw new Error('This device is enrolled to a different Dharma
|
|
1795
|
+
if ((flags.has('portal-url') || flags.has('hq-url')) && config.hqUrl !== requestedHqUrl) {
|
|
1796
|
+
throw new Error('This device is enrolled to a different Dharma portal origin. Use a separate DHARMA_HOME for each portal origin.');
|
|
1395
1797
|
}
|
|
1396
1798
|
const hqUrl = config.hqUrl;
|
|
1799
|
+
const providerIds = parseSelectedProviderIds(typeof flags.get('providers') === 'string'
|
|
1800
|
+
? [String(flags.get('providers'))]
|
|
1801
|
+
: typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
|
|
1397
1802
|
let registered = (await registry()).find((item) => item.path === workspace);
|
|
1398
|
-
if (!registered) {
|
|
1399
|
-
|
|
1803
|
+
if (!registered || (!registered.repositoryRemoteHash && typeof flags.get('repository-key') === 'string')) {
|
|
1804
|
+
const addFlags = new Map([
|
|
1400
1805
|
['organization-id', organizationId],
|
|
1401
1806
|
['path', workspace],
|
|
1402
1807
|
['name', String(flags.get('name') || basename(workspace))],
|
|
1403
|
-
])
|
|
1808
|
+
]);
|
|
1809
|
+
if (typeof flags.get('repository-key') === 'string')
|
|
1810
|
+
addFlags.set('repository-key', String(flags.get('repository-key')));
|
|
1811
|
+
await workspaceAdd(addFlags, [workspace]);
|
|
1404
1812
|
registered = (await registry()).find((item) => item.path === workspace);
|
|
1405
1813
|
}
|
|
1406
1814
|
if (!registered)
|
|
1407
1815
|
throw new Error('Workspace registration failed.');
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1816
|
+
const fabric = await client();
|
|
1817
|
+
registered = await bindRepositoryAgent(fabric, registered);
|
|
1818
|
+
const synced = await syncWorkspacePolicy(fabric, registered, policyRevision, true, providerIds);
|
|
1411
1819
|
const localPolicy = synced.localPolicy;
|
|
1412
1820
|
const authoritativeRevision = String(localPolicy?.revision || policyRevision);
|
|
1413
1821
|
const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
|
|
@@ -1416,9 +1824,12 @@ async function onboard(flags) {
|
|
|
1416
1824
|
hqUrl,
|
|
1417
1825
|
organizationId,
|
|
1418
1826
|
workspaceId: registered.workspaceId,
|
|
1827
|
+
repositoryAgentId: registered.repositoryAgentId,
|
|
1828
|
+
repositoryAgentKey: registered.repositoryAgentKey,
|
|
1829
|
+
controlBranch: registered.controlBranch,
|
|
1419
1830
|
policyRevision: authoritativeRevision,
|
|
1420
1831
|
});
|
|
1421
|
-
const providers = await Promise.all(
|
|
1832
|
+
const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
|
|
1422
1833
|
const nativeSkillResult = await installAvailableNativeAgentFabricBootstraps({
|
|
1423
1834
|
providers,
|
|
1424
1835
|
workspace,
|
|
@@ -1426,11 +1837,18 @@ async function onboard(flags) {
|
|
|
1426
1837
|
organizationId,
|
|
1427
1838
|
hqUrl,
|
|
1428
1839
|
});
|
|
1840
|
+
const primaryProvider = providers[0]?.provider || 'codex';
|
|
1429
1841
|
return {
|
|
1430
1842
|
ok: true,
|
|
1431
1843
|
stage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
|
|
1432
1844
|
organizationId,
|
|
1433
1845
|
workspaceId: registered.workspaceId,
|
|
1846
|
+
repositoryAgent: {
|
|
1847
|
+
id: registered.repositoryAgentId,
|
|
1848
|
+
key: registered.repositoryAgentKey,
|
|
1849
|
+
bindingId: registered.repositoryBindingId,
|
|
1850
|
+
controlBranch: registered.controlBranch,
|
|
1851
|
+
},
|
|
1434
1852
|
deviceId: config.deviceId,
|
|
1435
1853
|
providers,
|
|
1436
1854
|
organizationPolicy: {
|
|
@@ -1445,10 +1863,10 @@ async function onboard(flags) {
|
|
|
1445
1863
|
nativeSkillFailures: nativeSkillResult.failures,
|
|
1446
1864
|
workspaceSync: synced,
|
|
1447
1865
|
next: {
|
|
1448
|
-
preview:
|
|
1449
|
-
sync:
|
|
1866
|
+
preview: `dharma evidence preview --workspace . --provider ${primaryProvider}`,
|
|
1867
|
+
sync: `dharma evidence capture-batch --workspace . --provider ${primaryProvider} --policy .dharma/approved-policy.json --maximum-sessions 20 --sync`,
|
|
1450
1868
|
relay: 'dharma relay start --policy .dharma/approved-policy.json',
|
|
1451
|
-
verifySkill:
|
|
1869
|
+
verifySkill: `dharma skills verify --provider ${primaryProvider} --workspace .`,
|
|
1452
1870
|
},
|
|
1453
1871
|
};
|
|
1454
1872
|
}
|
|
@@ -1683,7 +2101,7 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
|
1683
2101
|
if (task.target.deviceId !== config.deviceId)
|
|
1684
2102
|
throw new Error('Task target does not match this enrolled device.');
|
|
1685
2103
|
const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
|
|
1686
|
-
const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider));
|
|
2104
|
+
const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider), task.workspaceId);
|
|
1687
2105
|
try {
|
|
1688
2106
|
assertTaskSkillPin(task.skillBundle, activeBundleId);
|
|
1689
2107
|
}
|
|
@@ -1816,6 +2234,13 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
1816
2234
|
const nativeMarkerPath = resolve(nativeRoot, 'dharma-agent-fabric', '.dharma-agent-fabric-bootstrap.json');
|
|
1817
2235
|
const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
|
|
1818
2236
|
const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
|
|
2237
|
+
let workspaceId;
|
|
2238
|
+
try {
|
|
2239
|
+
const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
|
|
2240
|
+
if (typeof connection.workspaceId === 'string')
|
|
2241
|
+
workspaceId = connection.workspaceId;
|
|
2242
|
+
}
|
|
2243
|
+
catch { }
|
|
1819
2244
|
return {
|
|
1820
2245
|
provider: input.provider,
|
|
1821
2246
|
ready: repositoryInstalled && nativeInstalled,
|
|
@@ -1824,7 +2249,8 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
1824
2249
|
repositorySkillPath,
|
|
1825
2250
|
connectionPath,
|
|
1826
2251
|
nativeSkillPath,
|
|
1827
|
-
|
|
2252
|
+
workspaceId: workspaceId || null,
|
|
2253
|
+
activeBundleId: workspaceId ? await getActiveSkillBundleId(nativeRoot, workspaceId) : null,
|
|
1828
2254
|
activation: 'next_session',
|
|
1829
2255
|
nextAction: repositoryInstalled && nativeInstalled
|
|
1830
2256
|
? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
|
|
@@ -1908,7 +2334,11 @@ async function skillSync(flags) {
|
|
|
1908
2334
|
const policy = await loadOrganizationPolicy(required(flags, 'policy'));
|
|
1909
2335
|
const destination = nativeSkillDirectory(provider);
|
|
1910
2336
|
const fabric = await client();
|
|
1911
|
-
const response = await fabric.pollSkill({
|
|
2337
|
+
const response = await fabric.pollSkill({
|
|
2338
|
+
workspaceId,
|
|
2339
|
+
provider,
|
|
2340
|
+
installedBundleId: await getActiveSkillBundleId(destination, workspaceId),
|
|
2341
|
+
});
|
|
1912
2342
|
const rollout = response.rollout;
|
|
1913
2343
|
if (!rollout)
|
|
1914
2344
|
return { ok: true, rollout: null, changed: false };
|
|
@@ -2023,7 +2453,7 @@ async function relayStart(flags) {
|
|
|
2023
2453
|
return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
|
|
2024
2454
|
}
|
|
2025
2455
|
export async function run(argv) {
|
|
2026
|
-
const { positional, flags } =
|
|
2456
|
+
const { positional, flags, repeated } = parseCliOptions(argv);
|
|
2027
2457
|
const [command, subcommand] = positional;
|
|
2028
2458
|
if (flags.has('help') || command === 'help')
|
|
2029
2459
|
return USAGE;
|
|
@@ -2035,6 +2465,22 @@ export async function run(argv) {
|
|
|
2035
2465
|
return login(flags);
|
|
2036
2466
|
if (command === 'providers' && subcommand === 'list')
|
|
2037
2467
|
return { providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())) };
|
|
2468
|
+
if (command === 'repositories' && subcommand === 'discover') {
|
|
2469
|
+
return repositoriesDiscover(flags, positional.slice(2), repeated.get('root') || []);
|
|
2470
|
+
}
|
|
2471
|
+
if (command === 'repositories' && subcommand === 'connect') {
|
|
2472
|
+
return repositoriesConnect(flags, positional.slice(2), repeated.get('repo') || [], repeated.get('repository-key') || [], repeated.get('provider') || []);
|
|
2473
|
+
}
|
|
2474
|
+
if (command === 'repositories' && subcommand === 'list')
|
|
2475
|
+
return repositoriesList(flags);
|
|
2476
|
+
if (command === 'repositories' && subcommand === 'status') {
|
|
2477
|
+
const listed = await repositoriesList(flags);
|
|
2478
|
+
return {
|
|
2479
|
+
...listed,
|
|
2480
|
+
relay: await relayProcessState(),
|
|
2481
|
+
providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())),
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2038
2484
|
if (command === 'workspace' && subcommand === 'add')
|
|
2039
2485
|
return workspaceAdd(flags, positional.slice(2));
|
|
2040
2486
|
if (command === 'workspace' && subcommand === 'sync')
|
|
@@ -2066,12 +2512,18 @@ export async function run(argv) {
|
|
|
2066
2512
|
return status;
|
|
2067
2513
|
}
|
|
2068
2514
|
}
|
|
2515
|
+
if ([
|
|
2516
|
+
'organization', 'agents', 'experiments', 'failures', 'remediations', 'handoffs', 'usage',
|
|
2517
|
+
].includes(String(command)) || (['tasks', 'skills'].includes(String(command))
|
|
2518
|
+
&& ['list', 'dispatch', 'release', 'rollout', 'rollback'].includes(String(subcommand)))) {
|
|
2519
|
+
const result = await runOrganizationCommand(command, subcommand, flags);
|
|
2520
|
+
if (result)
|
|
2521
|
+
return result;
|
|
2522
|
+
}
|
|
2069
2523
|
if (command === 'tasks' && subcommand === 'run-once')
|
|
2070
2524
|
return runOneTask(flags);
|
|
2071
2525
|
if (command === 'relay' && subcommand === 'start')
|
|
2072
2526
|
return relayStart(flags);
|
|
2073
|
-
if (command === 'tasks' && subcommand === 'list')
|
|
2074
|
-
return { tasks: [], coverage: 'server_poll_requires_relay' };
|
|
2075
2527
|
if (command === 'skills' && subcommand === 'sync')
|
|
2076
2528
|
return skillSync(flags);
|
|
2077
2529
|
if (command === 'skills' && subcommand === 'status') {
|
|
@@ -2079,7 +2531,13 @@ export async function run(argv) {
|
|
|
2079
2531
|
if (!['codex', 'claude', 'agy'].includes(providerValue))
|
|
2080
2532
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2081
2533
|
const root = nativeSkillDirectory(providerValue);
|
|
2082
|
-
|
|
2534
|
+
const workspaceId = required(flags, 'workspace-id');
|
|
2535
|
+
return {
|
|
2536
|
+
provider: providerValue,
|
|
2537
|
+
workspaceId,
|
|
2538
|
+
activeBundleId: await getActiveSkillBundleId(root, workspaceId),
|
|
2539
|
+
nativeSkillDirectory: root,
|
|
2540
|
+
};
|
|
2083
2541
|
}
|
|
2084
2542
|
if (command === 'skills' && subcommand === 'verify') {
|
|
2085
2543
|
const providerValue = required(flags, 'provider');
|