@dharma-ai-labs/agent-fabric 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1018 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from 'node:child_process';
3
+ import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
4
+ import { realpathSync } from 'node:fs';
5
+ import { access, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { promisify } from 'node:util';
10
+ import { canonicalize, sha256, validateContract, verifyCanonicalObject } from '@dharma-ai-labs/agent-fabric-contracts';
11
+ import { buildTrajectoryCapsule, redactValue } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
12
+ import { LocalVault, loadOrCreateVaultMasterKey } from '@dharma-ai-labs/agent-fabric-local-vault';
13
+ import { loadOrganizationPolicy } from '@dharma-ai-labs/agent-fabric-policy';
14
+ import { agyAdapter, claudeAdapter, codexAdapter, providerAdapters } from '@dharma-ai-labs/agent-fabric-provider-adapters';
15
+ import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
16
+ import { getActiveSkillBundleId, installSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
17
+ import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
18
+ const VERSION = '0.1.0';
19
+ const execFileAsync = promisify(execFile);
20
+ function options(args) {
21
+ const positional = [];
22
+ const flags = new Map();
23
+ for (let index = 0; index < args.length; index += 1) {
24
+ const value = args[index];
25
+ if (!value.startsWith('--')) {
26
+ positional.push(value);
27
+ continue;
28
+ }
29
+ const [rawKey, inline] = value.slice(2).split('=', 2);
30
+ if (inline !== undefined) {
31
+ flags.set(rawKey, inline);
32
+ continue;
33
+ }
34
+ const next = args[index + 1];
35
+ if (next && !next.startsWith('--')) {
36
+ flags.set(rawKey, next);
37
+ index += 1;
38
+ }
39
+ else
40
+ flags.set(rawKey, true);
41
+ }
42
+ return { positional, flags };
43
+ }
44
+ function required(flags, name) {
45
+ const value = flags.get(name);
46
+ if (typeof value !== 'string' || value.length === 0)
47
+ throw new Error(`Missing required option --${name}.`);
48
+ return value;
49
+ }
50
+ function print(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); }
51
+ export function isDirectExecution(argvPath, moduleUrl) {
52
+ if (!argvPath)
53
+ return false;
54
+ try {
55
+ return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ function dharmaHome() { return resolve(process.env.DHARMA_HOME || resolve(homedir(), '.dharma')); }
62
+ function configPath() { return resolve(dharmaHome(), 'device.json'); }
63
+ function pendingEnrollmentPath() { return resolve(dharmaHome(), 'pending-enrollment.json'); }
64
+ function protocolStatePath() { return resolve(dharmaHome(), 'relay', 'protocol-state.json'); }
65
+ function workspaceRegistryPath() { return resolve(dharmaHome(), 'registry', 'workspaces.json'); }
66
+ async function pathExists(path) {
67
+ try {
68
+ await access(path);
69
+ return true;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ export async function materializeWorkspacePolicy(input) {
76
+ const allowedCommands = {};
77
+ try {
78
+ const packageJson = JSON.parse(await readFile(resolve(input.workspace, 'package.json'), 'utf8'));
79
+ const scripts = packageJson.scripts || {};
80
+ for (const [script, commandId, timeoutSeconds] of [
81
+ ['test', 'repo.test', 1_200],
82
+ ['lint', 'repo.lint', 600],
83
+ ['typecheck', 'repo.typecheck', 600],
84
+ ['type-check', 'repo.typecheck', 600],
85
+ ['build', 'repo.build', 1_200],
86
+ ]) {
87
+ if (typeof scripts[script] === 'string' && !allowedCommands[commandId]) {
88
+ allowedCommands[commandId] = { argv: ['npm', 'run', script], timeoutSeconds };
89
+ }
90
+ }
91
+ }
92
+ catch { }
93
+ const writePaths = [];
94
+ for (const candidate of ['src', 'app', 'apps', 'lib', 'packages', 'test', 'tests', 'docs']) {
95
+ if (await pathExists(resolve(input.workspace, candidate)))
96
+ writePaths.push(`${candidate}/**`);
97
+ }
98
+ const policy = {
99
+ schema: 'dharma.organization-policy/v1',
100
+ organizationId: input.organizationId,
101
+ revision: input.revision,
102
+ evidence: {
103
+ defaultMode: 'deep',
104
+ registeredWorkspaceOnly: true,
105
+ excludePaths: ['.env', '.env.*', '.git/**', 'node_modules/**', 'dist/**', 'build/**', '**/*.pem', '**/*.key'],
106
+ maximumCapsuleBytes: 1_000_000,
107
+ maximumDailyUploadBytes: 50_000_000,
108
+ maximumExpansionBytes: 65_536,
109
+ pseudonymizeIdentity: true,
110
+ },
111
+ tasks: {
112
+ defaultNetwork: 'deny',
113
+ defaultGit: 'task_branch',
114
+ allowedCommands,
115
+ writePaths,
116
+ requireLocalConfirmationFor: ['network.allowlisted_domains', 'git.push', 'merge', 'deploy'],
117
+ },
118
+ skills: { automaticInstall: true, automaticPromotionMaxRisk: 'R2', canaryPercent: 10 },
119
+ retention: { rawLocalDays: 30, capsuleServerDays: 90 },
120
+ budgets: { dailyAnalysisCents: 1_000 },
121
+ };
122
+ const relativePath = '.dharma/approved-policy.json';
123
+ await mkdir(resolve(input.workspace, '.dharma'), { recursive: true, mode: 0o700 });
124
+ await writeFile(resolve(input.workspace, relativePath), `${JSON.stringify(policy, null, 2)}\n`, { mode: 0o600 });
125
+ return { relativePath, policy };
126
+ }
127
+ function providerAdapter(provider) {
128
+ if (provider === 'codex')
129
+ return codexAdapter;
130
+ if (provider === 'claude')
131
+ return claudeAdapter;
132
+ if (provider === 'agy')
133
+ return agyAdapter;
134
+ return null;
135
+ }
136
+ function deterministicUuid(value) {
137
+ const bytes = createHash('sha256').update(value).digest().subarray(0, 16);
138
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
139
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
140
+ const hex = bytes.toString('hex');
141
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
142
+ }
143
+ function responseTextFromEvent(value) {
144
+ if (!value || typeof value !== 'object' || Array.isArray(value))
145
+ return null;
146
+ const event = value;
147
+ const item = event.item && typeof event.item === 'object' && !Array.isArray(event.item)
148
+ ? event.item
149
+ : {};
150
+ if (item.type === 'agent_message' && typeof item.text === 'string')
151
+ return item.text;
152
+ if (event.type === 'result' && typeof event.result === 'string')
153
+ return event.result;
154
+ const message = event.message && typeof event.message === 'object' && !Array.isArray(event.message)
155
+ ? event.message
156
+ : {};
157
+ const content = Array.isArray(message.content) ? message.content : [];
158
+ const parts = content.flatMap((part) => {
159
+ if (!part || typeof part !== 'object' || Array.isArray(part))
160
+ return [];
161
+ const text = part.text;
162
+ return typeof text === 'string' ? [text] : [];
163
+ });
164
+ return parts.length ? parts.join('\n') : null;
165
+ }
166
+ export function taskResponsePreview(receipt) {
167
+ const provider = receipt.commandResults.find((result) => result.commandId.startsWith('provider.'));
168
+ if (!provider?.stdout)
169
+ return null;
170
+ const candidates = [];
171
+ for (const line of provider.stdout.split(/\r?\n/)) {
172
+ if (!line.trim())
173
+ continue;
174
+ try {
175
+ const response = responseTextFromEvent(JSON.parse(line));
176
+ if (response?.trim())
177
+ candidates.push(response.trim());
178
+ }
179
+ catch { }
180
+ }
181
+ const selected = candidates.at(-1);
182
+ if (!selected)
183
+ return null;
184
+ const stats = { classes: new Set(), redactedValues: 0, excludedPaths: 0, inputBytes: 0, outputBytes: 0 };
185
+ const redacted = String(redactValue(selected, stats));
186
+ return {
187
+ text: redacted.slice(0, 8_000),
188
+ truncated: redacted.length > 8_000,
189
+ redactionClasses: [...stats.classes].sort(),
190
+ redactedValues: stats.redactedValues,
191
+ };
192
+ }
193
+ export function assertTaskSkillPin(pinned, activeBundleId) {
194
+ if (pinned === undefined)
195
+ throw new Error('Task is missing its signed skill bundle pin.');
196
+ if ((pinned?.bundleId || null) !== activeBundleId) {
197
+ throw new Error(`Task skill bundle does not match the active local bundle (task=${pinned?.bundleId || 'none'}, local=${activeBundleId || 'none'}).`);
198
+ }
199
+ if (pinned && !/^sha256:[a-f0-9]{64}$/.test(pinned.bundleHash)) {
200
+ throw new Error('Task skill bundle hash is invalid.');
201
+ }
202
+ }
203
+ export function taskSkillPinFailureCode(error) {
204
+ const message = error instanceof Error ? error.message : '';
205
+ if (message.includes('missing its signed skill bundle pin'))
206
+ return 'skill_bundle_pin_missing';
207
+ if (message.includes('does not match the active local bundle'))
208
+ return 'skill_bundle_mismatch';
209
+ if (message.includes('bundle hash is invalid'))
210
+ return 'skill_bundle_hash_invalid';
211
+ return 'skill_bundle_preflight_failed';
212
+ }
213
+ async function platform() {
214
+ if (process.platform === 'win32')
215
+ return 'windows';
216
+ if (process.platform === 'darwin')
217
+ return 'macos';
218
+ if (process.platform === 'linux') {
219
+ try {
220
+ if (/microsoft|wsl/i.test(await readFile('/proc/version', 'utf8')))
221
+ return 'wsl';
222
+ }
223
+ catch { }
224
+ return 'linux';
225
+ }
226
+ throw new Error(`Unsupported device platform: ${process.platform}`);
227
+ }
228
+ async function registry() {
229
+ try {
230
+ return JSON.parse(await readFile(workspaceRegistryPath(), 'utf8'));
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
236
+ async function gitValue(workspace, argv) {
237
+ try {
238
+ return (await execFileAsync('git', ['-C', workspace, ...argv], { timeout: 10_000 })).stdout.trim() || null;
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ async function client() {
245
+ const instance = await AgentFabricClient.open({ configPath: configPath(), statePath: protocolStatePath() });
246
+ await instance.openSession(VERSION);
247
+ return instance;
248
+ }
249
+ async function login(flags) {
250
+ let pending;
251
+ if (flags.has('resume')) {
252
+ pending = JSON.parse(await readFile(pendingEnrollmentPath(), 'utf8'));
253
+ }
254
+ else {
255
+ const hqUrl = normalizeHqUrl(String(flags.get('hq-url') || 'https://www.dharma-ai.io'));
256
+ const organizationId = required(flags, 'organization-id');
257
+ const name = String(flags.get('device-name') || `${process.env.USER || process.env.USERNAME || 'developer'} device`);
258
+ const devicePlatform = await platform();
259
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl, organizationId });
260
+ const enrollment = await beginEnrollment({ hqUrl, organizationId, name, platform: devicePlatform, publicKeyEd25519: identity.publicKeyEd25519 });
261
+ pending = {
262
+ hqUrl, organizationId, name, platform: devicePlatform, publicKeyEd25519: identity.publicKeyEd25519,
263
+ deviceCode: enrollment.deviceCode, verificationUri: enrollment.verificationUri,
264
+ browserCode: enrollment.browserCode,
265
+ expiresAt: new Date(Date.now() + enrollment.expiresInSeconds * 1_000).toISOString(),
266
+ };
267
+ await mkdir(dharmaHome(), { recursive: true, mode: 0o700 });
268
+ await writeFile(pendingEnrollmentPath(), `${JSON.stringify(pending, null, 2)}\n`, { mode: 0o600 });
269
+ if (flags.has('no-wait')) {
270
+ return { ok: true, status: 'pending', deviceCode: pending.deviceCode, verificationUri: pending.verificationUri, browserCode: pending.browserCode, expiresAt: pending.expiresAt };
271
+ }
272
+ }
273
+ const deadline = Date.parse(pending.expiresAt);
274
+ while (Date.now() < deadline) {
275
+ const result = await pollEnrollment({ hqUrl: pending.hqUrl, deviceCode: pending.deviceCode });
276
+ if (result.status === 'approved') {
277
+ if (typeof result.deviceId !== 'string' || typeof result.relayUrl !== 'string' || typeof result.serverPublicKeyEd25519 !== 'string') {
278
+ throw new Error('Enrollment was approved but the relay or server signing key is not configured.');
279
+ }
280
+ const config = {
281
+ schema: 'dharma.device-config/v1', hqUrl: pending.hqUrl, organizationId: pending.organizationId, deviceId: result.deviceId,
282
+ deviceName: pending.name, platform: pending.platform, publicKeyEd25519: pending.publicKeyEd25519,
283
+ serverPublicKeyEd25519: result.serverPublicKeyEd25519, relayUrl: result.relayUrl, enrolledAt: new Date().toISOString(),
284
+ };
285
+ await saveDeviceConfig(configPath(), config);
286
+ await rm(pendingEnrollmentPath(), { force: true });
287
+ return { ok: true, status: 'approved', deviceId: config.deviceId, organizationId: pending.organizationId, relayUrl: config.relayUrl };
288
+ }
289
+ if (result.status === 'denied' || result.status === 'expired')
290
+ throw new Error(`Enrollment ${result.status}.`);
291
+ if (flags.has('no-wait'))
292
+ return { ok: true, status: 'pending', verificationUri: pending.verificationUri, expiresAt: pending.expiresAt };
293
+ await new Promise((accept) => setTimeout(accept, 2_000));
294
+ }
295
+ throw new Error(`Enrollment timed out. Approve it at ${pending.verificationUri}`);
296
+ }
297
+ async function capture(flags, batch = false) {
298
+ const workspace = await realpath(required(flags, 'workspace'));
299
+ const provider = required(flags, 'provider');
300
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
301
+ const adapter = providerAdapter(provider);
302
+ if (!adapter)
303
+ throw new Error(`Unsupported capture provider: ${provider}`);
304
+ const root = flags.get('source-root');
305
+ let sessions = await adapter.discover({
306
+ workspace,
307
+ roots: typeof root === 'string' ? [root] : undefined,
308
+ maximumSessions: batch ? Math.min(Math.max(Number(flags.get('maximum-sessions') || 100), 1), 1_000) : 1,
309
+ maximumBytesPerSession: Math.min(Math.max(Number(flags.get('maximum-bytes-per-session') || 8_388_608), 65_536), 67_108_864),
310
+ });
311
+ const sessionIdsFile = flags.get('session-ids-file');
312
+ if (typeof sessionIdsFile === 'string') {
313
+ const values = JSON.parse(await readFile(resolve(sessionIdsFile), 'utf8'));
314
+ if (!Array.isArray(values) || values.length === 0 || values.length > 1_000
315
+ || values.some((value) => typeof value !== 'string' || value.length > 160)) {
316
+ throw new Error('Session ID allowlist must be a non-empty JSON string array with at most 1,000 entries.');
317
+ }
318
+ const allowed = new Set(values);
319
+ sessions = sessions.filter((candidate) => allowed.has(candidate.sessionId));
320
+ }
321
+ if (sessions.length === 0)
322
+ throw new Error('No workspace-qualified provider sessions were found.');
323
+ const session = sessions.at(-1);
324
+ const device = JSON.parse(await readFile(configPath(), 'utf8'));
325
+ const registered = (await registry()).find((item) => item.path === workspace);
326
+ if (!registered)
327
+ throw new Error('Workspace is not registered locally. Run dharma workspace add.');
328
+ const vault = await LocalVault.open({ root: resolve(dharmaHome(), 'vault'), masterKey: await loadOrCreateVaultMasterKey() });
329
+ try {
330
+ const capsules = [];
331
+ const syncResults = [];
332
+ const fabric = flags.has('sync') ? await client() : null;
333
+ for (const selected of batch ? sessions : [session]) {
334
+ const rawTurn = Buffer.from(`${selected.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
335
+ const rawContentId = await vault.putBlob(rawTurn, 'raw-provider-turn');
336
+ vault.recordSession({ sessionId: selected.sessionId, provider: selected.provider, workspaceId: registered.workspaceId, sourceLocator: selected.sourcePath, status: selected.coverage, observedAt: selected.endedAt });
337
+ const capsule = buildTrajectoryCapsule({
338
+ organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
339
+ session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
340
+ });
341
+ const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
342
+ if (!validation.ok)
343
+ throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
344
+ const capsuleBlob = await vault.putBlob(Buffer.from(JSON.stringify(capsule)), 'trajectory-capsule');
345
+ vault.recordCapsule(capsule.trajectoryId, capsule.revision, capsule.capsuleHash, capsuleBlob);
346
+ capsules.push(capsule);
347
+ if (fabric)
348
+ syncResults.push(await fabric.syncTrajectory(capsule));
349
+ }
350
+ const output = flags.get('output');
351
+ if (!batch) {
352
+ const capsule = capsules[0];
353
+ if (typeof output === 'string')
354
+ await writeFile(resolve(output), `${JSON.stringify(capsule, null, 2)}\n`, { mode: 0o600 });
355
+ if (flags.has('sync'))
356
+ return { capsule, sync: syncResults[0] };
357
+ return capsule;
358
+ }
359
+ const manifest = {
360
+ ok: true,
361
+ captured: capsules.length,
362
+ synced: syncResults.length,
363
+ coverage: {
364
+ observed: capsules.filter((capsule) => capsule.coverage.state === 'observed').length,
365
+ partial: capsules.filter((capsule) => capsule.coverage.state === 'partial').length,
366
+ },
367
+ trajectories: capsules.map((capsule) => ({
368
+ trajectoryId: capsule.trajectoryId,
369
+ sessionId: capsule.sessionId,
370
+ capsuleHash: capsule.capsuleHash,
371
+ status: capsule.status,
372
+ eventCount: capsule.events.length,
373
+ timeRange: capsule.timeRange,
374
+ })),
375
+ };
376
+ if (typeof output === 'string')
377
+ await writeFile(resolve(output), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
378
+ return manifest;
379
+ }
380
+ finally {
381
+ vault.close();
382
+ }
383
+ }
384
+ async function evidencePreview(flags) {
385
+ const workspace = await realpath(required(flags, 'workspace'));
386
+ const provider = required(flags, 'provider');
387
+ const adapter = providerAdapter(provider);
388
+ if (!adapter)
389
+ throw new Error(`Unsupported preview provider: ${provider}`);
390
+ const root = flags.get('source-root');
391
+ const maximumSessions = Math.min(Math.max(Number(flags.get('maximum-sessions') || 100), 1), 1_000);
392
+ const maximumBytesPerSession = Math.min(Math.max(Number(flags.get('maximum-bytes-per-session') || 8_388_608), 65_536), 67_108_864);
393
+ const sessions = await adapter.discover({
394
+ workspace,
395
+ roots: typeof root === 'string' ? [root] : undefined,
396
+ maximumSessions,
397
+ maximumBytesPerSession,
398
+ });
399
+ const eventKinds = {};
400
+ let records = 0;
401
+ for (const session of sessions) {
402
+ records += session.records.length;
403
+ for (const record of session.records)
404
+ eventKinds[record.kind] = (eventKinds[record.kind] || 0) + 1;
405
+ }
406
+ return {
407
+ ok: true,
408
+ provider,
409
+ workspaceQualified: true,
410
+ trajectoryCount: sessions.length,
411
+ recordCount: records,
412
+ coverage: {
413
+ observed: sessions.filter((session) => session.coverage === 'observed').length,
414
+ partial: sessions.filter((session) => session.coverage === 'partial').length,
415
+ },
416
+ timeRange: sessions.length > 0
417
+ ? { start: sessions[0].startedAt, end: sessions.at(-1).endedAt }
418
+ : null,
419
+ eventKinds: Object.fromEntries(Object.entries(eventKinds).sort(([left], [right]) => left.localeCompare(right))),
420
+ sessions: sessions.map((session) => ({
421
+ sessionId: session.sessionId,
422
+ startedAt: session.startedAt,
423
+ endedAt: session.endedAt,
424
+ coverage: session.coverage,
425
+ records: session.records.length,
426
+ })),
427
+ };
428
+ }
429
+ async function workspaceAdd(flags, positional) {
430
+ const path = await realpath(positional[0] || required(flags, 'path'));
431
+ const device = JSON.parse(await readFile(configPath(), 'utf8'));
432
+ const organizationId = String(flags.get('organization-id') || device.organizationId);
433
+ if (organizationId !== device.organizationId)
434
+ throw new Error('Workspace organization must match the enrolled device.');
435
+ await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
436
+ const workspaceId = deterministicUuid(`${organizationId}:${device.deviceId}:${path}`);
437
+ const remote = await gitValue(path, ['config', '--get', 'remote.origin.url']);
438
+ const entry = {
439
+ workspaceId, organizationId, name: String(flags.get('name') || basename(path)), path,
440
+ routeHash: `sha256:${createHash('sha256').update(path).digest('hex')}`,
441
+ repositoryRemoteHash: remote ? `sha256:${createHash('sha256').update(remote).digest('hex')}` : null,
442
+ defaultBranch: await gitValue(path, ['branch', '--show-current']), status: 'active',
443
+ };
444
+ const without = (await registry()).filter((item) => item.workspaceId !== workspaceId);
445
+ without.push(entry);
446
+ await writeFile(workspaceRegistryPath(), `${JSON.stringify(without, null, 2)}\n`, { mode: 0o600 });
447
+ return { ok: true, workspaceId, organizationId, pathStoredLocally: true, pathDisclosedToServer: false };
448
+ }
449
+ async function workspaceSync(flags, positional) {
450
+ const workspaceId = positional[0] || required(flags, 'workspace-id');
451
+ const item = (await registry()).find((candidate) => candidate.workspaceId === workspaceId);
452
+ if (!item)
453
+ throw new Error('Workspace is not registered locally.');
454
+ const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
455
+ return (await client()).registerWorkspace({
456
+ workspaceId: item.workspaceId, name: item.name, routeHash: item.routeHash,
457
+ repositoryRemoteHash: item.repositoryRemoteHash, defaultBranch: item.defaultBranch,
458
+ policyRevision: required(flags, 'policy-revision'), providers,
459
+ });
460
+ }
461
+ async function readDeviceConfig() {
462
+ try {
463
+ return JSON.parse(await readFile(configPath(), 'utf8'));
464
+ }
465
+ catch {
466
+ return null;
467
+ }
468
+ }
469
+ export async function installRepositoryAgentFabricSkill(input) {
470
+ const skillRoot = resolve(input.workspace, '.agents', 'skills', 'dharma-agent-fabric');
471
+ const marker = resolve(skillRoot, '.dharma-agent-fabric.json');
472
+ let skillRootExists = true;
473
+ try {
474
+ await access(skillRoot);
475
+ }
476
+ catch {
477
+ skillRootExists = false;
478
+ }
479
+ if (skillRootExists) {
480
+ try {
481
+ await access(marker);
482
+ }
483
+ catch {
484
+ throw new Error('Refusing to replace an unmanaged repository skill at .agents/skills/dharma-agent-fabric.');
485
+ }
486
+ }
487
+ await mkdir(resolve(skillRoot, 'references'), { recursive: true, mode: 0o700 });
488
+ await mkdir(resolve(input.workspace, '.dharma'), { recursive: true, mode: 0o700 });
489
+ const skill = `---
490
+ name: dharma-agent-fabric
491
+ description: Connect this repository's coding agents to the organization's Dharma Agent Fabric control plane.
492
+ ---
493
+
494
+ # Dharma Agent Fabric
495
+
496
+ Use the installed \`dharma\` CLI for organization-scoped agent work. Never print, commit, or transmit provider credentials, developer tokens, local paths, or raw private trajectories.
497
+
498
+ ## Required flow
499
+
500
+ 1. Run \`dharma status\` and \`dharma providers list\` before accepting a remote task.
501
+ 2. Keep \`dharma relay start --policy .dharma/approved-policy.json\` running for signed task, evidence, and skill delivery.
502
+ 3. Capture reduced evidence with \`dharma evidence capture-batch --workspace . --provider <provider> --policy .dharma/approved-policy.json --sync\`.
503
+ 4. Use only signed tasks whose organization, device, workspace, authority, budget, and skill pin pass local validation.
504
+ 5. 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.
505
+ 6. Install only signed skill bundles. Preserve the active bundle receipt and automatic rollback result.
506
+
507
+ 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.
508
+ `;
509
+ const reference = `# Organization connection
510
+
511
+ - HQ API: ${input.hqUrl}
512
+ - Organization: ${input.organizationId}
513
+ - Workspace: ${input.workspaceId}
514
+ - Policy revision: ${input.policyRevision}
515
+ - OpenAPI: ${input.hqUrl}/api/v1/agent-fabric/openapi.json
516
+
517
+ 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.
518
+ `;
519
+ const connection = {
520
+ schema: 'dharma.repository-connection/v1',
521
+ hqUrl: input.hqUrl,
522
+ organizationId: input.organizationId,
523
+ workspaceId: input.workspaceId,
524
+ policyRevision: input.policyRevision,
525
+ openapiUrl: `${input.hqUrl}/api/v1/agent-fabric/openapi.json`,
526
+ };
527
+ await writeFile(resolve(skillRoot, 'SKILL.md'), skill, { mode: 0o600 });
528
+ await writeFile(resolve(skillRoot, 'references', 'organization.md'), reference, { mode: 0o600 });
529
+ await writeFile(marker, `${JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: input.workspaceId }, null, 2)}\n`, { mode: 0o600 });
530
+ await writeFile(resolve(input.workspace, '.dharma', 'agent-fabric.json'), `${JSON.stringify(connection, null, 2)}\n`, { mode: 0o600 });
531
+ return {
532
+ skillPath: '.agents/skills/dharma-agent-fabric/SKILL.md',
533
+ connectionPath: '.dharma/agent-fabric.json',
534
+ };
535
+ }
536
+ async function onboard(flags) {
537
+ const workspace = await realpath(String(flags.get('workspace') || flags.get('path') || '.'));
538
+ const organizationId = required(flags, 'organization-id');
539
+ const policyRevision = required(flags, 'policy-revision');
540
+ const requestedHqUrl = normalizeHqUrl(String(flags.get('hq-url') || 'https://www.dharma-ai.io'));
541
+ let config = await readDeviceConfig();
542
+ if (!config) {
543
+ const loginFlags = new Map(flags);
544
+ loginFlags.set('hq-url', requestedHqUrl);
545
+ loginFlags.set('organization-id', organizationId);
546
+ if (!flags.has('resume'))
547
+ loginFlags.set('no-wait', true);
548
+ const enrollment = await login(loginFlags);
549
+ if (enrollment.status !== 'approved') {
550
+ return {
551
+ ok: true,
552
+ stage: 'approve_device',
553
+ enrollment,
554
+ nextCommand: `dharma onboard --resume --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`,
555
+ };
556
+ }
557
+ config = await readDeviceConfig();
558
+ }
559
+ if (!config)
560
+ throw new Error('Device enrollment did not produce a local device configuration.');
561
+ if (config.organizationId !== organizationId) {
562
+ throw new Error('This DHARMA_HOME is enrolled to a different organization. Use a separate DHARMA_HOME for each organization.');
563
+ }
564
+ if (flags.has('hq-url') && config.hqUrl !== requestedHqUrl) {
565
+ throw new Error('This device is enrolled to a different Dharma HQ origin. Use a separate DHARMA_HOME for each HQ origin.');
566
+ }
567
+ const hqUrl = config.hqUrl;
568
+ let registered = (await registry()).find((item) => item.path === workspace);
569
+ if (!registered) {
570
+ await workspaceAdd(new Map([
571
+ ['organization-id', organizationId],
572
+ ['path', workspace],
573
+ ['name', String(flags.get('name') || basename(workspace))],
574
+ ]), [workspace]);
575
+ registered = (await registry()).find((item) => item.path === workspace);
576
+ }
577
+ if (!registered)
578
+ throw new Error('Workspace registration failed.');
579
+ const generatedPolicy = await materializeWorkspacePolicy({
580
+ workspace,
581
+ organizationId,
582
+ revision: policyRevision,
583
+ });
584
+ const installed = await installRepositoryAgentFabricSkill({
585
+ workspace,
586
+ hqUrl,
587
+ organizationId,
588
+ workspaceId: registered.workspaceId,
589
+ policyRevision,
590
+ });
591
+ const synced = await workspaceSync(new Map([['policy-revision', policyRevision]]), [registered.workspaceId]);
592
+ const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
593
+ return {
594
+ ok: true,
595
+ stage: 'ready',
596
+ organizationId,
597
+ workspaceId: registered.workspaceId,
598
+ deviceId: config.deviceId,
599
+ providers,
600
+ organizationPolicy: {
601
+ path: generatedPolicy.relativePath,
602
+ revision: generatedPolicy.policy.revision,
603
+ commandIds: Object.keys(generatedPolicy.policy.tasks.allowedCommands).sort(),
604
+ writePaths: generatedPolicy.policy.tasks.writePaths,
605
+ },
606
+ repositorySkill: installed,
607
+ workspaceSync: synced,
608
+ next: {
609
+ preview: 'dharma evidence preview --workspace . --provider codex',
610
+ sync: 'dharma evidence capture-batch --workspace . --provider codex --policy .dharma/approved-policy.json --sync',
611
+ relay: 'dharma relay start --policy .dharma/approved-policy.json',
612
+ },
613
+ };
614
+ }
615
+ async function evidenceSync(flags) {
616
+ const capsule = JSON.parse(await readFile(resolve(required(flags, 'file')), 'utf8'));
617
+ return (await client()).syncTrajectory(capsule);
618
+ }
619
+ async function processEvidenceRequest(fabric, policy, workspaceId) {
620
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
621
+ const workspaces = (await registry()).filter((item) => !workspaceId || item.workspaceId === workspaceId);
622
+ if (workspaces.length === 0)
623
+ throw new Error('Evidence workspace is not registered locally.');
624
+ let request = null;
625
+ let workspace = null;
626
+ for (const item of workspaces) {
627
+ const polled = await fabric.pollEvidence({ workspaceId: item.workspaceId });
628
+ if (polled.request && typeof polled.request === 'object') {
629
+ request = polled.request;
630
+ workspace = item;
631
+ break;
632
+ }
633
+ }
634
+ if (!request || !workspace)
635
+ return { ok: true, request: null };
636
+ const contract = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/evidence-request/v1', request);
637
+ if (!contract.ok)
638
+ throw new Error(`Evidence request failed schema validation: ${JSON.stringify(contract.errors)}`);
639
+ const { signature, ...unsignedRequest } = request;
640
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
641
+ if (!verifyCanonicalObject(unsignedRequest, signature, serverPublicKey))
642
+ throw new Error('Evidence request signature is invalid.');
643
+ if (request.organizationId !== config.organizationId || request.deviceId !== config.deviceId
644
+ || request.workspaceId !== workspace.workspaceId || policy.organizationId !== config.organizationId) {
645
+ throw new Error('Evidence request does not match the enrolled organization, device, workspace, or policy.');
646
+ }
647
+ if (Date.parse(request.expiresAt) <= Date.now())
648
+ throw new Error('Evidence request has expired.');
649
+ const vault = await LocalVault.open({ root: resolve(dharmaHome(), 'vault'), masterKey: await loadOrCreateVaultMasterKey() });
650
+ try {
651
+ const capsule = await vault.getLatestCapsule(request.trajectoryId);
652
+ const contentIndex = Array.isArray(capsule.contentIndex) ? capsule.contentIndex : [];
653
+ const available = new Set(contentIndex.flatMap((item) => {
654
+ if (!item || typeof item !== 'object' || Array.isArray(item))
655
+ return [];
656
+ const record = item;
657
+ return record.availableLocally === true && record.uploaded !== true && typeof record.contentId === 'string'
658
+ ? [record.contentId] : [];
659
+ }));
660
+ const stats = { classes: new Set(), redactedValues: 0, excludedPaths: 0, inputBytes: 0, outputBytes: 0 };
661
+ const approved = [];
662
+ const excluded = [];
663
+ const authorizedBytes = Math.min(request.maximumBytes, policy.evidence.maximumExpansionBytes);
664
+ let bytesPrepared = 0;
665
+ for (const selector of request.selectors) {
666
+ if (!available.has(selector.contentId)) {
667
+ excluded.push({ contentId: selector.contentId, reasonCode: 'not_available_in_capsule' });
668
+ continue;
669
+ }
670
+ try {
671
+ const source = await vault.getBlob(selector.contentId);
672
+ const start = selector.range?.start ?? 0;
673
+ const end = selector.range?.end ?? source.byteLength;
674
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start || end > source.byteLength) {
675
+ excluded.push({ contentId: selector.contentId, reasonCode: 'invalid_range' });
676
+ continue;
677
+ }
678
+ const redacted = Buffer.from(String(redactValue(source.subarray(start, end).toString('utf8'), stats, '', { pseudonymizeIdentity: policy.evidence.pseudonymizeIdentity })), 'utf8');
679
+ if (redacted.byteLength === 0) {
680
+ excluded.push({ contentId: selector.contentId, reasonCode: 'redacted_empty' });
681
+ continue;
682
+ }
683
+ if (bytesPrepared + redacted.byteLength > authorizedBytes) {
684
+ excluded.push({ contentId: selector.contentId, reasonCode: 'byte_limit_exceeded' });
685
+ continue;
686
+ }
687
+ approved.push({
688
+ contentId: selector.contentId, bytes: redacted.byteLength,
689
+ chunkHash: sha256(redacted), contentBase64: redacted.toString('base64'),
690
+ });
691
+ bytesPrepared += redacted.byteLength;
692
+ }
693
+ catch {
694
+ excluded.push({ contentId: selector.contentId, reasonCode: 'vault_content_unavailable' });
695
+ }
696
+ }
697
+ const unsignedResponse = {
698
+ schema: 'dharma.evidence-response/v1', responseId: randomUUID(), requestId: request.requestId,
699
+ organizationId: config.organizationId, deviceId: config.deviceId, workspaceId: workspace.workspaceId,
700
+ trajectoryId: request.trajectoryId, approved, excluded,
701
+ redactionReceipt: { policyRevision: policy.revision, classes: [...stats.classes].sort(), redactedValues: stats.redactedValues },
702
+ bytesPrepared, createdAt: new Date().toISOString(),
703
+ };
704
+ const response = { ...unsignedResponse, responseHash: sha256(canonicalize(unsignedResponse)), signature: null };
705
+ const responseContract = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/evidence-response/v1', response);
706
+ if (!responseContract.ok)
707
+ throw new Error(`Evidence response failed schema validation: ${JSON.stringify(responseContract.errors)}`);
708
+ const accepted = await fabric.postEvidenceResponse(request.requestId, response);
709
+ const receipt = accepted.receipt && typeof accepted.receipt === 'object' ? accepted.receipt : {};
710
+ const receiptHash = typeof receipt.hash === 'string' && /^sha256:[a-f0-9]{64}$/.test(receipt.hash)
711
+ ? receipt.hash : response.responseHash;
712
+ vault.recordDisclosure(unsignedResponse.responseId, receiptHash, bytesPrepared);
713
+ return {
714
+ ok: true, requestId: request.requestId, responseId: unsignedResponse.responseId,
715
+ approved: approved.length, excluded: excluded.length, bytesPrepared, receipt,
716
+ };
717
+ }
718
+ finally {
719
+ vault.close();
720
+ }
721
+ }
722
+ async function runOneEvidenceRequest(flags) {
723
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
724
+ return processEvidenceRequest(await client(), policy, typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined);
725
+ }
726
+ async function executeOneTask(fabric, policy, leaseSeconds) {
727
+ const polled = await fabric.pollTask(leaseSeconds);
728
+ const taskRow = polled.task;
729
+ if (!taskRow?.envelope)
730
+ return { ok: true, task: null };
731
+ const task = taskRow.envelope;
732
+ const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
733
+ if (!workspace)
734
+ throw new Error('Task workspace is not registered on this device.');
735
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
736
+ if (task.target.deviceId !== config.deviceId)
737
+ throw new Error('Task target does not match this enrolled device.');
738
+ const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
739
+ const activeBundleId = await getActiveSkillBundleId(nativeSkillDirectory(task.target.provider));
740
+ try {
741
+ assertTaskSkillPin(task.skillBundle, activeBundleId);
742
+ }
743
+ catch (error) {
744
+ await fabric.postTaskEvent(task.taskId, 'failed', {
745
+ phase: 'preflight',
746
+ code: taskSkillPinFailureCode(error),
747
+ taskBundleId: task.skillBundle?.bundleId || null,
748
+ localBundleId: activeBundleId,
749
+ }).catch(() => undefined);
750
+ throw error;
751
+ }
752
+ await fabric.postTaskEvent(task.taskId, 'started', {
753
+ bundleId: task.skillBundle?.bundleId || null,
754
+ bundleHash: task.skillBundle?.bundleHash || null,
755
+ });
756
+ const heartbeats = [];
757
+ const heartbeat = setInterval(() => {
758
+ heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
759
+ }, Math.max(15_000, Math.floor(leaseSeconds * 500)));
760
+ let receipt;
761
+ try {
762
+ receipt = await executeTask({
763
+ task, policy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
764
+ receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
765
+ });
766
+ }
767
+ finally {
768
+ clearInterval(heartbeat);
769
+ await Promise.allSettled(heartbeats);
770
+ }
771
+ const summary = {
772
+ status: receipt.status, branch: receipt.branch,
773
+ response: taskResponsePreview(receipt),
774
+ commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
775
+ startedAt: receipt.startedAt, completedAt: receipt.completedAt,
776
+ };
777
+ await fabric.postTaskEvent(task.taskId, receipt.status, summary);
778
+ return { ok: true, taskId: task.taskId, receipt: summary };
779
+ }
780
+ async function runOneTask(flags) {
781
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
782
+ return executeOneTask(await client(), policy, Number(flags.get('lease-seconds') || 120));
783
+ }
784
+ export function nativeSkillDirectory(provider, env = process.env, home = homedir()) {
785
+ if (provider === 'codex')
786
+ return resolve(env.CODEX_HOME || resolve(home, '.codex'), 'skills');
787
+ if (provider === 'claude')
788
+ return resolve(env.CLAUDE_CONFIG_DIR || resolve(home, '.claude'), 'skills');
789
+ return resolve(env.AGY_CONFIG_DIR || resolve(home, '.gemini', 'antigravity-cli'), 'plugins', 'dharma-agent-fabric', 'skills');
790
+ }
791
+ export async function activateAgyPlugin(input = {}) {
792
+ const root = resolve(nativeSkillDirectory('agy', input.env, input.home), '..');
793
+ const execute = input.execute || ((executable, argv, options) => execFileAsync(executable, argv, options));
794
+ await mkdir(root, { recursive: true, mode: 0o700 });
795
+ const manifest = resolve(root, 'plugin.json');
796
+ try {
797
+ await access(manifest);
798
+ }
799
+ catch {
800
+ await writeFile(manifest, `${JSON.stringify({ name: 'dharma-agent-fabric' }, null, 2)}\n`, { mode: 0o600 });
801
+ }
802
+ await execute('agy', ['plugin', 'validate', root], { timeout: 30_000 });
803
+ await execute('agy', ['plugin', 'enable', 'dharma-agent-fabric'], { timeout: 30_000 });
804
+ }
805
+ function containedInlinePath(root, value) {
806
+ if (!value || value.includes('\\') || isAbsolute(value))
807
+ throw new Error('Inline skill file path is invalid.');
808
+ const segments = value.split('/');
809
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..'))
810
+ throw new Error('Inline skill file path is invalid.');
811
+ const candidate = resolve(root, ...segments);
812
+ const route = relative(resolve(root), candidate);
813
+ if (route === '..' || route.startsWith('../') || route.startsWith('..\\') || isAbsolute(route)) {
814
+ throw new Error('Inline skill file path escapes its skill root.');
815
+ }
816
+ return candidate;
817
+ }
818
+ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
819
+ if (bundle.operation === 'clear')
820
+ return false;
821
+ const inline = bundle.skills.map((skill) => skill.files);
822
+ if (inline.every((files) => files === undefined))
823
+ return false;
824
+ if (!inline.every((files) => Array.isArray(files) && files.length > 0 && files.length <= 32)) {
825
+ throw new Error('Every skill in an inline bundle must contain 1-32 signed files.');
826
+ }
827
+ let totalBytes = 0;
828
+ for (const skill of bundle.skills) {
829
+ const skillRoot = containedInlinePath(sourceRoot, skill.path);
830
+ const seen = new Set();
831
+ for (const file of skill.files || []) {
832
+ if (!file || typeof file.path !== 'string' || typeof file.contentBase64 !== 'string' || typeof file.sha256 !== 'string') {
833
+ throw new Error('Inline skill file metadata is invalid.');
834
+ }
835
+ if (seen.has(file.path))
836
+ throw new Error('Inline skill bundle contains duplicate file paths.');
837
+ seen.add(file.path);
838
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.contentBase64)) {
839
+ throw new Error('Inline skill file encoding is invalid.');
840
+ }
841
+ const content = Buffer.from(file.contentBase64, 'base64');
842
+ totalBytes += content.length;
843
+ if (totalBytes > 1_048_576)
844
+ throw new Error('Inline skill bundle exceeds the 1 MiB limit.');
845
+ const digest = `sha256:${createHash('sha256').update(content).digest('hex')}`;
846
+ if (digest !== file.sha256)
847
+ throw new Error(`Inline skill file hash mismatch: ${file.path}`);
848
+ const destination = containedInlinePath(skillRoot, file.path);
849
+ await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
850
+ await writeFile(destination, content, { mode: 0o600 });
851
+ }
852
+ }
853
+ return true;
854
+ }
855
+ async function skillSync(flags) {
856
+ const workspaceId = required(flags, 'workspace-id');
857
+ const providerValue = required(flags, 'provider');
858
+ if (!['codex', 'claude', 'agy'].includes(providerValue))
859
+ throw new Error('Skill provider must be codex, claude, or agy.');
860
+ const provider = providerValue;
861
+ const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
862
+ if (!workspace)
863
+ throw new Error('Skill workspace is not registered locally.');
864
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
865
+ const destination = nativeSkillDirectory(provider);
866
+ const fabric = await client();
867
+ const response = await fabric.pollSkill({ workspaceId, provider, installedBundleId: await getActiveSkillBundleId(destination) });
868
+ const rollout = response.rollout;
869
+ if (!rollout)
870
+ return { ok: true, rollout: null, changed: false };
871
+ if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
872
+ throw new Error('Skill rollout response is invalid.');
873
+ const bundle = rollout.bundle;
874
+ if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
875
+ || (bundle.operation === 'install' && bundle.skills.length === 0)
876
+ || (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
877
+ throw new Error('Skill bundle does not match local organization policy.');
878
+ }
879
+ const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
880
+ const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
881
+ if (bundle.operation === 'install') {
882
+ if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
883
+ throw new Error('Skill bundle must pin one full Git commit.');
884
+ if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
885
+ throw new Error('Skill bundle must pin one credential-free GitHub repository.');
886
+ }
887
+ }
888
+ const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
889
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
890
+ verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
891
+ await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
892
+ await rm(sourceRoot, { recursive: true, force: true });
893
+ await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
894
+ const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
895
+ if (bundle.operation === 'install' && !materializedInline) {
896
+ await rm(sourceRoot, { recursive: true, force: true });
897
+ await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
898
+ await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
899
+ await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
900
+ }
901
+ try {
902
+ const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
903
+ const receipt = await installSkillBundle({
904
+ bundle,
905
+ sourceDirectory: sourceRoot,
906
+ nativeSkillDirectory: destination,
907
+ policy,
908
+ serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
909
+ devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
910
+ deviceId: config.deviceId,
911
+ workspaceId,
912
+ provider,
913
+ smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
914
+ organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
915
+ });
916
+ if (provider === 'agy' && receipt.status === 'active')
917
+ await activateAgyPlugin();
918
+ await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
919
+ return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
920
+ }
921
+ finally {
922
+ await rm(sourceRoot, { recursive: true, force: true });
923
+ }
924
+ }
925
+ async function relayStart(flags) {
926
+ const policy = await loadOrganizationPolicy(required(flags, 'policy'));
927
+ const fabric = await client();
928
+ const leaseSeconds = Number(flags.get('lease-seconds') || 120);
929
+ const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
930
+ const pidPath = resolve(dharmaHome(), 'relay', 'relay.pid');
931
+ await mkdir(resolve(dharmaHome(), 'relay'), { recursive: true, mode: 0o700 });
932
+ await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
933
+ let stopping = false;
934
+ const stop = () => { stopping = true; };
935
+ process.once('SIGINT', stop);
936
+ process.once('SIGTERM', stop);
937
+ let tasksCompleted = 0;
938
+ let evidenceResponsesCompleted = 0;
939
+ try {
940
+ do {
941
+ const evidence = await processEvidenceRequest(fabric, policy);
942
+ if (evidence.requestId)
943
+ evidenceResponsesCompleted += 1;
944
+ const result = await executeOneTask(fabric, policy, leaseSeconds);
945
+ if (result.taskId)
946
+ tasksCompleted += 1;
947
+ if (flags.has('once'))
948
+ break;
949
+ if (!result.taskId && !evidence.requestId)
950
+ await new Promise((accept) => setTimeout(accept, pollMs));
951
+ } while (!stopping);
952
+ }
953
+ finally {
954
+ process.removeListener('SIGINT', stop);
955
+ process.removeListener('SIGTERM', stop);
956
+ await rm(pidPath, { force: true });
957
+ }
958
+ return { ok: true, stopped: true, tasksCompleted, evidenceResponsesCompleted };
959
+ }
960
+ export async function run(argv) {
961
+ const { positional, flags } = options(argv);
962
+ const [command, subcommand] = positional;
963
+ if (flags.has('version') || command === 'version')
964
+ return { version: VERSION };
965
+ if (command === 'onboard')
966
+ return onboard(flags);
967
+ if (command === 'login')
968
+ return login(flags);
969
+ if (command === 'providers' && subcommand === 'list')
970
+ return { providers: await Promise.all(providerAdapters.map((adapter) => adapter.capability())) };
971
+ if (command === 'workspace' && subcommand === 'add')
972
+ return workspaceAdd(flags, positional.slice(2));
973
+ if (command === 'workspace' && subcommand === 'sync')
974
+ return workspaceSync(flags, positional.slice(2));
975
+ if (command === 'capture' || (command === 'evidence' && subcommand === 'capture'))
976
+ return capture(flags);
977
+ if (command === 'evidence' && subcommand === 'capture-batch')
978
+ return capture(flags, true);
979
+ if (command === 'evidence' && subcommand === 'preview')
980
+ return evidencePreview(flags);
981
+ if (command === 'evidence' && subcommand === 'sync')
982
+ return evidenceSync(flags);
983
+ if (command === 'evidence' && subcommand === 'run-request')
984
+ return runOneEvidenceRequest(flags);
985
+ if (command === 'status') {
986
+ try {
987
+ const config = JSON.parse(await readFile(configPath(), 'utf8'));
988
+ return { version: VERSION, home: dharmaHome(), enrolled: true, organizationId: config.organizationId, deviceId: config.deviceId, relay: 'on_demand' };
989
+ }
990
+ catch {
991
+ return { version: VERSION, home: dharmaHome(), enrolled: false, relay: 'stopped' };
992
+ }
993
+ }
994
+ if (command === 'tasks' && subcommand === 'run-once')
995
+ return runOneTask(flags);
996
+ if (command === 'relay' && subcommand === 'start')
997
+ return relayStart(flags);
998
+ if (command === 'tasks' && subcommand === 'list')
999
+ return { tasks: [], coverage: 'server_poll_requires_relay' };
1000
+ if (command === 'skills' && subcommand === 'sync')
1001
+ return skillSync(flags);
1002
+ if (command === 'skills' && subcommand === 'status') {
1003
+ const providerValue = required(flags, 'provider');
1004
+ if (!['codex', 'claude', 'agy'].includes(providerValue))
1005
+ throw new Error('Skill provider must be codex, claude, or agy.');
1006
+ const root = nativeSkillDirectory(providerValue);
1007
+ return { provider: providerValue, activeBundleId: await getActiveSkillBundleId(root), nativeSkillDirectory: root };
1008
+ }
1009
+ throw new Error('Usage: dharma <onboard|login|status|providers list|workspace add|workspace sync|evidence preview|evidence capture|evidence capture-batch|evidence sync|evidence run-request|relay start|tasks run-once|skills sync|skills status> [options]');
1010
+ }
1011
+ if (isDirectExecution(process.argv[1], import.meta.url)) {
1012
+ run(process.argv.slice(2)).then(print).catch((error) => {
1013
+ const message = error instanceof Error ? error.message : String(error);
1014
+ process.stderr.write(`${message}\n`);
1015
+ process.exitCode = 2;
1016
+ });
1017
+ }
1018
+ //# sourceMappingURL=index.js.map