@dharma-ai-labs/agent-fabric 0.1.26 → 0.2.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/README.md +22 -3
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +731 -123
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +214 -1
- package/dist/index.test.js.map +1 -1
- package/dist/schemas/skill-bundle.schema.json +67 -4
- package/dist/schemas/trajectory-capsule-v2.schema.json +427 -0
- package/dist/schemas/trajectory-capsule-v3.schema.json +568 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -8,15 +8,15 @@ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { canonicalize, sha256, validateContract, verifyCanonicalObject } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
11
|
-
import { buildTrajectoryCapsule, redactValue, referencesExcludedPath } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
11
|
+
import { buildTrajectoryCapsule, redactValue, referencesExcludedPath, trajectoryCapsuleHash } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
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
|
-
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
14
|
+
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, deleteActiveSkillAuthorizationAnchor, loadActiveSkillAuthorizationAnchor, loadDeviceEnrollmentAnchor, saveActiveSkillAuthorizationAnchor, isDefinitiveAgentFabricRejection, saveDeviceConfig, saveDeviceEnrollmentAnchor, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
15
15
|
import { AgentFabricClient as AgentFabricApiClient } from '@dharma-ai-labs/agent-fabric-sdk';
|
|
16
|
-
import {
|
|
16
|
+
import { getActiveSkillBundleAuthorization, getExpiredSkillBundleAuthorizationForReplacement, getLegacySkillBundleIdForUpgrade, installSkillBundle, rollbackUnconfirmedSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
|
|
17
17
|
import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
|
|
18
18
|
import { CLI_USAGE } from './usage.js';
|
|
19
|
-
const VERSION = '0.
|
|
19
|
+
const VERSION = '0.2.0';
|
|
20
20
|
const USAGE = CLI_USAGE;
|
|
21
21
|
const execFileAsync = promisify(execFile);
|
|
22
22
|
export function parseCliOptions(args) {
|
|
@@ -133,6 +133,21 @@ function evidenceLedgerForPolicyActivation(value, day) {
|
|
|
133
133
|
throw new Error('Evidence upload ledger is invalid.');
|
|
134
134
|
return newEvidenceUploadLedger(day);
|
|
135
135
|
}
|
|
136
|
+
export async function pathExistsOrThrow(path, check = access) {
|
|
137
|
+
try {
|
|
138
|
+
await check(path);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code === 'ENOENT')
|
|
143
|
+
return false;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function canonicalFilesystemPath(path) {
|
|
148
|
+
return realpath(path);
|
|
149
|
+
}
|
|
150
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
136
151
|
async function pathExists(path) {
|
|
137
152
|
try {
|
|
138
153
|
await access(path);
|
|
@@ -223,7 +238,11 @@ async function refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspa
|
|
|
223
238
|
if (!item)
|
|
224
239
|
throw new Error('Content transmission workspace is not registered locally.');
|
|
225
240
|
const canonicalPolicyPath = resolve(item.path, '.dharma', 'approved-policy.json');
|
|
226
|
-
|
|
241
|
+
const [providedIdentity, registeredIdentity] = await Promise.all([
|
|
242
|
+
canonicalFilesystemPath(resolve(policyPath)),
|
|
243
|
+
canonicalFilesystemPath(canonicalPolicyPath),
|
|
244
|
+
]);
|
|
245
|
+
if (providedIdentity !== registeredIdentity) {
|
|
227
246
|
throw new Error('Content transmission requires the canonical registered workspace policy path.');
|
|
228
247
|
}
|
|
229
248
|
const current = await loadOrganizationPolicy(policyPath);
|
|
@@ -236,11 +255,21 @@ async function refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspa
|
|
|
236
255
|
}
|
|
237
256
|
export function assertCapsuleIntegrity(capsule) {
|
|
238
257
|
const capsuleHash = String(capsule.capsuleHash || '');
|
|
239
|
-
|
|
240
|
-
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash) || sha256(canonicalize(unsigned)) !== capsuleHash) {
|
|
258
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash) || trajectoryCapsuleHash(capsule) !== capsuleHash) {
|
|
241
259
|
throw new Error('Trajectory capsule integrity check failed.');
|
|
242
260
|
}
|
|
243
261
|
}
|
|
262
|
+
function trajectoryCapsuleSchemaId(capsule) {
|
|
263
|
+
const schema = capsule && typeof capsule === 'object' && !Array.isArray(capsule)
|
|
264
|
+
? capsule.schema : null;
|
|
265
|
+
if (schema === 'dharma.trajectory-capsule/v1')
|
|
266
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v1';
|
|
267
|
+
if (schema === 'dharma.trajectory-capsule/v2')
|
|
268
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v2';
|
|
269
|
+
if (schema === 'dharma.trajectory-capsule/v3')
|
|
270
|
+
return 'https://schemas.dharma-ai.io/trajectory-capsule/v3';
|
|
271
|
+
throw new Error('Trajectory capsule schema is unsupported.');
|
|
272
|
+
}
|
|
244
273
|
export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
245
274
|
assertPolicy(policy);
|
|
246
275
|
const mode = capsule.automaticDisclosureMode;
|
|
@@ -256,6 +285,10 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
256
285
|
const events = Array.isArray(capsule.events) ? capsule.events : [];
|
|
257
286
|
if (mode !== 'customer_authorized_content') {
|
|
258
287
|
const provider = String(capsule.provider || '');
|
|
288
|
+
const capsuleSchema = String(capsule.schema || '');
|
|
289
|
+
const captureProvenance = capsule.captureProvenance && typeof capsule.captureProvenance === 'object'
|
|
290
|
+
&& !Array.isArray(capsule.captureProvenance)
|
|
291
|
+
? capsule.captureProvenance : {};
|
|
259
292
|
const fixedEventKinds = new Set([
|
|
260
293
|
'user_message', 'agent_message', 'tool_call', 'tool_result', 'command', 'file_read', 'file_write',
|
|
261
294
|
'git', 'validation', 'permission', 'subagent', 'error', 'retry', 'session_state', 'metadata', 'collapsed_output',
|
|
@@ -282,9 +315,19 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
282
315
|
const coverage = capsule.coverage && typeof capsule.coverage === 'object' && !Array.isArray(capsule.coverage)
|
|
283
316
|
? capsule.coverage : {};
|
|
284
317
|
const missingFields = Array.isArray(coverage.missingFields) ? coverage.missingFields : null;
|
|
318
|
+
const signedTaskCapture = capsuleSchema === 'dharma.trajectory-capsule/v3'
|
|
319
|
+
&& captureProvenance.sourceClass === 'signed_task_execution';
|
|
320
|
+
const signedTaskBundleIds = [...new Set(events.map((event) => (event && typeof event === 'object' && !Array.isArray(event)
|
|
321
|
+
? String(event.skillBundleId || '') : '')).filter(Boolean))];
|
|
285
322
|
if (!['codex', 'claude', 'agy'].includes(provider)
|
|
286
323
|
|| !digest.test(String(capsule.sessionId || ''))
|
|
287
|
-
||
|
|
324
|
+
|| (signedTaskCapture
|
|
325
|
+
? !uuid.test(String(capsule.taskId || ''))
|
|
326
|
+
|| !digest.test(String(captureProvenance.taskReceiptHash || ''))
|
|
327
|
+
|| !Number.isFinite(Date.parse(String(captureProvenance.collectedAt || '')))
|
|
328
|
+
|| signedTaskBundleIds.length !== 1
|
|
329
|
+
|| !uuid.test(signedTaskBundleIds[0] || '')
|
|
330
|
+
: capsule.taskId !== null || capsuleSchema === 'dharma.trajectory-capsule/v3')
|
|
288
331
|
|| !uuid.test(String(capsule.deviceId || ''))
|
|
289
332
|
|| capsule.redactionReceipt === null
|
|
290
333
|
|| receipt.policyRevision !== policy.revision
|
|
@@ -358,7 +401,8 @@ export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
|
358
401
|
|| !Array.isArray(eventRecord.contentRefs) || eventRecord.contentRefs.length !== 0
|
|
359
402
|
|| source.nativeEventId !== null || source.localLocatorId !== null || source.sourceKind !== eventKind
|
|
360
403
|
|| record.nativeKind !== eventKind
|
|
361
|
-
|| eventRecord.skillBundleId !==
|
|
404
|
+
|| eventRecord.skillBundleId !== (signedTaskCapture ? signedTaskBundleIds[0] : null)
|
|
405
|
+
|| eventRecord.providerModel !== null) {
|
|
362
406
|
throw new Error('Reduced trajectory capsule contains unauthorized event descriptors.');
|
|
363
407
|
}
|
|
364
408
|
}
|
|
@@ -694,6 +738,13 @@ export async function withWorkspacePolicyRefreshLock(workspaceId, operation) {
|
|
|
694
738
|
throw new Error('Content transmission requires a registered workspace ID.');
|
|
695
739
|
return withFileLock(`${workspaceAuthorizationStatePath(workspaceId)}.refresh.lock`, operation, 'Timed out waiting for the workspace policy refresh lock.');
|
|
696
740
|
}
|
|
741
|
+
export async function withWorkspaceSkillActivationLock(workspaceId, provider, operation) {
|
|
742
|
+
if (!workspaceId || !['codex', 'claude', 'agy'].includes(provider)) {
|
|
743
|
+
throw new Error('Skill activation lock requires a registered workspace and supported provider.');
|
|
744
|
+
}
|
|
745
|
+
const key = createHash('sha256').update(`${workspaceId}:${provider}`).digest('hex');
|
|
746
|
+
return withFileLock(resolve(dharmaHome(), 'registry', 'skill-activation-locks', `${key}.lock`), operation, 'Timed out waiting for the workspace skill activation lock.');
|
|
747
|
+
}
|
|
697
748
|
async function assertWorkspaceAuthorizationCurrent(workspaceId, authorization, requireExisting = true) {
|
|
698
749
|
const statePath = workspaceAuthorizationStatePath(workspaceId);
|
|
699
750
|
let previous = null;
|
|
@@ -919,6 +970,8 @@ function responseTextFromEvent(value) {
|
|
|
919
970
|
return item.text;
|
|
920
971
|
if (event.type === 'result' && typeof event.result === 'string')
|
|
921
972
|
return event.result;
|
|
973
|
+
if (event.status === 'SUCCESS' && typeof event.response === 'string')
|
|
974
|
+
return event.response;
|
|
922
975
|
const message = event.message && typeof event.message === 'object' && !Array.isArray(event.message)
|
|
923
976
|
? event.message
|
|
924
977
|
: {};
|
|
@@ -1095,16 +1148,36 @@ async function runOrganizationCommand(command, subcommand, flags) {
|
|
|
1095
1148
|
if (command === 'remediations' && subcommand === 'list')
|
|
1096
1149
|
return api.listRemediations();
|
|
1097
1150
|
if (command === 'remediations' && subcommand === 'act') {
|
|
1098
|
-
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1099
1151
|
const targetId = required(flags, 'target-id');
|
|
1100
1152
|
const body = await commandJsonBody(flags);
|
|
1101
1153
|
const action = String(flags.get('action') || body.action || '');
|
|
1102
|
-
if (!['run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1103
|
-
throw new Error('Remediation action must be run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1154
|
+
if (!['stage_evaluation', 'run_backtest', 'link_backtest', 'approve', 'merge_pr', 'release', 'expand', 'rollback'].includes(action)) {
|
|
1155
|
+
throw new Error('Remediation action must be stage_evaluation, run_backtest, link_backtest, approve, merge_pr, release, expand, or rollback.');
|
|
1104
1156
|
}
|
|
1157
|
+
if (action === 'stage_evaluation') {
|
|
1158
|
+
const endpointId = String(flags.get('endpoint-id') || body.endpointId || '');
|
|
1159
|
+
if (!UUID_PATTERN.test(endpointId))
|
|
1160
|
+
throw new Error('stage_evaluation requires --endpoint-id with an exact endpoint UUID.');
|
|
1161
|
+
if (flags.get('dry-run') === true) {
|
|
1162
|
+
return {
|
|
1163
|
+
ok: true,
|
|
1164
|
+
planned: true,
|
|
1165
|
+
serverMutation: false,
|
|
1166
|
+
targetId,
|
|
1167
|
+
transition: { action, endpointId },
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
requireExplicitConfirmation(flags, 'Staging a repository remediation evaluation');
|
|
1171
|
+
return api.transitionRemediationTarget(targetId, { action, endpointId });
|
|
1172
|
+
}
|
|
1173
|
+
requireExplicitConfirmation(flags, 'Changing a repository remediation release');
|
|
1105
1174
|
return api.transitionRemediationTarget(targetId, {
|
|
1106
|
-
...body,
|
|
1107
1175
|
action: action,
|
|
1176
|
+
...(Array.isArray(body.trajectoryIds) ? { trajectoryIds: body.trajectoryIds.map(String) } : {}),
|
|
1177
|
+
...(typeof body.campaignId === 'string' ? { campaignId: body.campaignId } : {}),
|
|
1178
|
+
...(typeof body.establishAutoUpdatePolicy === 'boolean'
|
|
1179
|
+
? { establishAutoUpdatePolicy: body.establishAutoUpdatePolicy }
|
|
1180
|
+
: {}),
|
|
1108
1181
|
});
|
|
1109
1182
|
}
|
|
1110
1183
|
if (command === 'skills' && subcommand === 'list')
|
|
@@ -1196,6 +1269,7 @@ async function login(flags) {
|
|
|
1196
1269
|
serverPublicKeyEd25519: result.serverPublicKeyEd25519, relayUrl: result.relayUrl, enrolledAt: new Date().toISOString(),
|
|
1197
1270
|
};
|
|
1198
1271
|
await saveDeviceConfig(configPath(), config);
|
|
1272
|
+
await saveDeviceEnrollmentAnchor({ config });
|
|
1199
1273
|
await rm(pendingEnrollmentPath(), { force: true });
|
|
1200
1274
|
return { ok: true, status: 'approved', deviceId: config.deviceId, organizationId: pending.organizationId, relayUrl: config.relayUrl };
|
|
1201
1275
|
}
|
|
@@ -1242,6 +1316,11 @@ async function capture(flags, batch = false) {
|
|
|
1242
1316
|
const registered = (await registry()).find((item) => item.path === workspace);
|
|
1243
1317
|
if (!registered)
|
|
1244
1318
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
1319
|
+
const captureProvenance = (collectedAt) => ({
|
|
1320
|
+
sourceClass: (typeof root === 'string' ? 'explicit_import' : 'provider_discovery'),
|
|
1321
|
+
collectedAt,
|
|
1322
|
+
taskReceiptHash: null,
|
|
1323
|
+
});
|
|
1245
1324
|
let policy = await loadVerifiedWorkspacePolicy(policyPath, registered.workspaceId);
|
|
1246
1325
|
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
1247
1326
|
const fabric = flags.has('sync') ? await client() : null;
|
|
@@ -1263,6 +1342,7 @@ async function capture(flags, batch = false) {
|
|
|
1263
1342
|
const firstRevision = buildTrajectoryCapsule({
|
|
1264
1343
|
organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
|
|
1265
1344
|
session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
|
|
1345
|
+
captureProvenance: captureProvenance(selected.endedAt),
|
|
1266
1346
|
});
|
|
1267
1347
|
const latestMetadata = vault.getLatestCapsuleMetadata(firstRevision.trajectoryId);
|
|
1268
1348
|
const latestCapsule = latestMetadata
|
|
@@ -1279,9 +1359,10 @@ async function capture(flags, batch = false) {
|
|
|
1279
1359
|
organizationId: device.organizationId, deviceId: device.deviceId, workspaceId: registered.workspaceId,
|
|
1280
1360
|
session: selected, policy, rawContentId, rawBytes: rawTurn.byteLength, rawKind: 'raw-provider-turn',
|
|
1281
1361
|
revision: latestMetadata.revision + 1, previousRevisionHash: latestMetadata.capsuleHash,
|
|
1362
|
+
captureProvenance: captureProvenance(selected.endedAt),
|
|
1282
1363
|
})
|
|
1283
1364
|
: firstRevision;
|
|
1284
|
-
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'),
|
|
1365
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
1285
1366
|
if (!validation.ok)
|
|
1286
1367
|
throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
1287
1368
|
if (!latestCapsule || latestCapsule.capsuleHash !== capsule.capsuleHash) {
|
|
@@ -1369,6 +1450,11 @@ async function evidencePreview(flags) {
|
|
|
1369
1450
|
if (!registered)
|
|
1370
1451
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
1371
1452
|
const policy = await loadVerifiedWorkspacePolicy(policyPath, registered?.workspaceId);
|
|
1453
|
+
const captureProvenance = (collectedAt) => ({
|
|
1454
|
+
sourceClass: (typeof root === 'string' ? 'explicit_import' : 'provider_discovery'),
|
|
1455
|
+
collectedAt,
|
|
1456
|
+
taskReceiptHash: null,
|
|
1457
|
+
});
|
|
1372
1458
|
const capsules = sessions.map((session) => {
|
|
1373
1459
|
const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
1374
1460
|
return buildTrajectoryCapsule({
|
|
@@ -1380,6 +1466,7 @@ async function evidencePreview(flags) {
|
|
|
1380
1466
|
rawContentId: sha256(rawTurn),
|
|
1381
1467
|
rawBytes: rawTurn.byteLength,
|
|
1382
1468
|
rawKind: 'raw-provider-turn',
|
|
1469
|
+
captureProvenance: captureProvenance(session.endedAt),
|
|
1383
1470
|
});
|
|
1384
1471
|
});
|
|
1385
1472
|
automaticDisclosure = {
|
|
@@ -1690,6 +1777,73 @@ async function readDeviceConfig() {
|
|
|
1690
1777
|
return null;
|
|
1691
1778
|
}
|
|
1692
1779
|
}
|
|
1780
|
+
async function activeSkillAuthorization(provider, workspaceId, organizationAgentId, config) {
|
|
1781
|
+
const root = nativeSkillDirectory(provider);
|
|
1782
|
+
const pointer = resolve(root, '.dharma-managed', 'workspaces', workspaceId, 'ACTIVE_BUNDLE');
|
|
1783
|
+
if (!await pathExistsOrThrow(pointer))
|
|
1784
|
+
return null;
|
|
1785
|
+
if (!organizationAgentId)
|
|
1786
|
+
throw new Error('Workspace is not bound to a repository agent. Run dharma workspace sync.');
|
|
1787
|
+
const [identity, enrollment, active] = await Promise.all([
|
|
1788
|
+
loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId }),
|
|
1789
|
+
loadDeviceEnrollmentAnchor({ config }),
|
|
1790
|
+
loadActiveSkillAuthorizationAnchor({ config, workspaceId, organizationAgentId, provider }),
|
|
1791
|
+
]);
|
|
1792
|
+
if (!active)
|
|
1793
|
+
throw new Error('Active skill state is not anchored in secure storage. Run dharma skill sync again.');
|
|
1794
|
+
if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519) {
|
|
1795
|
+
throw new Error('Active skill device identity does not match secure enrollment state.');
|
|
1796
|
+
}
|
|
1797
|
+
return getActiveSkillBundleAuthorization({
|
|
1798
|
+
nativeSkillDirectory: root,
|
|
1799
|
+
workspaceId,
|
|
1800
|
+
provider,
|
|
1801
|
+
organizationId: config.organizationId,
|
|
1802
|
+
organizationAgentId,
|
|
1803
|
+
deviceId: config.deviceId,
|
|
1804
|
+
serverPublicKey: createPublicKey({
|
|
1805
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 },
|
|
1806
|
+
format: 'jwk',
|
|
1807
|
+
}),
|
|
1808
|
+
devicePublicKey: createPublicKey({
|
|
1809
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 },
|
|
1810
|
+
format: 'jwk',
|
|
1811
|
+
}),
|
|
1812
|
+
expectedReceiptHash: active.receiptHash,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
async function expiredSkillAuthorizationForReplacement(provider, workspaceId, organizationAgentId, config) {
|
|
1816
|
+
const root = nativeSkillDirectory(provider);
|
|
1817
|
+
if (!organizationAgentId)
|
|
1818
|
+
throw new Error('Workspace is not bound to a repository agent. Run dharma workspace sync.');
|
|
1819
|
+
const [identity, enrollment, active] = await Promise.all([
|
|
1820
|
+
loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId }),
|
|
1821
|
+
loadDeviceEnrollmentAnchor({ config }),
|
|
1822
|
+
loadActiveSkillAuthorizationAnchor({ config, workspaceId, organizationAgentId, provider }),
|
|
1823
|
+
]);
|
|
1824
|
+
if (!active)
|
|
1825
|
+
throw new Error('Active skill state is not anchored in secure storage. Run dharma skill sync again.');
|
|
1826
|
+
if (identity.publicKeyEd25519 !== enrollment.devicePublicKeyEd25519) {
|
|
1827
|
+
throw new Error('Active skill device identity does not match secure enrollment state.');
|
|
1828
|
+
}
|
|
1829
|
+
return getExpiredSkillBundleAuthorizationForReplacement({
|
|
1830
|
+
nativeSkillDirectory: root,
|
|
1831
|
+
workspaceId,
|
|
1832
|
+
provider,
|
|
1833
|
+
organizationId: config.organizationId,
|
|
1834
|
+
organizationAgentId,
|
|
1835
|
+
deviceId: config.deviceId,
|
|
1836
|
+
serverPublicKey: createPublicKey({
|
|
1837
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: enrollment.serverPublicKeyEd25519 },
|
|
1838
|
+
format: 'jwk',
|
|
1839
|
+
}),
|
|
1840
|
+
devicePublicKey: createPublicKey({
|
|
1841
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: identity.publicKeyEd25519 },
|
|
1842
|
+
format: 'jwk',
|
|
1843
|
+
}),
|
|
1844
|
+
expectedReceiptHash: active.receiptHash,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1693
1847
|
async function loadVerifiedWorkspacePolicy(path, workspaceId) {
|
|
1694
1848
|
const policy = await loadOrganizationPolicy(path);
|
|
1695
1849
|
if (policy.evidence.automaticDisclosure?.mode !== 'customer_authorized_content')
|
|
@@ -1908,7 +2062,7 @@ async function onboard(flags) {
|
|
|
1908
2062
|
async function evidenceSync(flags) {
|
|
1909
2063
|
const capsule = JSON.parse(await readFile(resolve(required(flags, 'file')), 'utf8'));
|
|
1910
2064
|
const workspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : String(capsule.workspaceId || '');
|
|
1911
|
-
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'),
|
|
2065
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
1912
2066
|
if (!validation.ok)
|
|
1913
2067
|
throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
1914
2068
|
assertCapsuleIntegrity(capsule);
|
|
@@ -2123,7 +2277,279 @@ async function runOneEvidenceRequest(flags) {
|
|
|
2123
2277
|
const policy = await refreshVerifiedWorkspacePolicyForTransmission(required(flags, 'policy'), workspaceId, fabric);
|
|
2124
2278
|
return processEvidenceRequest(fabric, policy, workspaceId);
|
|
2125
2279
|
}
|
|
2126
|
-
|
|
2280
|
+
export function taskReceiptSession(task, receipt, workspace) {
|
|
2281
|
+
return {
|
|
2282
|
+
provider: task.target.provider,
|
|
2283
|
+
sessionId: `dharma-task-${task.taskId}`,
|
|
2284
|
+
sourcePath: 'dharma-task-receipt',
|
|
2285
|
+
workspace,
|
|
2286
|
+
coverage: 'observed',
|
|
2287
|
+
startedAt: receipt.startedAt,
|
|
2288
|
+
endedAt: receipt.completedAt,
|
|
2289
|
+
records: receipt.commandResults.map((result, index) => ({
|
|
2290
|
+
native: {
|
|
2291
|
+
taskId: task.taskId,
|
|
2292
|
+
commandId: result.commandId,
|
|
2293
|
+
exitCode: result.exitCode,
|
|
2294
|
+
signal: result.signal,
|
|
2295
|
+
timedOut: result.timedOut,
|
|
2296
|
+
stdout: result.stdout,
|
|
2297
|
+
stderr: result.stderr,
|
|
2298
|
+
stdoutSha256: result.stdoutSha256,
|
|
2299
|
+
stderrSha256: result.stderrSha256,
|
|
2300
|
+
},
|
|
2301
|
+
sourcePath: 'dharma-task-receipt',
|
|
2302
|
+
line: index + 1,
|
|
2303
|
+
workspace,
|
|
2304
|
+
timestamp: receipt.completedAt,
|
|
2305
|
+
kind: result.commandId.startsWith('provider.') ? 'agent_message' : 'validation',
|
|
2306
|
+
coverage: 'observed',
|
|
2307
|
+
})),
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
function prepareSignedTaskTrajectory(input) {
|
|
2311
|
+
if (!input.activeSkill || input.task.skillBundle?.bundleId !== input.activeSkill.bundleId) {
|
|
2312
|
+
throw new Error('Signed task trajectory requires the active task-pinned skill bundle.');
|
|
2313
|
+
}
|
|
2314
|
+
const session = taskReceiptSession(input.task, input.receipt, input.workspace.path);
|
|
2315
|
+
const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
2316
|
+
const rawContentId = sha256(rawTurn);
|
|
2317
|
+
const capsule = buildTrajectoryCapsule({
|
|
2318
|
+
organizationId: input.device.organizationId,
|
|
2319
|
+
deviceId: input.device.deviceId,
|
|
2320
|
+
workspaceId: input.workspace.workspaceId,
|
|
2321
|
+
session,
|
|
2322
|
+
policy: input.policy,
|
|
2323
|
+
rawContentId,
|
|
2324
|
+
rawBytes: rawTurn.byteLength,
|
|
2325
|
+
rawKind: 'raw-provider-turn',
|
|
2326
|
+
taskId: input.task.taskId,
|
|
2327
|
+
captureProvenance: {
|
|
2328
|
+
sourceClass: 'signed_task_execution',
|
|
2329
|
+
collectedAt: input.collectedAt,
|
|
2330
|
+
taskReceiptHash: input.taskReceiptHash,
|
|
2331
|
+
},
|
|
2332
|
+
activeSkillBundleId: input.activeSkill.bundleId,
|
|
2333
|
+
activeSkillBundleActivatedAt: input.activeSkill.activatedAt,
|
|
2334
|
+
activeSkillBundleExpiresAt: input.activeSkill.expiresAt,
|
|
2335
|
+
activeSkillBundleVerifiedAt: input.activeSkillVerifiedAt,
|
|
2336
|
+
});
|
|
2337
|
+
return { capsule, rawTurn, rawContentId, session };
|
|
2338
|
+
}
|
|
2339
|
+
async function syncSignedTaskTrajectory(input) {
|
|
2340
|
+
const { capsule, rawTurn, rawContentId, session } = input.prepared;
|
|
2341
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), trajectoryCapsuleSchemaId(capsule), capsule);
|
|
2342
|
+
if (!validation.ok)
|
|
2343
|
+
throw new Error(`Signed task capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
2344
|
+
assertCapsuleAuthorizedByCurrentPolicy(capsule, input.policy);
|
|
2345
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2346
|
+
const vault = await LocalVault.open({
|
|
2347
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2348
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2349
|
+
rawLocalDays: rawLocalRetentionDays(input.policy),
|
|
2350
|
+
});
|
|
2351
|
+
try {
|
|
2352
|
+
const existing = vault.getCapsuleMetadata(capsule.trajectoryId, capsule.revision);
|
|
2353
|
+
if (!existing) {
|
|
2354
|
+
await vault.commitCapture({
|
|
2355
|
+
raw: { plaintext: rawTurn, kind: 'raw-provider-turn', expectedContentId: rawContentId },
|
|
2356
|
+
capsule: {
|
|
2357
|
+
plaintext: Buffer.from(JSON.stringify(capsule)), trajectoryId: capsule.trajectoryId,
|
|
2358
|
+
revision: capsule.revision, capsuleHash: capsule.capsuleHash,
|
|
2359
|
+
},
|
|
2360
|
+
session: {
|
|
2361
|
+
sessionId: session.sessionId, provider: session.provider, workspaceId: input.workspace.workspaceId,
|
|
2362
|
+
sourceLocator: session.sourcePath, status: session.coverage, observedAt: session.endedAt,
|
|
2363
|
+
},
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
else if (existing.capsuleHash !== capsule.capsuleHash) {
|
|
2367
|
+
throw new Error('Signed task evidence already exists with different immutable content.');
|
|
2368
|
+
}
|
|
2369
|
+
vault.queueCapsuleSync(capsule.trajectoryId, capsule.revision);
|
|
2370
|
+
await reserveDailyContentUpload(capsule, input.policy);
|
|
2371
|
+
const synced = await input.fabric.syncTrajectory(capsule);
|
|
2372
|
+
vault.markCapsuleSynced(capsule.trajectoryId, capsule.revision);
|
|
2373
|
+
return synced;
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
vault.close();
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
export function assertRecoveredTaskWorkspacePolicy(input) {
|
|
2380
|
+
if (input.workspace.workspaceId !== input.recoveryWorkspaceId) {
|
|
2381
|
+
throw new Error('Recovered task completion does not match its registered workspace.');
|
|
2382
|
+
}
|
|
2383
|
+
if (input.policy.organizationId !== input.workspace.organizationId
|
|
2384
|
+
|| (input.policy.serverAuthorization
|
|
2385
|
+
&& input.policy.serverAuthorization.workspaceId !== input.workspace.workspaceId)) {
|
|
2386
|
+
throw new Error('Recovered task completion policy does not authorize its workspace.');
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
export function recoveredTaskPolicyWasSuperseded(capsule, currentPolicy) {
|
|
2390
|
+
return capsule.redactionReceipt?.policyRevision !== currentPolicy.revision;
|
|
2391
|
+
}
|
|
2392
|
+
export function assertTaskWorkspacePolicy(input) {
|
|
2393
|
+
if (input.task.workspaceId !== input.workspace.workspaceId
|
|
2394
|
+
|| input.task.organizationId !== input.workspace.organizationId
|
|
2395
|
+
|| input.policy.organizationId !== input.workspace.organizationId
|
|
2396
|
+
|| (input.policy.serverAuthorization
|
|
2397
|
+
&& input.policy.serverAuthorization.workspaceId !== input.workspace.workspaceId)) {
|
|
2398
|
+
throw new Error('Task workspace policy does not match the signed task and local registration.');
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async function stageSignedTaskTrajectoryRecovery(taskId, workspaceId, policy, prepared) {
|
|
2402
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2403
|
+
const vault = await LocalVault.open({
|
|
2404
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2405
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2406
|
+
rawLocalDays: rawLocalRetentionDays(policy),
|
|
2407
|
+
});
|
|
2408
|
+
try {
|
|
2409
|
+
const recovery = {
|
|
2410
|
+
schema: 'dharma.signed-task-trajectory-recovery/v1', taskId, workspaceId,
|
|
2411
|
+
prepared: {
|
|
2412
|
+
capsule: prepared.capsule,
|
|
2413
|
+
rawTurnBase64: prepared.rawTurn.toString('base64'),
|
|
2414
|
+
rawContentId: prepared.rawContentId,
|
|
2415
|
+
session: prepared.session,
|
|
2416
|
+
},
|
|
2417
|
+
};
|
|
2418
|
+
await vault.stageTaskCompletionRecovery(taskId, Buffer.from(JSON.stringify(recovery)));
|
|
2419
|
+
}
|
|
2420
|
+
finally {
|
|
2421
|
+
vault.close();
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
async function finalizeRecoveredSignedTaskTrajectories(fabric, vaultPolicy, onlyTaskId) {
|
|
2425
|
+
const completions = fabric.listRecoveredTaskCompletions()
|
|
2426
|
+
.filter((item) => !onlyTaskId || item.taskId === onlyTaskId);
|
|
2427
|
+
const finalized = [];
|
|
2428
|
+
for (const completion of completions) {
|
|
2429
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
2430
|
+
const vault = await LocalVault.open({
|
|
2431
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2432
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2433
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2434
|
+
});
|
|
2435
|
+
let recovery;
|
|
2436
|
+
try {
|
|
2437
|
+
recovery = await vault.getTaskCompletionRecovery(completion.taskId);
|
|
2438
|
+
}
|
|
2439
|
+
finally {
|
|
2440
|
+
vault.close();
|
|
2441
|
+
}
|
|
2442
|
+
if (!recovery || recovery.schema !== 'dharma.signed-task-trajectory-recovery/v1'
|
|
2443
|
+
|| recovery.taskId !== completion.taskId) {
|
|
2444
|
+
throw new Error(`Recovered task completion ${completion.taskId} is missing its encrypted local evidence.`);
|
|
2445
|
+
}
|
|
2446
|
+
const workspace = (await registry()).find((item) => item.workspaceId === recovery.workspaceId);
|
|
2447
|
+
if (!workspace)
|
|
2448
|
+
throw new Error(`Recovered task completion ${completion.taskId} has no registered workspace.`);
|
|
2449
|
+
const recoveryPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), recovery.workspaceId, fabric);
|
|
2450
|
+
assertRecoveredTaskWorkspacePolicy({
|
|
2451
|
+
recoveryWorkspaceId: recovery.workspaceId,
|
|
2452
|
+
workspace,
|
|
2453
|
+
policy: recoveryPolicy,
|
|
2454
|
+
});
|
|
2455
|
+
const rawTurn = Buffer.from(recovery.prepared.rawTurnBase64, 'base64');
|
|
2456
|
+
if (recovery.prepared.capsule.taskId !== completion.taskId
|
|
2457
|
+
|| recovery.prepared.capsule.workspaceId !== recovery.workspaceId
|
|
2458
|
+
|| recovery.prepared.capsule.capsuleHash !== completion.trajectoryCapsuleHash
|
|
2459
|
+
|| sha256(rawTurn) !== recovery.prepared.rawContentId) {
|
|
2460
|
+
throw new Error(`Recovered task completion ${completion.taskId} does not match its trajectory capsule.`);
|
|
2461
|
+
}
|
|
2462
|
+
const capsule = {
|
|
2463
|
+
...recovery.prepared.capsule,
|
|
2464
|
+
captureProvenance: {
|
|
2465
|
+
...recovery.prepared.capsule.captureProvenance,
|
|
2466
|
+
taskReceiptHash: completion.receiptHash,
|
|
2467
|
+
},
|
|
2468
|
+
};
|
|
2469
|
+
assertCapsuleIntegrity(capsule);
|
|
2470
|
+
if (recoveredTaskPolicyWasSuperseded(capsule, recoveryPolicy)) {
|
|
2471
|
+
const supersededVault = await LocalVault.open({
|
|
2472
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2473
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2474
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2475
|
+
});
|
|
2476
|
+
try {
|
|
2477
|
+
const existing = supersededVault.getCapsuleMetadata(capsule.trajectoryId, capsule.revision);
|
|
2478
|
+
if (!existing) {
|
|
2479
|
+
await supersededVault.commitCapture({
|
|
2480
|
+
raw: { plaintext: rawTurn, kind: 'raw-provider-turn', expectedContentId: recovery.prepared.rawContentId },
|
|
2481
|
+
capsule: {
|
|
2482
|
+
plaintext: Buffer.from(JSON.stringify(capsule)), trajectoryId: capsule.trajectoryId,
|
|
2483
|
+
revision: capsule.revision, capsuleHash: capsule.capsuleHash,
|
|
2484
|
+
},
|
|
2485
|
+
session: {
|
|
2486
|
+
sessionId: recovery.prepared.session.sessionId,
|
|
2487
|
+
provider: recovery.prepared.session.provider,
|
|
2488
|
+
workspaceId: recovery.workspaceId,
|
|
2489
|
+
sourceLocator: recovery.prepared.session.sourcePath,
|
|
2490
|
+
status: recovery.prepared.session.coverage,
|
|
2491
|
+
observedAt: recovery.prepared.session.endedAt,
|
|
2492
|
+
},
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
else if (existing.capsuleHash !== capsule.capsuleHash) {
|
|
2496
|
+
throw new Error('Recovered task evidence already exists with different immutable content.');
|
|
2497
|
+
}
|
|
2498
|
+
supersededVault.discardPendingCapsuleSync(capsule.trajectoryId, capsule.revision, 'policy_revision_superseded');
|
|
2499
|
+
}
|
|
2500
|
+
finally {
|
|
2501
|
+
supersededVault.close();
|
|
2502
|
+
}
|
|
2503
|
+
await fabric.acknowledgeRecoveredTaskCompletion(completion.taskId, completion.receiptHash);
|
|
2504
|
+
const acknowledgedVault = await LocalVault.open({
|
|
2505
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2506
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2507
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2508
|
+
});
|
|
2509
|
+
try {
|
|
2510
|
+
await acknowledgedVault.clearTaskCompletionRecovery(completion.taskId);
|
|
2511
|
+
}
|
|
2512
|
+
finally {
|
|
2513
|
+
acknowledgedVault.close();
|
|
2514
|
+
}
|
|
2515
|
+
finalized.push({
|
|
2516
|
+
taskId: completion.taskId,
|
|
2517
|
+
trajectory: {
|
|
2518
|
+
ok: false,
|
|
2519
|
+
status: 'withheld',
|
|
2520
|
+
reason: 'policy_revision_superseded',
|
|
2521
|
+
trajectoryId: capsule.trajectoryId,
|
|
2522
|
+
revision: capsule.revision,
|
|
2523
|
+
},
|
|
2524
|
+
});
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
const trajectory = await syncSignedTaskTrajectory({
|
|
2528
|
+
fabric, policy: recoveryPolicy, workspace,
|
|
2529
|
+
prepared: {
|
|
2530
|
+
capsule,
|
|
2531
|
+
rawTurn,
|
|
2532
|
+
rawContentId: recovery.prepared.rawContentId,
|
|
2533
|
+
session: recovery.prepared.session,
|
|
2534
|
+
},
|
|
2535
|
+
});
|
|
2536
|
+
await fabric.acknowledgeRecoveredTaskCompletion(completion.taskId, completion.receiptHash);
|
|
2537
|
+
const cleanupVault = await LocalVault.open({
|
|
2538
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
2539
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
2540
|
+
rawLocalDays: rawLocalRetentionDays(vaultPolicy),
|
|
2541
|
+
});
|
|
2542
|
+
try {
|
|
2543
|
+
await cleanupVault.clearTaskCompletionRecovery(completion.taskId);
|
|
2544
|
+
}
|
|
2545
|
+
finally {
|
|
2546
|
+
cleanupVault.close();
|
|
2547
|
+
}
|
|
2548
|
+
finalized.push({ taskId: completion.taskId, trajectory });
|
|
2549
|
+
}
|
|
2550
|
+
return finalized;
|
|
2551
|
+
}
|
|
2552
|
+
async function executeOneTask(fabric, leaseSeconds) {
|
|
2127
2553
|
const polled = await fabric.pollTask(leaseSeconds);
|
|
2128
2554
|
const taskRow = polled.task;
|
|
2129
2555
|
if (!taskRow?.envelope)
|
|
@@ -2132,54 +2558,113 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
|
2132
2558
|
const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
|
|
2133
2559
|
if (!workspace)
|
|
2134
2560
|
throw new Error('Task workspace is not registered on this device.');
|
|
2561
|
+
const taskPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), workspace.workspaceId, fabric);
|
|
2562
|
+
assertTaskWorkspacePolicy({ task, workspace, policy: taskPolicy });
|
|
2135
2563
|
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2564
|
+
const waitingHeartbeats = [];
|
|
2565
|
+
const waitingHeartbeat = setInterval(() => {
|
|
2566
|
+
waitingHeartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', {
|
|
2567
|
+
taskId: task.taskId,
|
|
2568
|
+
phase: 'waiting_for_skill_activation_lock',
|
|
2569
|
+
}).catch(() => undefined));
|
|
2570
|
+
}, Math.max(5_000, Math.floor(leaseSeconds * 500)));
|
|
2140
2571
|
try {
|
|
2141
|
-
|
|
2572
|
+
return await withWorkspaceSkillActivationLock(task.workspaceId, task.target.provider, async () => {
|
|
2573
|
+
if (task.target.deviceId !== config.deviceId)
|
|
2574
|
+
throw new Error('Task target does not match this enrolled device.');
|
|
2575
|
+
const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' });
|
|
2576
|
+
const activeSkill = await activeSkillAuthorization(task.target.provider, task.workspaceId, String(workspace.repositoryAgentId || ''), config);
|
|
2577
|
+
const activeBundleId = activeSkill?.bundleId ?? null;
|
|
2578
|
+
try {
|
|
2579
|
+
assertTaskSkillPin(task.skillBundle, activeBundleId);
|
|
2580
|
+
}
|
|
2581
|
+
catch (error) {
|
|
2582
|
+
await fabric.postTaskEvent(task.taskId, 'failed', {
|
|
2583
|
+
phase: 'preflight',
|
|
2584
|
+
code: taskSkillPinFailureCode(error),
|
|
2585
|
+
taskBundleId: task.skillBundle?.bundleId || null,
|
|
2586
|
+
localBundleId: activeBundleId,
|
|
2587
|
+
}).catch(() => undefined);
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
await fabric.postTaskEvent(task.taskId, 'started', {
|
|
2591
|
+
bundleId: task.skillBundle?.bundleId || null,
|
|
2592
|
+
bundleHash: task.skillBundle?.bundleHash || null,
|
|
2593
|
+
});
|
|
2594
|
+
const heartbeats = [];
|
|
2595
|
+
const heartbeat = setInterval(() => {
|
|
2596
|
+
heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
|
|
2597
|
+
}, Math.max(15_000, Math.floor(leaseSeconds * 500)));
|
|
2598
|
+
let receipt;
|
|
2599
|
+
try {
|
|
2600
|
+
receipt = await executeTask({
|
|
2601
|
+
task, policy: taskPolicy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
|
|
2602
|
+
receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
finally {
|
|
2606
|
+
clearInterval(heartbeat);
|
|
2607
|
+
await Promise.allSettled(heartbeats);
|
|
2608
|
+
}
|
|
2609
|
+
const summary = {
|
|
2610
|
+
status: receipt.status, branch: receipt.branch,
|
|
2611
|
+
response: taskResponsePreview(receipt),
|
|
2612
|
+
commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
|
|
2613
|
+
startedAt: receipt.startedAt, completedAt: receipt.completedAt,
|
|
2614
|
+
};
|
|
2615
|
+
const collectedAt = receipt.completedAt;
|
|
2616
|
+
const prepared = receipt.status === 'completed' && task.skillBundle
|
|
2617
|
+
? prepareSignedTaskTrajectory({
|
|
2618
|
+
policy: taskPolicy, task, receipt, taskReceiptHash: `sha256:${'0'.repeat(64)}`, collectedAt,
|
|
2619
|
+
workspace, device: config, activeSkill,
|
|
2620
|
+
// The receipt is durable and retry-idempotent. Its start time is after
|
|
2621
|
+
// the first successful local bundle verification, so it is also the
|
|
2622
|
+
// stable verification timestamp for a recovered trajectory.
|
|
2623
|
+
activeSkillVerifiedAt: receipt.startedAt,
|
|
2624
|
+
})
|
|
2625
|
+
: null;
|
|
2626
|
+
if (prepared)
|
|
2627
|
+
await stageSignedTaskTrajectoryRecovery(task.taskId, workspace.workspaceId, taskPolicy, prepared);
|
|
2628
|
+
await fabric.postTaskEvent(task.taskId, receipt.status, {
|
|
2629
|
+
...summary,
|
|
2630
|
+
...(prepared ? { trajectoryCapsuleHash: prepared.capsule.capsuleHash } : {}),
|
|
2631
|
+
});
|
|
2632
|
+
let trajectory = null;
|
|
2633
|
+
if (receipt.status === 'completed' && task.skillBundle) {
|
|
2634
|
+
const finalized = await finalizeRecoveredSignedTaskTrajectories(fabric, taskPolicy, task.taskId);
|
|
2635
|
+
trajectory = finalized[0]?.trajectory || null;
|
|
2636
|
+
if (!trajectory)
|
|
2637
|
+
throw new Error('Task completion did not return a recoverable server receipt.');
|
|
2638
|
+
}
|
|
2639
|
+
return { ok: true, taskId: task.taskId, receipt: summary, trajectory };
|
|
2640
|
+
});
|
|
2142
2641
|
}
|
|
2143
2642
|
catch (error) {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
}
|
|
2643
|
+
if (error instanceof Error && error.message === 'Timed out waiting for the workspace skill activation lock.') {
|
|
2644
|
+
await fabric.postTaskEvent(task.taskId, 'failed', {
|
|
2645
|
+
phase: 'coordination',
|
|
2646
|
+
code: 'skill_activation_lock_timeout',
|
|
2647
|
+
}).catch(() => undefined);
|
|
2648
|
+
}
|
|
2150
2649
|
throw error;
|
|
2151
2650
|
}
|
|
2152
|
-
await fabric.postTaskEvent(task.taskId, 'started', {
|
|
2153
|
-
bundleId: task.skillBundle?.bundleId || null,
|
|
2154
|
-
bundleHash: task.skillBundle?.bundleHash || null,
|
|
2155
|
-
});
|
|
2156
|
-
const heartbeats = [];
|
|
2157
|
-
const heartbeat = setInterval(() => {
|
|
2158
|
-
heartbeats.push(fabric.postTaskEvent(task.taskId, 'lease_extended', { taskId: task.taskId }).catch(() => undefined));
|
|
2159
|
-
}, Math.max(15_000, Math.floor(leaseSeconds * 500)));
|
|
2160
|
-
let receipt;
|
|
2161
|
-
try {
|
|
2162
|
-
receipt = await executeTask({
|
|
2163
|
-
task, policy, workspace: workspace.path, relayStateDirectory: resolve(dharmaHome(), 'relay'), serverPublicKey,
|
|
2164
|
-
receiptStore: new FileTaskReceiptStore(resolve(dharmaHome(), 'relay', 'receipts')),
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
2651
|
finally {
|
|
2168
|
-
clearInterval(
|
|
2169
|
-
await Promise.allSettled(
|
|
2170
|
-
}
|
|
2171
|
-
const summary = {
|
|
2172
|
-
status: receipt.status, branch: receipt.branch,
|
|
2173
|
-
response: taskResponsePreview(receipt),
|
|
2174
|
-
commandResults: receipt.commandResults.map(({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 }) => ({ commandId, exitCode, signal, timedOut, stdoutSha256, stderrSha256 })),
|
|
2175
|
-
startedAt: receipt.startedAt, completedAt: receipt.completedAt,
|
|
2176
|
-
};
|
|
2177
|
-
await fabric.postTaskEvent(task.taskId, receipt.status, summary);
|
|
2178
|
-
return { ok: true, taskId: task.taskId, receipt: summary };
|
|
2652
|
+
clearInterval(waitingHeartbeat);
|
|
2653
|
+
await Promise.allSettled(waitingHeartbeats);
|
|
2654
|
+
}
|
|
2179
2655
|
}
|
|
2180
2656
|
async function runOneTask(flags) {
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2657
|
+
const policyPath = resolve(required(flags, 'policy'));
|
|
2658
|
+
const requestedWorkspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined;
|
|
2659
|
+
const selectedWorkspace = (await registry()).find((item) => (resolve(item.path, '.dharma', 'approved-policy.json') === policyPath
|
|
2660
|
+
&& (!requestedWorkspaceId || item.workspaceId === requestedWorkspaceId)));
|
|
2661
|
+
if (!selectedWorkspace)
|
|
2662
|
+
throw new Error('Task policy must be the canonical policy of one registered workspace.');
|
|
2663
|
+
const policy = await loadVerifiedWorkspacePolicy(policyPath, selectedWorkspace.workspaceId);
|
|
2664
|
+
const fabric = await client();
|
|
2665
|
+
const recoveredTaskTrajectories = await finalizeRecoveredSignedTaskTrajectories(fabric, policy);
|
|
2666
|
+
const result = await executeOneTask(fabric, Number(flags.get('lease-seconds') || 120));
|
|
2667
|
+
return recoveredTaskTrajectories.length ? { ...result, recoveredTaskTrajectories } : result;
|
|
2183
2668
|
}
|
|
2184
2669
|
export function nativeSkillDirectory(provider, env = process.env, home = homedir()) {
|
|
2185
2670
|
if (provider === 'codex')
|
|
@@ -2270,12 +2755,19 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
2270
2755
|
const repositoryInstalled = await pathExists(repositorySkillPath) && await pathExists(connectionPath);
|
|
2271
2756
|
const nativeInstalled = await pathExists(nativeSkillPath) && await pathExists(nativeMarkerPath);
|
|
2272
2757
|
let workspaceId;
|
|
2758
|
+
let organizationAgentId;
|
|
2273
2759
|
try {
|
|
2274
2760
|
const connection = JSON.parse(await readFile(connectionPath, 'utf8'));
|
|
2275
|
-
if (typeof connection.workspaceId === 'string')
|
|
2761
|
+
if (typeof connection.workspaceId === 'string') {
|
|
2276
2762
|
workspaceId = connection.workspaceId;
|
|
2763
|
+
organizationAgentId = (await registry()).find((item) => item.workspaceId === workspaceId)?.repositoryAgentId || undefined;
|
|
2764
|
+
}
|
|
2277
2765
|
}
|
|
2278
2766
|
catch { }
|
|
2767
|
+
const config = await readDeviceConfig();
|
|
2768
|
+
const activeBundleId = workspaceId && organizationAgentId && config
|
|
2769
|
+
? (await activeSkillAuthorization(input.provider, workspaceId, organizationAgentId, config))?.bundleId ?? null
|
|
2770
|
+
: null;
|
|
2279
2771
|
return {
|
|
2280
2772
|
provider: input.provider,
|
|
2281
2773
|
ready: repositoryInstalled && nativeInstalled,
|
|
@@ -2285,7 +2777,7 @@ export async function verifyAgentFabricSkillInstallation(input) {
|
|
|
2285
2777
|
connectionPath,
|
|
2286
2778
|
nativeSkillPath,
|
|
2287
2779
|
workspaceId: workspaceId || null,
|
|
2288
|
-
activeBundleId
|
|
2780
|
+
activeBundleId,
|
|
2289
2781
|
activation: 'next_session',
|
|
2290
2782
|
nextAction: repositoryInstalled && nativeInstalled
|
|
2291
2783
|
? `Start a new ${input.provider} session from ${workspace} and invoke the dharma-agent-fabric skill.`
|
|
@@ -2357,79 +2849,177 @@ export async function materializeInlineSkillFiles(bundle, sourceRoot) {
|
|
|
2357
2849
|
}
|
|
2358
2850
|
return true;
|
|
2359
2851
|
}
|
|
2852
|
+
export async function recoverLegacySkillBundleIdAfterAuthorizationFailure(input) {
|
|
2853
|
+
const legacyBundleId = await getLegacySkillBundleIdForUpgrade({
|
|
2854
|
+
nativeSkillDirectory: input.nativeSkillDirectory,
|
|
2855
|
+
workspaceId: input.workspaceId,
|
|
2856
|
+
});
|
|
2857
|
+
if (!legacyBundleId)
|
|
2858
|
+
throw input.authorizationError;
|
|
2859
|
+
return legacyBundleId;
|
|
2860
|
+
}
|
|
2360
2861
|
async function skillSync(flags) {
|
|
2361
2862
|
const workspaceId = required(flags, 'workspace-id');
|
|
2362
2863
|
const providerValue = required(flags, 'provider');
|
|
2363
2864
|
if (!['codex', 'claude', 'agy'].includes(providerValue))
|
|
2364
2865
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2365
2866
|
const provider = providerValue;
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
|
|
2392
|
-
throw new Error('Skill bundle must pin one full Git commit.');
|
|
2393
|
-
if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
|
|
2394
|
-
throw new Error('Skill bundle must pin one credential-free GitHub repository.');
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
|
|
2398
|
-
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2399
|
-
verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
|
|
2400
|
-
await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
|
|
2401
|
-
await rm(sourceRoot, { recursive: true, force: true });
|
|
2402
|
-
await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
|
|
2403
|
-
const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
|
|
2404
|
-
if (bundle.operation === 'install' && !materializedInline) {
|
|
2405
|
-
await rm(sourceRoot, { recursive: true, force: true });
|
|
2406
|
-
await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
|
|
2407
|
-
await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
|
|
2408
|
-
await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
|
|
2409
|
-
}
|
|
2410
|
-
try {
|
|
2411
|
-
const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
|
|
2412
|
-
const receipt = await installSkillBundle({
|
|
2413
|
-
bundle,
|
|
2414
|
-
sourceDirectory: sourceRoot,
|
|
2415
|
-
nativeSkillDirectory: destination,
|
|
2416
|
-
policy,
|
|
2417
|
-
serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
|
|
2418
|
-
devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
|
|
2419
|
-
deviceId: config.deviceId,
|
|
2867
|
+
return withWorkspaceSkillActivationLock(workspaceId, provider, async () => {
|
|
2868
|
+
const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
|
|
2869
|
+
if (!workspace)
|
|
2870
|
+
throw new Error('Skill workspace is not registered locally.');
|
|
2871
|
+
const policy = await loadOrganizationPolicy(required(flags, 'policy'));
|
|
2872
|
+
const destination = nativeSkillDirectory(provider);
|
|
2873
|
+
const fabric = await client();
|
|
2874
|
+
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
2875
|
+
let activeBundleId;
|
|
2876
|
+
try {
|
|
2877
|
+
activeBundleId = (await activeSkillAuthorization(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
|
|
2878
|
+
}
|
|
2879
|
+
catch (error) {
|
|
2880
|
+
if (error instanceof Error && error.message === 'Skill bundle has expired.') {
|
|
2881
|
+
activeBundleId = (await expiredSkillAuthorizationForReplacement(provider, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null;
|
|
2882
|
+
}
|
|
2883
|
+
else {
|
|
2884
|
+
activeBundleId = await recoverLegacySkillBundleIdAfterAuthorizationFailure({
|
|
2885
|
+
nativeSkillDirectory: destination,
|
|
2886
|
+
workspaceId,
|
|
2887
|
+
authorizationError: error,
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const response = await fabric.pollSkill({
|
|
2420
2892
|
workspaceId,
|
|
2421
2893
|
provider,
|
|
2422
|
-
|
|
2423
|
-
organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
|
|
2894
|
+
installedBundleId: activeBundleId,
|
|
2424
2895
|
});
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2896
|
+
const rollout = response.rollout;
|
|
2897
|
+
if (!rollout)
|
|
2898
|
+
return { ok: true, rollout: null, changed: false };
|
|
2899
|
+
if (typeof rollout.id !== 'string' || !rollout.bundle || typeof rollout.bundle !== 'object')
|
|
2900
|
+
throw new Error('Skill rollout response is invalid.');
|
|
2901
|
+
const bundle = rollout.bundle;
|
|
2902
|
+
if (bundle.organizationId !== policy.organizationId || !Array.isArray(bundle.skills)
|
|
2903
|
+
|| (bundle.operation === 'install' && bundle.skills.length === 0)
|
|
2904
|
+
|| (bundle.operation === 'clear' && bundle.skills.length !== 0)) {
|
|
2905
|
+
throw new Error('Skill bundle does not match local organization policy.');
|
|
2906
|
+
}
|
|
2907
|
+
const commits = [...new Set(bundle.skills.map((skill) => skill.commit))];
|
|
2908
|
+
const repositories = [...new Set(bundle.skills.map((skill) => skill.repository))];
|
|
2909
|
+
if (bundle.operation === 'install') {
|
|
2910
|
+
if (commits.length !== 1 || !/^[a-f0-9]{40,64}$/i.test(commits[0]))
|
|
2911
|
+
throw new Error('Skill bundle must pin one full Git commit.');
|
|
2912
|
+
if (repositories.length !== 1 || !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(repositories[0])) {
|
|
2913
|
+
throw new Error('Skill bundle must pin one credential-free GitHub repository.');
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const sourceRoot = resolve(dharmaHome(), 'relay', 'skill-sources', bundle.bundleId);
|
|
2917
|
+
verifySkillBundle(bundle, createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }));
|
|
2918
|
+
await mkdir(resolve(dharmaHome(), 'relay', 'skill-sources'), { recursive: true, mode: 0o700 });
|
|
2431
2919
|
await rm(sourceRoot, { recursive: true, force: true });
|
|
2432
|
-
|
|
2920
|
+
await mkdir(sourceRoot, { recursive: true, mode: 0o700 });
|
|
2921
|
+
const materializedInline = await materializeInlineSkillFiles(bundle, sourceRoot);
|
|
2922
|
+
if (bundle.operation === 'install' && !materializedInline) {
|
|
2923
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
2924
|
+
await execFileAsync('git', ['clone', '--filter=blob:none', '--no-checkout', repositories[0], sourceRoot], { timeout: 120_000 });
|
|
2925
|
+
await execFileAsync('git', ['-C', sourceRoot, 'fetch', '--no-tags', '--depth=1', 'origin', commits[0]], { timeout: 120_000 });
|
|
2926
|
+
await execFileAsync('git', ['-C', sourceRoot, 'checkout', '--detach', commits[0]], { timeout: 30_000 });
|
|
2927
|
+
}
|
|
2928
|
+
try {
|
|
2929
|
+
const previousAnchor = await loadActiveSkillAuthorizationAnchor({
|
|
2930
|
+
config,
|
|
2931
|
+
workspaceId,
|
|
2932
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
2933
|
+
provider,
|
|
2934
|
+
});
|
|
2935
|
+
const identity = await loadOrCreateDeviceIdentity({ hqUrl: config.hqUrl, organizationId: config.organizationId });
|
|
2936
|
+
const receipt = await installSkillBundle({
|
|
2937
|
+
bundle,
|
|
2938
|
+
sourceDirectory: sourceRoot,
|
|
2939
|
+
nativeSkillDirectory: destination,
|
|
2940
|
+
policy,
|
|
2941
|
+
serverPublicKey: createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: config.serverPublicKeyEd25519 }, format: 'jwk' }),
|
|
2942
|
+
devicePrivateKey: createPrivateKey({ key: identity.privateJwk, format: 'jwk' }),
|
|
2943
|
+
deviceId: config.deviceId,
|
|
2944
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
2945
|
+
workspaceId,
|
|
2946
|
+
provider,
|
|
2947
|
+
smokeCommandId: typeof flags.get('smoke-command') === 'string' ? String(flags.get('smoke-command')) : undefined,
|
|
2948
|
+
organizationApprovalId: typeof flags.get('approval-id') === 'string' ? String(flags.get('approval-id')) : undefined,
|
|
2949
|
+
});
|
|
2950
|
+
const restoreAnchor = async () => {
|
|
2951
|
+
if (previousAnchor) {
|
|
2952
|
+
await saveActiveSkillAuthorizationAnchor({
|
|
2953
|
+
config,
|
|
2954
|
+
workspaceId,
|
|
2955
|
+
organizationAgentId: previousAnchor.organizationAgentId,
|
|
2956
|
+
provider,
|
|
2957
|
+
bundleId: previousAnchor.bundleId,
|
|
2958
|
+
receiptHash: previousAnchor.receiptHash,
|
|
2959
|
+
activatedAt: previousAnchor.activatedAt,
|
|
2960
|
+
expiresAt: previousAnchor.expiresAt,
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
await deleteActiveSkillAuthorizationAnchor({ config, workspaceId, provider });
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
const recoverLocalInstallation = async (error) => {
|
|
2968
|
+
const recoveryErrors = [];
|
|
2969
|
+
try {
|
|
2970
|
+
await rollbackUnconfirmedSkillBundle({
|
|
2971
|
+
nativeSkillDirectory: destination,
|
|
2972
|
+
workspaceId,
|
|
2973
|
+
receipt,
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
catch (rollbackError) {
|
|
2977
|
+
recoveryErrors.push(rollbackError);
|
|
2978
|
+
}
|
|
2979
|
+
try {
|
|
2980
|
+
await restoreAnchor();
|
|
2981
|
+
}
|
|
2982
|
+
catch (anchorError) {
|
|
2983
|
+
recoveryErrors.push(anchorError);
|
|
2984
|
+
}
|
|
2985
|
+
if (recoveryErrors.length) {
|
|
2986
|
+
throw new AggregateError([error, ...recoveryErrors], 'Skill installation failed and local recovery was incomplete.');
|
|
2987
|
+
}
|
|
2988
|
+
throw error;
|
|
2989
|
+
};
|
|
2990
|
+
try {
|
|
2991
|
+
if (receipt.status === 'active') {
|
|
2992
|
+
await saveActiveSkillAuthorizationAnchor({
|
|
2993
|
+
config,
|
|
2994
|
+
workspaceId,
|
|
2995
|
+
organizationAgentId: String(workspace.repositoryAgentId || ''),
|
|
2996
|
+
provider,
|
|
2997
|
+
bundleId: receipt.bundleId,
|
|
2998
|
+
receiptHash: receipt.receiptHash,
|
|
2999
|
+
activatedAt: receipt.completedAt,
|
|
3000
|
+
expiresAt: bundle.expiresAt ?? null,
|
|
3001
|
+
});
|
|
3002
|
+
if (provider === 'agy')
|
|
3003
|
+
await activateAgyPlugin();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
catch (error) {
|
|
3007
|
+
await recoverLocalInstallation(error);
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
await fabric.postInstallReceipt(bundle.bundleId, rollout.id, receipt);
|
|
3011
|
+
}
|
|
3012
|
+
catch (error) {
|
|
3013
|
+
if (isDefinitiveAgentFabricRejection(error))
|
|
3014
|
+
await recoverLocalInstallation(error);
|
|
3015
|
+
throw error;
|
|
3016
|
+
}
|
|
3017
|
+
return { ok: true, rolloutId: rollout.id, bundleId: bundle.bundleId, status: receipt.status, changed: true };
|
|
3018
|
+
}
|
|
3019
|
+
finally {
|
|
3020
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
2433
3023
|
}
|
|
2434
3024
|
async function relayStart(flags) {
|
|
2435
3025
|
const policyPath = resolve(required(flags, 'policy'));
|
|
@@ -2448,10 +3038,19 @@ async function relayStart(flags) {
|
|
|
2448
3038
|
process.once('SIGINT', stop);
|
|
2449
3039
|
process.once('SIGTERM', stop);
|
|
2450
3040
|
let tasksCompleted = 0;
|
|
3041
|
+
let taskTrajectoriesRecovered = 0;
|
|
2451
3042
|
let evidenceResponsesCompleted = 0;
|
|
3043
|
+
let trajectorySyncsCompleted = 0;
|
|
2452
3044
|
let nextPolicyRefreshAt = 0;
|
|
2453
3045
|
let evidencePolicyFresh = false;
|
|
3046
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
3047
|
+
const vault = await LocalVault.open({
|
|
3048
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
3049
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
3050
|
+
rawLocalDays: rawLocalRetentionDays(policy),
|
|
3051
|
+
});
|
|
2454
3052
|
try {
|
|
3053
|
+
taskTrajectoriesRecovered += (await finalizeRecoveredSignedTaskTrajectories(fabric, policy)).length;
|
|
2455
3054
|
do {
|
|
2456
3055
|
if (Date.now() >= nextPolicyRefreshAt) {
|
|
2457
3056
|
try {
|
|
@@ -2466,12 +3065,13 @@ async function relayStart(flags) {
|
|
|
2466
3065
|
}
|
|
2467
3066
|
let evidenceRequestId;
|
|
2468
3067
|
if (evidencePolicyFresh) {
|
|
3068
|
+
trajectorySyncsCompleted += await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
|
|
2469
3069
|
const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
|
|
2470
3070
|
evidenceRequestId = typeof evidence.requestId === 'string' ? evidence.requestId : undefined;
|
|
2471
3071
|
if (evidenceRequestId)
|
|
2472
3072
|
evidenceResponsesCompleted += 1;
|
|
2473
3073
|
}
|
|
2474
|
-
const result = await executeOneTask(fabric,
|
|
3074
|
+
const result = await executeOneTask(fabric, leaseSeconds);
|
|
2475
3075
|
if (result.taskId)
|
|
2476
3076
|
tasksCompleted += 1;
|
|
2477
3077
|
if (flags.has('once'))
|
|
@@ -2481,11 +3081,15 @@ async function relayStart(flags) {
|
|
|
2481
3081
|
} while (!stopping);
|
|
2482
3082
|
}
|
|
2483
3083
|
finally {
|
|
3084
|
+
vault.close();
|
|
2484
3085
|
process.removeListener('SIGINT', stop);
|
|
2485
3086
|
process.removeListener('SIGTERM', stop);
|
|
2486
3087
|
await rm(pidPath, { force: true });
|
|
2487
3088
|
}
|
|
2488
|
-
return {
|
|
3089
|
+
return {
|
|
3090
|
+
ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
|
|
3091
|
+
evidenceResponsesCompleted, trajectorySyncsCompleted,
|
|
3092
|
+
};
|
|
2489
3093
|
}
|
|
2490
3094
|
export async function run(argv) {
|
|
2491
3095
|
const { positional, flags, repeated } = parseCliOptions(argv);
|
|
@@ -2567,10 +3171,14 @@ export async function run(argv) {
|
|
|
2567
3171
|
throw new Error('Skill provider must be codex, claude, or agy.');
|
|
2568
3172
|
const root = nativeSkillDirectory(providerValue);
|
|
2569
3173
|
const workspaceId = required(flags, 'workspace-id');
|
|
3174
|
+
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
3175
|
+
const workspace = (await registry()).find((item) => item.workspaceId === workspaceId);
|
|
3176
|
+
if (!workspace)
|
|
3177
|
+
throw new Error('Skill workspace is not registered locally.');
|
|
2570
3178
|
return {
|
|
2571
3179
|
provider: providerValue,
|
|
2572
3180
|
workspaceId,
|
|
2573
|
-
activeBundleId: await
|
|
3181
|
+
activeBundleId: (await activeSkillAuthorization(providerValue, workspaceId, String(workspace.repositoryAgentId || ''), config))?.bundleId ?? null,
|
|
2574
3182
|
nativeSkillDirectory: root,
|
|
2575
3183
|
};
|
|
2576
3184
|
}
|