@dharma-ai-labs/agent-fabric 0.1.13 → 0.1.19
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 +533 -45
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +210 -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 +32 -12
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.19';
|
|
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
|
}
|
|
@@ -97,6 +106,33 @@ function assertEvidenceLedger(ledger, day) {
|
|
|
97
106
|
throw new Error('Evidence upload ledger is invalid.');
|
|
98
107
|
}
|
|
99
108
|
}
|
|
109
|
+
function evidenceLedgerForPolicyActivation(value, day) {
|
|
110
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
111
|
+
throw new Error('Evidence upload ledger is invalid.');
|
|
112
|
+
}
|
|
113
|
+
const ledger = value;
|
|
114
|
+
if (typeof ledger.day !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(ledger.day)) {
|
|
115
|
+
throw new Error('Evidence upload ledger is invalid.');
|
|
116
|
+
}
|
|
117
|
+
if (ledger.day !== day) {
|
|
118
|
+
if (ledger.day > day)
|
|
119
|
+
throw new Error('Evidence upload ledger is invalid.');
|
|
120
|
+
return newEvidenceUploadLedger(day);
|
|
121
|
+
}
|
|
122
|
+
if (ledger.schema === 'dharma.evidence-upload-ledger/v2') {
|
|
123
|
+
assertEvidenceLedger(ledger, day);
|
|
124
|
+
return ledger;
|
|
125
|
+
}
|
|
126
|
+
const legacyEmpty = ledger.schema === undefined
|
|
127
|
+
&& ledger.totalBytes === 0
|
|
128
|
+
&& Array.isArray(ledger.capsuleHashes)
|
|
129
|
+
&& ledger.capsuleHashes.length === 0
|
|
130
|
+
&& (ledger.capsuleBytes === undefined
|
|
131
|
+
|| (typeof ledger.capsuleBytes === 'object' && Object.keys(ledger.capsuleBytes).length === 0));
|
|
132
|
+
if (!legacyEmpty)
|
|
133
|
+
throw new Error('Evidence upload ledger is invalid.');
|
|
134
|
+
return newEvidenceUploadLedger(day);
|
|
135
|
+
}
|
|
100
136
|
async function pathExists(path) {
|
|
101
137
|
try {
|
|
102
138
|
await access(path);
|
|
@@ -690,8 +726,11 @@ async function applyWorkspaceAuthorizationAtomically(input) {
|
|
|
690
726
|
const contentAuthorized = input.policy.evidence.automaticDisclosure?.mode === 'customer_authorized_content';
|
|
691
727
|
const ledgerExists = await pathExists(evidenceUploadLedgerPath());
|
|
692
728
|
if (contentAuthorized && ledgerExists) {
|
|
693
|
-
const
|
|
694
|
-
|
|
729
|
+
const current = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
|
|
730
|
+
const ledger = evidenceLedgerForPolicyActivation(current, new Date().toISOString().slice(0, 10));
|
|
731
|
+
if (JSON.stringify(current) !== JSON.stringify(ledger)) {
|
|
732
|
+
await writeJsonAtomic(evidenceUploadLedgerPath(), ledger);
|
|
733
|
+
}
|
|
695
734
|
}
|
|
696
735
|
if (contentAuthorized && !ledgerExists) {
|
|
697
736
|
const ledger = newEvidenceUploadLedger(new Date().toISOString().slice(0, 10));
|
|
@@ -808,6 +847,62 @@ function deterministicUuid(value) {
|
|
|
808
847
|
const hex = bytes.toString('hex');
|
|
809
848
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
810
849
|
}
|
|
850
|
+
export function normalizeGitRemoteIdentity(value) {
|
|
851
|
+
const raw = value.trim();
|
|
852
|
+
if (!raw || /[\u0000-\u001f\u007f]/.test(raw))
|
|
853
|
+
throw new Error('Git remote is empty or invalid.');
|
|
854
|
+
let host;
|
|
855
|
+
let repositoryPath;
|
|
856
|
+
const scp = raw.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
|
|
857
|
+
if (!raw.includes('://') && scp && !/^[A-Za-z]:[\\/]/.test(raw)) {
|
|
858
|
+
host = scp[1].toLowerCase();
|
|
859
|
+
repositoryPath = scp[2];
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
let remote;
|
|
863
|
+
try {
|
|
864
|
+
remote = new URL(raw);
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
throw new Error('Git remote must be a hosted URL or use --repository-key.');
|
|
868
|
+
}
|
|
869
|
+
if (!['https:', 'http:', 'ssh:', 'git:'].includes(remote.protocol) || !remote.hostname) {
|
|
870
|
+
throw new Error('Local and file Git remotes require an explicit stable --repository-key.');
|
|
871
|
+
}
|
|
872
|
+
host = remote.hostname.toLowerCase();
|
|
873
|
+
const port = remote.port && !((remote.protocol === 'https:' && remote.port === '443') || (remote.protocol === 'http:' && remote.port === '80'))
|
|
874
|
+
? `:${remote.port}` : '';
|
|
875
|
+
host += port;
|
|
876
|
+
repositoryPath = remote.pathname;
|
|
877
|
+
}
|
|
878
|
+
repositoryPath = repositoryPath.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
|
|
879
|
+
if (!repositoryPath || repositoryPath.includes('..') || /\s/.test(repositoryPath)) {
|
|
880
|
+
throw new Error('Git remote repository path is invalid.');
|
|
881
|
+
}
|
|
882
|
+
if (host === 'github.com' || host === 'gitlab.com')
|
|
883
|
+
repositoryPath = repositoryPath.toLowerCase();
|
|
884
|
+
return `${host}/${repositoryPath}`;
|
|
885
|
+
}
|
|
886
|
+
export function sourceRepositoryFingerprint(remote, explicitKey) {
|
|
887
|
+
const key = explicitKey?.trim();
|
|
888
|
+
let identity;
|
|
889
|
+
let source;
|
|
890
|
+
if (key) {
|
|
891
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(key) || key.includes('..')) {
|
|
892
|
+
throw new Error('--repository-key must be a stable 1-160 character identifier without secrets or path traversal.');
|
|
893
|
+
}
|
|
894
|
+
identity = `key:${key}`;
|
|
895
|
+
source = 'explicit_key';
|
|
896
|
+
}
|
|
897
|
+
else if (remote) {
|
|
898
|
+
identity = `remote:${normalizeGitRemoteIdentity(remote)}`;
|
|
899
|
+
source = 'remote';
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
throw new Error('Repository has no hosted Git remote. Supply a stable --repository-key for this repository.');
|
|
903
|
+
}
|
|
904
|
+
return { fingerprint: `sha256:${createHash('sha256').update(identity).digest('hex')}`, source };
|
|
905
|
+
}
|
|
811
906
|
function responseTextFromEvent(value) {
|
|
812
907
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
813
908
|
return null;
|
|
@@ -901,6 +996,15 @@ async function registry() {
|
|
|
901
996
|
return [];
|
|
902
997
|
}
|
|
903
998
|
}
|
|
999
|
+
async function saveRegistry(items) {
|
|
1000
|
+
await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
|
|
1001
|
+
await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
|
|
1002
|
+
}
|
|
1003
|
+
async function saveWorkspaceRecord(entry) {
|
|
1004
|
+
const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
|
|
1005
|
+
items.push(entry);
|
|
1006
|
+
await saveRegistry(items);
|
|
1007
|
+
}
|
|
904
1008
|
async function gitValue(workspace, argv) {
|
|
905
1009
|
try {
|
|
906
1010
|
return (await execFileAsync('git', ['-C', workspace, ...argv], { timeout: 10_000 })).stdout.trim() || null;
|
|
@@ -914,6 +1018,121 @@ async function client() {
|
|
|
914
1018
|
await instance.openSession(VERSION);
|
|
915
1019
|
return instance;
|
|
916
1020
|
}
|
|
1021
|
+
async function organizationApi(flags) {
|
|
1022
|
+
let enrolled = null;
|
|
1023
|
+
try {
|
|
1024
|
+
enrolled = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
1025
|
+
}
|
|
1026
|
+
catch { }
|
|
1027
|
+
const organizationId = String(flags.get('organization-id') || enrolled?.organizationId || '').trim();
|
|
1028
|
+
if (!organizationId)
|
|
1029
|
+
throw new Error('Organization command requires --organization-id or an enrolled device.');
|
|
1030
|
+
const token = String(process.env.DHARMA_ORG_API_TOKEN || '').trim();
|
|
1031
|
+
if (!token)
|
|
1032
|
+
throw new Error('Organization command requires DHARMA_ORG_API_TOKEN. Tokens are not accepted on the command line.');
|
|
1033
|
+
return new AgentFabricApiClient({
|
|
1034
|
+
organizationId,
|
|
1035
|
+
token,
|
|
1036
|
+
baseUrl: portalUrl(flags, enrolled?.hqUrl || 'https://www.dharma-ai.io'),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async function commandJsonBody(flags) {
|
|
1040
|
+
const inline = flags.get('json-body');
|
|
1041
|
+
const path = flags.get('body-file');
|
|
1042
|
+
if (typeof inline === 'string' && typeof path === 'string') {
|
|
1043
|
+
throw new Error('Use either --json-body or --body-file, not both.');
|
|
1044
|
+
}
|
|
1045
|
+
const serialized = typeof inline === 'string'
|
|
1046
|
+
? inline
|
|
1047
|
+
: typeof path === 'string'
|
|
1048
|
+
? await readFile(resolve(path), 'utf8')
|
|
1049
|
+
: '{}';
|
|
1050
|
+
const parsed = JSON.parse(serialized);
|
|
1051
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1052
|
+
throw new Error('Command body must be a JSON object.');
|
|
1053
|
+
}
|
|
1054
|
+
return parsed;
|
|
1055
|
+
}
|
|
1056
|
+
export function requireExplicitConfirmation(flags, action) {
|
|
1057
|
+
if (flags.get('confirm') !== true) {
|
|
1058
|
+
throw new Error(`${action} requires --confirm after reviewing organization scope, cost, and authority.`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async function runOrganizationCommand(command, subcommand, flags) {
|
|
1062
|
+
const api = await organizationApi(flags);
|
|
1063
|
+
if (command === 'organization' && subcommand === 'status')
|
|
1064
|
+
return api.instructions();
|
|
1065
|
+
if (command === 'agents' && subcommand === 'list')
|
|
1066
|
+
return api.listAgents();
|
|
1067
|
+
if (command === 'agents' && subcommand === 'bind-runtime') {
|
|
1068
|
+
requireExplicitConfirmation(flags, 'Binding a managed or cloud BYOK runtime endpoint');
|
|
1069
|
+
const agentId = required(flags, 'agent-id');
|
|
1070
|
+
const body = await commandJsonBody(flags);
|
|
1071
|
+
const endpointKind = String(body.endpointKind || '');
|
|
1072
|
+
if (!['managed_runtime', 'cloud_byok'].includes(endpointKind)) {
|
|
1073
|
+
throw new Error('Runtime endpoint kind must be managed_runtime or cloud_byok.');
|
|
1074
|
+
}
|
|
1075
|
+
return api.bindRuntimeEndpoint(agentId, {
|
|
1076
|
+
endpointKind: endpointKind,
|
|
1077
|
+
managedAgentId: String(body.managedAgentId || ''),
|
|
1078
|
+
runtimeBindingId: String(body.runtimeBindingId || ''),
|
|
1079
|
+
...(body.priority === undefined ? {} : { priority: Number(body.priority) }),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (command === 'experiments' && subcommand === 'list')
|
|
1083
|
+
return api.listAnalysisWindows();
|
|
1084
|
+
if (command === 'experiments' && subcommand === 'run') {
|
|
1085
|
+
requireExplicitConfirmation(flags, 'Running an experiment');
|
|
1086
|
+
return api.requestAnalysis(await commandJsonBody(flags));
|
|
1087
|
+
}
|
|
1088
|
+
if (command === 'failures' && subcommand === 'list')
|
|
1089
|
+
return api.listFailures();
|
|
1090
|
+
if (command === 'remediations' && subcommand === 'list')
|
|
1091
|
+
return api.listRemediations();
|
|
1092
|
+
if (command === 'remediations' && subcommand === 'act') {
|
|
1093
|
+
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1094
|
+
const targetId = required(flags, 'target-id');
|
|
1095
|
+
const body = await commandJsonBody(flags);
|
|
1096
|
+
const action = String(flags.get('action') || body.action || '');
|
|
1097
|
+
if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1098
|
+
throw new Error('Remediation action must be run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1099
|
+
}
|
|
1100
|
+
return api.transitionRemediationTarget(targetId, {
|
|
1101
|
+
...body,
|
|
1102
|
+
action: action,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (command === 'skills' && subcommand === 'list')
|
|
1106
|
+
return api.listSkills();
|
|
1107
|
+
if (command === 'skills' && subcommand === 'release') {
|
|
1108
|
+
requireExplicitConfirmation(flags, 'Releasing a skill');
|
|
1109
|
+
return api.releaseSkill(await commandJsonBody(flags));
|
|
1110
|
+
}
|
|
1111
|
+
if (command === 'skills' && (subcommand === 'rollout' || subcommand === 'rollback')) {
|
|
1112
|
+
requireExplicitConfirmation(flags, `${subcommand === 'rollback' ? 'Rolling back' : 'Rolling out'} a skill`);
|
|
1113
|
+
const bundleId = required(flags, 'bundle-id');
|
|
1114
|
+
const body = await commandJsonBody(flags);
|
|
1115
|
+
return api.transitionSkillRollout(bundleId, {
|
|
1116
|
+
action: subcommand === 'rollback' ? 'rollback' : String(body.action || 'start'),
|
|
1117
|
+
...(body.canaryPercent === undefined ? {} : { canaryPercent: Number(body.canaryPercent) }),
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (command === 'tasks' && subcommand === 'list')
|
|
1121
|
+
return api.listTasks();
|
|
1122
|
+
if (command === 'tasks' && subcommand === 'dispatch') {
|
|
1123
|
+
requireExplicitConfirmation(flags, 'Dispatching a task');
|
|
1124
|
+
return api.dispatchTask(await commandJsonBody(flags));
|
|
1125
|
+
}
|
|
1126
|
+
if (command === 'handoffs' && subcommand === 'list')
|
|
1127
|
+
return api.listHandoffs();
|
|
1128
|
+
if (command === 'handoffs' && subcommand === 'dispatch') {
|
|
1129
|
+
requireExplicitConfirmation(flags, 'Dispatching an A2A handoff');
|
|
1130
|
+
return api.dispatchHandoff(await commandJsonBody(flags));
|
|
1131
|
+
}
|
|
1132
|
+
if (command === 'usage' && subcommand === 'list')
|
|
1133
|
+
return api.usage();
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
917
1136
|
async function openVerificationUri(url) {
|
|
918
1137
|
const attempts = process.platform === 'darwin'
|
|
919
1138
|
? [['open', [url]]]
|
|
@@ -939,7 +1158,7 @@ async function login(flags) {
|
|
|
939
1158
|
pending = JSON.parse(await readFile(pendingEnrollmentPath(), 'utf8'));
|
|
940
1159
|
}
|
|
941
1160
|
else {
|
|
942
|
-
const hqUrl = normalizeHqUrl(
|
|
1161
|
+
const hqUrl = normalizeHqUrl(portalUrl(flags));
|
|
943
1162
|
const organizationId = required(flags, 'organization-id');
|
|
944
1163
|
const name = String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`);
|
|
945
1164
|
const devicePlatform = await platform();
|
|
@@ -1213,19 +1432,207 @@ async function workspaceAdd(flags, positional) {
|
|
|
1213
1432
|
const organizationId = String(flags.get('organization-id') || device.organizationId);
|
|
1214
1433
|
if (organizationId !== device.organizationId)
|
|
1215
1434
|
throw new Error('Workspace organization must match the enrolled device.');
|
|
1216
|
-
await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
|
|
1217
1435
|
const workspaceId = deterministicUuid(`${organizationId}:${device.deviceId}:${path}`);
|
|
1218
1436
|
const remote = await gitValue(path, ['config', '--get', 'remote.origin.url']);
|
|
1437
|
+
const identity = sourceRepositoryFingerprint(remote, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
|
|
1219
1438
|
const entry = {
|
|
1220
1439
|
workspaceId, organizationId, name: String(flags.get('name') || basename(path)), path,
|
|
1221
1440
|
routeHash: `sha256:${createHash('sha256').update(path).digest('hex')}`,
|
|
1222
|
-
repositoryRemoteHash:
|
|
1441
|
+
repositoryRemoteHash: identity.fingerprint,
|
|
1442
|
+
repositoryIdentityVersion: 'normalized-v1',
|
|
1443
|
+
repositoryAgentId: null,
|
|
1444
|
+
repositoryBindingId: null,
|
|
1445
|
+
repositoryAgentKey: null,
|
|
1446
|
+
controlBranch: null,
|
|
1223
1447
|
defaultBranch: await gitValue(path, ['branch', '--show-current']), status: 'active',
|
|
1224
1448
|
};
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1449
|
+
await saveWorkspaceRecord(entry);
|
|
1450
|
+
return {
|
|
1451
|
+
ok: true,
|
|
1452
|
+
workspaceId,
|
|
1453
|
+
organizationId,
|
|
1454
|
+
sourceFingerprint: identity.fingerprint,
|
|
1455
|
+
repositoryIdentitySource: identity.source,
|
|
1456
|
+
pathStoredLocally: true,
|
|
1457
|
+
pathDisclosedToServer: false,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
async function discoverRepositoryAt(path) {
|
|
1461
|
+
const canonical = await realpath(path);
|
|
1462
|
+
const topLevel = await gitValue(canonical, ['rev-parse', '--show-toplevel']);
|
|
1463
|
+
if (!topLevel)
|
|
1464
|
+
return null;
|
|
1465
|
+
const workspace = await realpath(topLevel);
|
|
1466
|
+
const remote = await gitValue(workspace, ['config', '--get', 'remote.origin.url']);
|
|
1467
|
+
let normalizedRemote = null;
|
|
1468
|
+
let fingerprint = null;
|
|
1469
|
+
if (remote) {
|
|
1470
|
+
normalizedRemote = normalizeGitRemoteIdentity(remote);
|
|
1471
|
+
fingerprint = sourceRepositoryFingerprint(remote).fingerprint;
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
name: basename(workspace),
|
|
1475
|
+
path: workspace,
|
|
1476
|
+
remote: normalizedRemote,
|
|
1477
|
+
sourceFingerprint: fingerprint,
|
|
1478
|
+
defaultBranch: await gitValue(workspace, ['branch', '--show-current']),
|
|
1479
|
+
connectable: Boolean(fingerprint),
|
|
1480
|
+
requiredAction: fingerprint ? null : 'Supply --repository-key when connecting this repository.',
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async function repositoriesDiscover(flags, positional, roots) {
|
|
1484
|
+
const requested = [...positional, ...roots.filter((value) => typeof value === 'string')];
|
|
1485
|
+
if (requested.length === 0)
|
|
1486
|
+
requested.push(String(flags.get('root') || '.'));
|
|
1487
|
+
const discovered = new Map();
|
|
1488
|
+
for (const requestedRoot of requested) {
|
|
1489
|
+
const root = await realpath(requestedRoot);
|
|
1490
|
+
const direct = await discoverRepositoryAt(root);
|
|
1491
|
+
if (direct) {
|
|
1492
|
+
discovered.set(direct.path, direct);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
1496
|
+
if (!entry.isDirectory())
|
|
1497
|
+
continue;
|
|
1498
|
+
const candidate = await discoverRepositoryAt(resolve(root, entry.name));
|
|
1499
|
+
if (candidate)
|
|
1500
|
+
discovered.set(candidate.path, candidate);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
scanBoundary: 'explicit_roots_and_immediate_children',
|
|
1506
|
+
repositories: [...discovered.values()].filter(Boolean),
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function repositoryKeyAssignments(paths, values) {
|
|
1510
|
+
const keys = values.filter((value) => typeof value === 'string');
|
|
1511
|
+
const assignments = new Map();
|
|
1512
|
+
if (keys.length === 0)
|
|
1513
|
+
return assignments;
|
|
1514
|
+
const named = keys.filter((value) => value.includes('='));
|
|
1515
|
+
if (named.length > 0 && named.length !== keys.length) {
|
|
1516
|
+
throw new Error('Use either ordered --repository-key values or path=key assignments, not both.');
|
|
1517
|
+
}
|
|
1518
|
+
if (named.length > 0) {
|
|
1519
|
+
for (const value of named) {
|
|
1520
|
+
const split = value.indexOf('=');
|
|
1521
|
+
const path = resolve(value.slice(0, split));
|
|
1522
|
+
const index = paths.findIndex((candidate) => resolve(candidate) === path);
|
|
1523
|
+
if (index < 0)
|
|
1524
|
+
throw new Error(`Repository key assignment does not match a selected repository: ${value.slice(0, split)}`);
|
|
1525
|
+
assignments.set(index, value.slice(split + 1));
|
|
1526
|
+
}
|
|
1527
|
+
return assignments;
|
|
1528
|
+
}
|
|
1529
|
+
if (keys.length !== paths.length) {
|
|
1530
|
+
if (paths.length === 1 && keys.length === 1)
|
|
1531
|
+
return new Map([[0, keys[0]]]);
|
|
1532
|
+
throw new Error('Provide one ordered --repository-key per selected repository without a hosted remote.');
|
|
1533
|
+
}
|
|
1534
|
+
keys.forEach((key, index) => assignments.set(index, key));
|
|
1535
|
+
return assignments;
|
|
1536
|
+
}
|
|
1537
|
+
async function repositoriesConnect(flags, positional, repeatedRepos, repeatedKeys, repeatedProviders) {
|
|
1538
|
+
const selected = [...positional, ...repeatedRepos.filter((value) => typeof value === 'string')];
|
|
1539
|
+
if (selected.length === 0)
|
|
1540
|
+
selected.push(String(flags.get('repo') || '.'));
|
|
1541
|
+
const canonical = await Promise.all(selected.map((path) => realpath(path)));
|
|
1542
|
+
const unique = [...new Set(canonical)];
|
|
1543
|
+
const keys = repositoryKeyAssignments(unique, repeatedKeys.length ? repeatedKeys : (typeof flags.get('repository-key') === 'string' ? [String(flags.get('repository-key'))] : []));
|
|
1544
|
+
const providerIds = parseSelectedProviderIds(repeatedProviders.length ? repeatedProviders : (typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []));
|
|
1545
|
+
const results = [];
|
|
1546
|
+
for (let index = 0; index < unique.length; index += 1) {
|
|
1547
|
+
const connectFlags = new Map(flags);
|
|
1548
|
+
connectFlags.set('workspace', unique[index]);
|
|
1549
|
+
const repositoryKey = keys.get(index);
|
|
1550
|
+
if (repositoryKey)
|
|
1551
|
+
connectFlags.set('repository-key', repositoryKey);
|
|
1552
|
+
else
|
|
1553
|
+
connectFlags.delete('repository-key');
|
|
1554
|
+
if (providerIds)
|
|
1555
|
+
connectFlags.set('providers', providerIds.join(','));
|
|
1556
|
+
const result = await onboard(connectFlags);
|
|
1557
|
+
results.push(result);
|
|
1558
|
+
if (result?.stage === 'approve_device')
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
return { ok: true, requested: unique.length, connected: results.length, repositories: results };
|
|
1562
|
+
}
|
|
1563
|
+
export function parseSelectedProviderIds(values) {
|
|
1564
|
+
const selected = [...new Set(values
|
|
1565
|
+
.filter((value) => typeof value === 'string')
|
|
1566
|
+
.flatMap((value) => value.split(','))
|
|
1567
|
+
.map((value) => value.trim())
|
|
1568
|
+
.filter(Boolean))];
|
|
1569
|
+
if (selected.length === 0)
|
|
1570
|
+
return null;
|
|
1571
|
+
if (selected.some((value) => !['codex', 'claude', 'agy'].includes(value))) {
|
|
1572
|
+
throw new Error('Repository providers must be codex, claude, or agy. Repeat --provider to select more than one.');
|
|
1573
|
+
}
|
|
1574
|
+
return selected;
|
|
1575
|
+
}
|
|
1576
|
+
function selectedProviderAdapters(providerIds) {
|
|
1577
|
+
return providerIds
|
|
1578
|
+
? providerAdapters.filter((adapter) => providerIds.includes(adapter.providerId))
|
|
1579
|
+
: providerAdapters;
|
|
1580
|
+
}
|
|
1581
|
+
async function repositoriesList(flags) {
|
|
1582
|
+
const verbose = flags.has('verbose') || flags.has('diagnostic');
|
|
1583
|
+
return {
|
|
1584
|
+
ok: true,
|
|
1585
|
+
repositories: (await registry()).map((item) => ({
|
|
1586
|
+
workspaceId: item.workspaceId,
|
|
1587
|
+
name: item.name,
|
|
1588
|
+
repositoryAgentId: item.repositoryAgentId || null,
|
|
1589
|
+
repositoryAgentKey: item.repositoryAgentKey || null,
|
|
1590
|
+
controlBranch: item.controlBranch || null,
|
|
1591
|
+
connected: Boolean(item.repositoryAgentId && item.controlBranch),
|
|
1592
|
+
...(verbose ? { localPath: item.path, sourceFingerprint: item.repositoryRemoteHash } : {}),
|
|
1593
|
+
})),
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
async function bindRepositoryAgent(fabric, item) {
|
|
1597
|
+
if (item.repositoryAgentId && item.repositoryBindingId && item.repositoryAgentKey && item.controlBranch)
|
|
1598
|
+
return item;
|
|
1599
|
+
const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
|
|
1600
|
+
const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
|
|
1601
|
+
? item.repositoryRemoteHash
|
|
1602
|
+
: remote ? sourceRepositoryFingerprint(remote).fingerprint : item.repositoryRemoteHash;
|
|
1603
|
+
if (!currentIdentity) {
|
|
1604
|
+
throw new Error('Repository identity is missing. Re-run dharma workspace add with --repository-key.');
|
|
1605
|
+
}
|
|
1606
|
+
const response = await fabric.connectRepositoryAgent({
|
|
1607
|
+
sourceFingerprint: currentIdentity,
|
|
1608
|
+
displayName: item.name,
|
|
1609
|
+
defaultSourceRef: item.defaultBranch,
|
|
1610
|
+
workspaceId: item.workspaceId,
|
|
1611
|
+
legacySourceFingerprint: item.repositoryIdentityVersion === 'normalized-v1' ? null : item.repositoryRemoteHash,
|
|
1612
|
+
});
|
|
1613
|
+
const repositoryAgent = response.repositoryAgent && typeof response.repositoryAgent === 'object'
|
|
1614
|
+
? response.repositoryAgent
|
|
1615
|
+
: null;
|
|
1616
|
+
const branch = repositoryAgent?.branch && typeof repositoryAgent.branch === 'object'
|
|
1617
|
+
? repositoryAgent.branch
|
|
1618
|
+
: null;
|
|
1619
|
+
const updated = {
|
|
1620
|
+
...item,
|
|
1621
|
+
repositoryRemoteHash: currentIdentity,
|
|
1622
|
+
repositoryIdentityVersion: 'normalized-v1',
|
|
1623
|
+
repositoryAgentId: String(repositoryAgent?.organization_agent_id || ''),
|
|
1624
|
+
repositoryBindingId: String(repositoryAgent?.id || ''),
|
|
1625
|
+
repositoryAgentKey: String(repositoryAgent?.agent_key || ''),
|
|
1626
|
+
controlBranch: String(repositoryAgent?.control_branch || branch?.branch || ''),
|
|
1627
|
+
};
|
|
1628
|
+
if (!/^[0-9a-f-]{36}$/i.test(updated.repositoryAgentId || '')
|
|
1629
|
+
|| !/^[0-9a-f-]{36}$/i.test(updated.repositoryBindingId || '')
|
|
1630
|
+
|| !/^repo:[a-f0-9]{24}$/.test(updated.repositoryAgentKey || '')
|
|
1631
|
+
|| !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
|
|
1632
|
+
throw new Error('Dharma HQ returned an invalid repository-agent binding.');
|
|
1633
|
+
}
|
|
1634
|
+
await saveWorkspaceRecord(updated);
|
|
1635
|
+
return updated;
|
|
1229
1636
|
}
|
|
1230
1637
|
async function workspaceSync(flags, positional) {
|
|
1231
1638
|
const workspaceId = positional[0] || required(flags, 'workspace-id');
|
|
@@ -1239,13 +1646,16 @@ async function workspaceSync(flags, positional) {
|
|
|
1239
1646
|
next: `dharma workspace sync ${workspaceId} --policy-revision ${policyRevision} --apply`,
|
|
1240
1647
|
};
|
|
1241
1648
|
}
|
|
1242
|
-
|
|
1649
|
+
const fabric = await client();
|
|
1650
|
+
const providerIds = parseSelectedProviderIds(typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
|
|
1651
|
+
return syncWorkspacePolicy(fabric, await bindRepositoryAgent(fabric, item), policyRevision, true, providerIds);
|
|
1243
1652
|
}
|
|
1244
|
-
async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true) {
|
|
1245
|
-
const providers = await Promise.all(
|
|
1653
|
+
async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true, providerIds = null) {
|
|
1654
|
+
const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
|
|
1246
1655
|
const response = await fabric.registerWorkspace({
|
|
1247
1656
|
workspaceId: item.workspaceId, name: item.name, routeHash: item.routeHash,
|
|
1248
1657
|
repositoryRemoteHash: item.repositoryRemoteHash, defaultBranch: item.defaultBranch,
|
|
1658
|
+
repositoryAgentId: item.repositoryAgentId,
|
|
1249
1659
|
policyRevision, providers,
|
|
1250
1660
|
});
|
|
1251
1661
|
const generated = await materializeWorkspacePolicy({
|
|
@@ -1334,46 +1744,68 @@ Use the installed \`dharma\` CLI for organization-scoped agent work. Never print
|
|
|
1334
1744
|
6. Use only signed tasks whose organization, device, workspace, authority, budget, and skill pin pass local validation.
|
|
1335
1745
|
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
1746
|
8. Install only signed skill bundles. Preserve the active bundle receipt and automatic rollback result.
|
|
1747
|
+
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
1748
|
|
|
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.
|
|
1749
|
+
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
1750
|
`;
|
|
1340
1751
|
const reference = `# Organization connection
|
|
1341
1752
|
|
|
1342
1753
|
- HQ API: ${input.hqUrl}
|
|
1343
1754
|
- Organization: ${input.organizationId}
|
|
1344
1755
|
- Workspace: ${input.workspaceId}
|
|
1756
|
+
- Repository agent: ${input.repositoryAgentKey || 'pending'}
|
|
1757
|
+
- Permanent control branch: ${input.controlBranch || 'pending'}
|
|
1345
1758
|
- Policy revision: ${input.policyRevision}
|
|
1346
1759
|
- OpenAPI: ${input.hqUrl}/api/v1/agent-fabric/openapi.json
|
|
1760
|
+
- Organization instructions: ${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions
|
|
1347
1761
|
|
|
1348
1762
|
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
1763
|
`;
|
|
1350
1764
|
const connection = {
|
|
1351
|
-
schema: 'dharma.repository-connection/
|
|
1765
|
+
schema: 'dharma.repository-connection/v2',
|
|
1352
1766
|
hqUrl: input.hqUrl,
|
|
1353
1767
|
organizationId: input.organizationId,
|
|
1354
1768
|
workspaceId: input.workspaceId,
|
|
1769
|
+
repositoryAgentId: input.repositoryAgentId || null,
|
|
1770
|
+
repositoryAgentKey: input.repositoryAgentKey || null,
|
|
1771
|
+
controlBranch: input.controlBranch || null,
|
|
1355
1772
|
policyRevision: input.policyRevision,
|
|
1356
1773
|
openapiUrl: `${input.hqUrl}/api/v1/agent-fabric/openapi.json`,
|
|
1774
|
+
instructionsUrl: `${input.hqUrl}/api/v1/orgs/${input.organizationId}/agent-fabric/instructions`,
|
|
1775
|
+
};
|
|
1776
|
+
const repositoryAgent = {
|
|
1777
|
+
schema: 'dharma.repository-agent/v1',
|
|
1778
|
+
organizationId: input.organizationId,
|
|
1779
|
+
organizationAgentId: input.repositoryAgentId || null,
|
|
1780
|
+
agentKey: input.repositoryAgentKey || null,
|
|
1781
|
+
controlBranch: input.controlBranch || null,
|
|
1782
|
+
workspaceId: input.workspaceId,
|
|
1357
1783
|
};
|
|
1358
1784
|
await writeFile(resolve(skillRoot, 'SKILL.md'), skill, { mode: 0o600 });
|
|
1359
1785
|
await writeFile(resolve(skillRoot, 'references', 'organization.md'), reference, { mode: 0o600 });
|
|
1360
1786
|
await writeFile(marker, `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`, { mode: 0o600 });
|
|
1361
1787
|
await writeFile(resolve(input.workspace, '.dharma', 'agent-fabric.json'), `${JSON.stringify(connection, null, 2)}\n`, { mode: 0o600 });
|
|
1788
|
+
await writeFile(resolve(input.workspace, '.dharma', 'repository-agent.json'), `${JSON.stringify(repositoryAgent, null, 2)}\n`, { mode: 0o600 });
|
|
1362
1789
|
return {
|
|
1363
1790
|
skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
|
|
1364
1791
|
connectionPath: '.dharma/agent-fabric.json',
|
|
1792
|
+
repositoryAgentPath: '.dharma/repository-agent.json',
|
|
1365
1793
|
};
|
|
1366
1794
|
}
|
|
1367
1795
|
async function onboard(flags) {
|
|
1368
1796
|
const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
|
|
1369
1797
|
const organizationId = required(flags, 'organization-id');
|
|
1370
1798
|
const policyRevision = required(flags, 'policy-revision');
|
|
1371
|
-
const requestedHqUrl = normalizeHqUrl(
|
|
1799
|
+
const requestedHqUrl = normalizeHqUrl(portalUrl(flags));
|
|
1372
1800
|
let config = await readDeviceConfig();
|
|
1373
1801
|
if (!config) {
|
|
1374
1802
|
const loginFlags = new Map(flags);
|
|
1375
1803
|
loginFlags.set('hq-url', requestedHqUrl);
|
|
1376
1804
|
loginFlags.set('organization-id', organizationId);
|
|
1805
|
+
if (flags.has('non-interactive')) {
|
|
1806
|
+
loginFlags.set('no-browser', true);
|
|
1807
|
+
loginFlags.set('no-wait', true);
|
|
1808
|
+
}
|
|
1377
1809
|
const enrollment = await login(loginFlags);
|
|
1378
1810
|
if (enrollment.status !== 'approved') {
|
|
1379
1811
|
return {
|
|
@@ -1390,24 +1822,30 @@ async function onboard(flags) {
|
|
|
1390
1822
|
if (config.organizationId !== organizationId) {
|
|
1391
1823
|
throw new Error('This DHARMA_HOME is enrolled to a different organization. Use a separate DHARMA_HOME for each organization.');
|
|
1392
1824
|
}
|
|
1393
|
-
if (flags.has('hq-url') && config.hqUrl !== requestedHqUrl) {
|
|
1394
|
-
throw new Error('This device is enrolled to a different Dharma
|
|
1825
|
+
if ((flags.has('portal-url') || flags.has('hq-url')) && config.hqUrl !== requestedHqUrl) {
|
|
1826
|
+
throw new Error('This device is enrolled to a different Dharma portal origin. Use a separate DHARMA_HOME for each portal origin.');
|
|
1395
1827
|
}
|
|
1396
1828
|
const hqUrl = config.hqUrl;
|
|
1829
|
+
const providerIds = parseSelectedProviderIds(typeof flags.get('providers') === 'string'
|
|
1830
|
+
? [String(flags.get('providers'))]
|
|
1831
|
+
: typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
|
|
1397
1832
|
let registered = (await registry()).find((item) => item.path === workspace);
|
|
1398
|
-
if (!registered) {
|
|
1399
|
-
|
|
1833
|
+
if (!registered || (!registered.repositoryRemoteHash && typeof flags.get('repository-key') === 'string')) {
|
|
1834
|
+
const addFlags = new Map([
|
|
1400
1835
|
['organization-id', organizationId],
|
|
1401
1836
|
['path', workspace],
|
|
1402
1837
|
['name', String(flags.get('name') || basename(workspace))],
|
|
1403
|
-
])
|
|
1838
|
+
]);
|
|
1839
|
+
if (typeof flags.get('repository-key') === 'string')
|
|
1840
|
+
addFlags.set('repository-key', String(flags.get('repository-key')));
|
|
1841
|
+
await workspaceAdd(addFlags, [workspace]);
|
|
1404
1842
|
registered = (await registry()).find((item) => item.path === workspace);
|
|
1405
1843
|
}
|
|
1406
1844
|
if (!registered)
|
|
1407
1845
|
throw new Error('Workspace registration failed.');
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1846
|
+
const fabric = await client();
|
|
1847
|
+
registered = await bindRepositoryAgent(fabric, registered);
|
|
1848
|
+
const synced = await syncWorkspacePolicy(fabric, registered, policyRevision, true, providerIds);
|
|
1411
1849
|
const localPolicy = synced.localPolicy;
|
|
1412
1850
|
const authoritativeRevision = String(localPolicy?.revision || policyRevision);
|
|
1413
1851
|
const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
|
|
@@ -1416,9 +1854,12 @@ async function onboard(flags) {
|
|
|
1416
1854
|
hqUrl,
|
|
1417
1855
|
organizationId,
|
|
1418
1856
|
workspaceId: registered.workspaceId,
|
|
1857
|
+
repositoryAgentId: registered.repositoryAgentId,
|
|
1858
|
+
repositoryAgentKey: registered.repositoryAgentKey,
|
|
1859
|
+
controlBranch: registered.controlBranch,
|
|
1419
1860
|
policyRevision: authoritativeRevision,
|
|
1420
1861
|
});
|
|
1421
|
-
const providers = await Promise.all(
|
|
1862
|
+
const providers = await Promise.all(selectedProviderAdapters(providerIds).map((adapter) => adapter.capability()));
|
|
1422
1863
|
const nativeSkillResult = await installAvailableNativeAgentFabricBootstraps({
|
|
1423
1864
|
providers,
|
|
1424
1865
|
workspace,
|
|
@@ -1426,11 +1867,18 @@ async function onboard(flags) {
|
|
|
1426
1867
|
organizationId,
|
|
1427
1868
|
hqUrl,
|
|
1428
1869
|
});
|
|
1870
|
+
const primaryProvider = providers[0]?.provider || 'codex';
|
|
1429
1871
|
return {
|
|
1430
1872
|
ok: true,
|
|
1431
1873
|
stage: nativeSkillResult.failures.length ? 'ready_with_provider_actions' : 'ready',
|
|
1432
1874
|
organizationId,
|
|
1433
1875
|
workspaceId: registered.workspaceId,
|
|
1876
|
+
repositoryAgent: {
|
|
1877
|
+
id: registered.repositoryAgentId,
|
|
1878
|
+
key: registered.repositoryAgentKey,
|
|
1879
|
+
bindingId: registered.repositoryBindingId,
|
|
1880
|
+
controlBranch: registered.controlBranch,
|
|
1881
|
+
},
|
|
1434
1882
|
deviceId: config.deviceId,
|
|
1435
1883
|
providers,
|
|
1436
1884
|
organizationPolicy: {
|
|
@@ -1445,10 +1893,10 @@ async function onboard(flags) {
|
|
|
1445
1893
|
nativeSkillFailures: nativeSkillResult.failures,
|
|
1446
1894
|
workspaceSync: synced,
|
|
1447
1895
|
next: {
|
|
1448
|
-
preview:
|
|
1449
|
-
sync:
|
|
1896
|
+
preview: `dharma evidence preview --workspace . --provider ${primaryProvider}`,
|
|
1897
|
+
sync: `dharma evidence capture-batch --workspace . --provider ${primaryProvider} --policy .dharma/approved-policy.json --maximum-sessions 20 --sync`,
|
|
1450
1898
|
relay: 'dharma relay start --policy .dharma/approved-policy.json',
|
|
1451
|
-
verifySkill:
|
|
1899
|
+
verifySkill: `dharma skills verify --provider ${primaryProvider} --workspace .`,
|
|
1452
1900
|
},
|
|
1453
1901
|
};
|
|
1454
1902
|
}
|
|
@@ -1683,7 +2131,7 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
|
1683
2131
|
if (task.target.deviceId !== config.deviceId)
|
|
1684
2132
|
throw new Error('Task target does not match this enrolled device.');
|
|
1685
2133
|
const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
|
|
1686
|
-
const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider));
|
|
2134
|
+
const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider), task.workspaceId);
|
|
1687
2135
|
try {
|
|
1688
2136
|
assertTaskSkillPin(task.skillBundle, activeBundleId);
|
|
1689
2137
|
}
|
|
@@ -1816,6 +2264,13 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
1816
2264
|
const nativeMarkerPath = resolve(nativeRoot, 'dharma-agent-fabric', '.dharma-agent-fabric-bootstrap.json');
|
|
1817
2265
|
const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
|
|
1818
2266
|
const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
|
|
2267
|
+
let workspaceId;
|
|
2268
|
+
try {
|
|
2269
|
+
const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
|
|
2270
|
+
if (typeof connection.workspaceId === 'string')
|
|
2271
|
+
workspaceId = connection.workspaceId;
|
|
2272
|
+
}
|
|
2273
|
+
catch { }
|
|
1819
2274
|
return {
|
|
1820
2275
|
provider: input.provider,
|
|
1821
2276
|
ready: repositoryInstalled && nativeInstalled,
|
|
@@ -1824,7 +2279,8 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
1824
2279
|
repositorySkillPath,
|
|
1825
2280
|
connectionPath,
|
|
1826
2281
|
nativeSkillPath,
|
|
1827
|
-
|
|
2282
|
+
workspaceId: workspaceId || null,
|
|
2283
|
+
activeBundleId: workspaceId ? await getActiveSkillBundleId(nativeRoot, workspaceId) : null,
|
|
1828
2284
|
activation: 'next_session',
|
|
1829
2285
|
nextAction: repositoryInstalled && nativeInstalled
|
|
1830
2286
|
? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
|
|
@@ -1908,7 +2364,11 @@ async function skillSync(flags) {
|
|
|
1908
2364
|
const policy = await loadOrganizationPolicy(required(flags, 'policy'));
|
|
1909
2365
|
const destination = nativeSkillDirectory(provider);
|
|
1910
2366
|
const fabric = await client();
|
|
1911
|
-
const response = await fabric.pollSkill({
|
|
2367
|
+
const response = await fabric.pollSkill({
|
|
2368
|
+
workspaceId,
|
|
2369
|
+
provider,
|
|
2370
|
+
installedBundleId: await getActiveSkillBundleId(destination, workspaceId),
|
|
2371
|
+
});
|
|
1912
2372
|
const rollout = response.rollout;
|
|
1913
2373
|
if (!rollout)
|
|
1914
2374
|
return { ok: true, rollout: null, changed: false };
|
|
@@ -2023,7 +2483,7 @@ async function relayStart(flags) {
|
|
|
2023
2483
|
return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
|
|
2024
2484
|
}
|
|
2025
2485
|
export async function run(argv) {
|
|
2026
|
-
const { positional, flags } =
|
|
2486
|
+
const { positional, flags, repeated } = parseCliOptions(argv);
|
|
2027
2487
|
const [command, subcommand] = positional;
|
|
2028
2488
|
if (flags.has('help') || command === 'help')
|
|
2029
2489
|
return USAGE;
|
|
@@ -2035,6 +2495,22 @@ export async function run(argv) {
|
|
|
2035
2495
|
return login(flags);
|
|
2036
2496
|
if (command === 'providers' && subcommand === 'list')
|
|
2037
2497
|
return { providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())) };
|
|
2498
|
+
if (command === 'repositories' && subcommand === 'discover') {
|
|
2499
|
+
return repositoriesDiscover(flags, positional.slice(2), repeated.get('root') || []);
|
|
2500
|
+
}
|
|
2501
|
+
if (command === 'repositories' && subcommand === 'connect') {
|
|
2502
|
+
return repositoriesConnect(flags, positional.slice(2), repeated.get('repo') || [], repeated.get('repository-key') || [], repeated.get('provider') || []);
|
|
2503
|
+
}
|
|
2504
|
+
if (command === 'repositories' && subcommand === 'list')
|
|
2505
|
+
return repositoriesList(flags);
|
|
2506
|
+
if (command === 'repositories' && subcommand === 'status') {
|
|
2507
|
+
const listed = await repositoriesList(flags);
|
|
2508
|
+
return {
|
|
2509
|
+
...listed,
|
|
2510
|
+
relay: await relayProcessState(),
|
|
2511
|
+
providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())),
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2038
2514
|
if (command === 'workspace' && subcommand === 'add')
|
|
2039
2515
|
return workspaceAdd(flags, positional.slice(2));
|
|
2040
2516
|
if (command === 'workspace' && subcommand === 'sync')
|
|
@@ -2066,12 +2542,18 @@ export async function run(argv) {
|
|
|
2066
2542
|
return status;
|
|
2067
2543
|
}
|
|
2068
2544
|
}
|
|
2545
|
+
if ([
|
|
2546
|
+
'organization', 'agents', 'experiments', 'failures', 'remediations', 'handoffs', 'usage',
|
|
2547
|
+
].includes(String(command)) || (['tasks', 'skills'].includes(String(command))
|
|
2548
|
+
&& ['list', 'dispatch', 'release', 'rollout', 'rollback'].includes(String(subcommand)))) {
|
|
2549
|
+
const result = await runOrganizationCommand(command, subcommand, flags);
|
|
2550
|
+
if (result)
|
|
2551
|
+
return result;
|
|
2552
|
+
}
|
|
2069
2553
|
if (command === 'tasks' && subcommand === 'run-once')
|
|
2070
2554
|
return runOneTask(flags);
|
|
2071
2555
|
if (command === 'relay' && subcommand === 'start')
|
|
2072
2556
|
return relayStart(flags);
|
|
2073
|
-
if (command === 'tasks' && subcommand === 'list')
|
|
2074
|
-
return { tasks: [], coverage: 'server_poll_requires_relay' };
|
|
2075
2557
|
if (command === 'skills' && subcommand === 'sync')
|
|
2076
2558
|
return skillSync(flags);
|
|
2077
2559
|
if (command === 'skills' && subcommand === 'status') {
|
|
@@ -2079,7 +2561,13 @@ export async function run(argv) {
|
|
|
2079
2561
|
if (!['codex', 'claude', 'agy'].includes(providerValue))
|
|
2080
2562
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2081
2563
|
const root = nativeSkillDirectory(providerValue);
|
|
2082
|
-
|
|
2564
|
+
const workspaceId = required(flags, 'workspace-id');
|
|
2565
|
+
return {
|
|
2566
|
+
provider: providerValue,
|
|
2567
|
+
workspaceId,
|
|
2568
|
+
activeBundleId: await getActiveSkillBundleId(root, workspaceId),
|
|
2569
|
+
nativeSkillDirectory: root,
|
|
2570
|
+
};
|
|
2083
2571
|
}
|
|
2084
2572
|
if (command === 'skills' && subcommand === 'verify') {
|
|
2085
2573
|
const providerValue = required(flags, 'provider');
|