@dharma-ai-labs/agent-fabric 0.1.7 → 0.1.10
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 +9 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +819 -47
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +303 -7
- package/dist/index.test.js.map +1 -1
- package/dist/schemas/evidence-request-v2.schema.json +143 -0
- package/dist/schemas/evidence-request.schema.json +14 -1
- package/dist/schemas/organization-policy-v2.schema.json +254 -0
- package/dist/schemas/trajectory-capsule.schema.json +57 -0
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
|
-
import { access, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { access, link, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
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 } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
12
|
-
import { loadOrganizationPolicy } from '@dharma-ai-labs/agent-fabric-policy';
|
|
11
|
+
import { buildTrajectoryCapsule, redactValue, referencesExcludedPath } from '@dharma-ai-labs/agent-fabric-evidence-reduction';
|
|
12
|
+
import { assertPolicy, loadOrganizationPolicy, verifyServerAuthorizedPolicy } from '@dharma-ai-labs/agent-fabric-policy';
|
|
13
13
|
import { agyAdapter, claudeAdapter, codexAdapter, providerAdapters } from '@dharma-ai-labs/agent-fabric-provider-adapters';
|
|
14
14
|
import { AgentFabricClient, beginEnrollment, loadOrCreateDeviceIdentity, normalizeHqUrl, pollEnrollment, saveDeviceConfig, } from '@dharma-ai-labs/agent-fabric-relay-client';
|
|
15
15
|
import { getActiveSkillBundleId, installSkillBundle, verifySkillBundle } from '@dharma-ai-labs/agent-fabric-skill-manager';
|
|
16
16
|
import { executeTask, FileTaskReceiptStore } from '@dharma-ai-labs/agent-fabric-task-runner';
|
|
17
|
-
const VERSION = '0.1.
|
|
17
|
+
const VERSION = '0.1.10';
|
|
18
18
|
const USAGE = '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|skills verify> [options]';
|
|
19
19
|
const execFileAsync = promisify(execFile);
|
|
20
20
|
function options(args) {
|
|
@@ -80,6 +80,23 @@ function configPath() { return resolve(dharmaHome(), 'device.json'); }
|
|
|
80
80
|
function pendingEnrollmentPath() { return resolve(dharmaHome(), 'pending-enrollment.json'); }
|
|
81
81
|
function protocolStatePath() { return resolve(dharmaHome(), 'relay', 'protocol-state.json'); }
|
|
82
82
|
function workspaceRegistryPath() { return resolve(dharmaHome(), 'registry', 'workspaces.json'); }
|
|
83
|
+
function evidenceUploadLedgerPath() { return resolve(dharmaHome(), 'relay', 'evidence-upload-ledger.json'); }
|
|
84
|
+
function evidenceRequestReceiptPath(requestId) { return resolve(dharmaHome(), 'relay', 'evidence-requests', `${requestId}.json`); }
|
|
85
|
+
function newEvidenceUploadLedger(day) {
|
|
86
|
+
return { schema: 'dharma.evidence-upload-ledger/v2', day, totalBytes: 0, capsuleHashes: [], capsuleBytes: {} };
|
|
87
|
+
}
|
|
88
|
+
function assertEvidenceLedger(ledger, day) {
|
|
89
|
+
if (ledger.schema !== 'dharma.evidence-upload-ledger/v2' || ledger.day !== day
|
|
90
|
+
|| !Number.isSafeInteger(ledger.totalBytes) || ledger.totalBytes < 0
|
|
91
|
+
|| !Array.isArray(ledger.capsuleHashes) || new Set(ledger.capsuleHashes).size !== ledger.capsuleHashes.length
|
|
92
|
+
|| ledger.capsuleHashes.some((hash) => !/^sha256:[a-f0-9]{64}$/.test(hash))
|
|
93
|
+
|| !ledger.capsuleBytes || typeof ledger.capsuleBytes !== 'object'
|
|
94
|
+
|| ledger.capsuleHashes.some((hash) => !Number.isSafeInteger(ledger.capsuleBytes[hash]) || ledger.capsuleBytes[hash] < 0)
|
|
95
|
+
|| Object.keys(ledger.capsuleBytes).some((hash) => !ledger.capsuleHashes.includes(hash))
|
|
96
|
+
|| ledger.capsuleHashes.reduce((sum, hash) => sum + ledger.capsuleBytes[hash], 0) !== ledger.totalBytes) {
|
|
97
|
+
throw new Error('Evidence upload ledger is invalid.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
83
100
|
async function pathExists(path) {
|
|
84
101
|
try {
|
|
85
102
|
await access(path);
|
|
@@ -114,19 +131,415 @@ export function rawLocalRetentionDays(policy) {
|
|
|
114
131
|
}
|
|
115
132
|
return Number(value);
|
|
116
133
|
}
|
|
117
|
-
async function syncPendingRetentionCapsules(vault, fabric) {
|
|
134
|
+
async function syncPendingRetentionCapsules(vault, fabric, policy, workspaceId) {
|
|
135
|
+
// The caller must obtain this policy from an authoritative control-plane
|
|
136
|
+
// refresh. An expired policy pauses the queue instead of mutating it.
|
|
137
|
+
assertPolicy(policy);
|
|
118
138
|
let synced = 0;
|
|
139
|
+
let offset = 0;
|
|
119
140
|
for (;;) {
|
|
120
|
-
const pending = await vault.listPendingCapsuleSyncs(
|
|
141
|
+
const pending = await vault.listPendingCapsuleSyncs(1_000, offset);
|
|
121
142
|
if (pending.length === 0)
|
|
122
143
|
return synced;
|
|
144
|
+
let matched = 0;
|
|
123
145
|
for (const item of pending) {
|
|
146
|
+
if (String(item.capsule.workspaceId || '') !== workspaceId)
|
|
147
|
+
continue;
|
|
148
|
+
matched += 1;
|
|
149
|
+
try {
|
|
150
|
+
assertCapsuleIntegrity(item.capsule);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
vault.discardPendingCapsuleSync(item.trajectoryId, item.revision, 'capsule_integrity_failed');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Policy expiry and signature failure pause the queue. Only a capsule
|
|
157
|
+
// mismatch under a still-current policy is an authoritative discard.
|
|
158
|
+
assertPolicy(policy);
|
|
159
|
+
try {
|
|
160
|
+
assertCapsuleAuthorizedByCurrentPolicy(item.capsule, policy);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
assertPolicy(policy);
|
|
164
|
+
// A successfully refreshed policy is authoritative. Retire only the
|
|
165
|
+
// superseded capsule so it cannot permanently block later valid work.
|
|
166
|
+
vault.discardPendingCapsuleSync(item.trajectoryId, item.revision, 'authorization_superseded');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
await reserveDailyContentUpload(item.capsule, policy);
|
|
124
170
|
await fabric.syncTrajectory(item.capsule);
|
|
125
171
|
vault.markCapsuleSynced(item.trajectoryId, item.revision);
|
|
126
172
|
synced += 1;
|
|
127
173
|
}
|
|
174
|
+
if (matched === 0) {
|
|
175
|
+
if (pending.length < 1_000)
|
|
176
|
+
return synced;
|
|
177
|
+
offset += pending.length;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
offset = 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function refreshVerifiedWorkspacePolicyForTransmission(policyPath, workspaceId, fabric) {
|
|
185
|
+
if (!workspaceId)
|
|
186
|
+
throw new Error('Content transmission requires a registered workspace ID.');
|
|
187
|
+
const item = (await registry()).find((candidate) => candidate.workspaceId === workspaceId);
|
|
188
|
+
if (!item)
|
|
189
|
+
throw new Error('Content transmission workspace is not registered locally.');
|
|
190
|
+
const canonicalPolicyPath = resolve(item.path, '.dharma', 'approved-policy.json');
|
|
191
|
+
if (resolve(policyPath) !== canonicalPolicyPath) {
|
|
192
|
+
throw new Error('Content transmission requires the canonical registered workspace policy path.');
|
|
193
|
+
}
|
|
194
|
+
const current = await loadOrganizationPolicy(policyPath);
|
|
195
|
+
if (current.organizationId !== item.organizationId) {
|
|
196
|
+
throw new Error('Workspace policy organization does not match registration.');
|
|
197
|
+
}
|
|
198
|
+
await syncWorkspacePolicy(fabric, item, current.revision, true);
|
|
199
|
+
return loadVerifiedWorkspacePolicy(policyPath, workspaceId);
|
|
200
|
+
}
|
|
201
|
+
export function assertCapsuleIntegrity(capsule) {
|
|
202
|
+
const capsuleHash = String(capsule.capsuleHash || '');
|
|
203
|
+
const { capsuleHash: _capsuleHash, ...unsigned } = capsule;
|
|
204
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash) || sha256(canonicalize(unsigned)) !== capsuleHash) {
|
|
205
|
+
throw new Error('Trajectory capsule integrity check failed.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function assertCapsuleAuthorizedByCurrentPolicy(capsule, policy) {
|
|
209
|
+
assertPolicy(policy);
|
|
210
|
+
const mode = capsule.automaticDisclosureMode;
|
|
211
|
+
const receipt = capsule.redactionReceipt && typeof capsule.redactionReceipt === 'object' && !Array.isArray(capsule.redactionReceipt)
|
|
212
|
+
? capsule.redactionReceipt : {};
|
|
213
|
+
if (!['metadata_only', 'local_analysis', 'customer_authorized_content'].includes(String(mode))
|
|
214
|
+
|| receipt.disclosureMode !== mode
|
|
215
|
+
|| mode !== (policy.evidence.automaticDisclosure?.mode || 'local_analysis')
|
|
216
|
+
|| capsule.organizationId !== policy.organizationId
|
|
217
|
+
|| (policy.serverAuthorization && capsule.workspaceId !== policy.serverAuthorization.workspaceId)) {
|
|
218
|
+
throw new Error('Trajectory capsule disclosure mode is invalid or inconsistent.');
|
|
219
|
+
}
|
|
220
|
+
const events = Array.isArray(capsule.events) ? capsule.events : [];
|
|
221
|
+
if (mode !== 'customer_authorized_content') {
|
|
222
|
+
const provider = String(capsule.provider || '');
|
|
223
|
+
const fixedEventKinds = new Set([
|
|
224
|
+
'user_message', 'agent_message', 'tool_call', 'tool_result', 'command', 'file_read', 'file_write',
|
|
225
|
+
'git', 'validation', 'permission', 'subagent', 'error', 'retry', 'session_state', 'metadata', 'collapsed_output',
|
|
226
|
+
]);
|
|
227
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
228
|
+
const digest = /^sha256:[a-f0-9]{64}$/;
|
|
229
|
+
const allowedMissingFields = new Set([
|
|
230
|
+
'workspace_on_some_events', 'events_collapsed_for_size', 'native_payload_collapsed_for_size',
|
|
231
|
+
]);
|
|
232
|
+
const allowedDisclosedClasses = new Set([
|
|
233
|
+
'tenant_identifier', 'device_identifier', 'workspace_identifier', 'pseudonymous_session_identifier',
|
|
234
|
+
'provider_name', 'event_kind', 'event_timestamp', 'event_coverage', 'source_kind', 'record_size',
|
|
235
|
+
'local_evidence_descriptor', 'local_deterministic_analysis',
|
|
236
|
+
]);
|
|
237
|
+
const allowedExcludedClasses = new Set([
|
|
238
|
+
'encrypted_reasoning', 'execution_configuration', 'instruction_text', 'local_path', 'native_provider_payload',
|
|
239
|
+
'prompt_text', 'rate_limit_metadata', 'response_text', 'token_metadata', 'tool_schema', 'tool_input', 'tool_output',
|
|
240
|
+
]);
|
|
241
|
+
const allowedRedactionClasses = new Set([
|
|
242
|
+
'automatic_content_omission', 'configured_excluded_path', 'invalid_unicode_nul', 'local_path',
|
|
243
|
+
'private_key', 'github_token', 'openai_key', 'aws_access_key', 'jwt', 'connection_string', 'authorization',
|
|
244
|
+
'google_api_key', 'slack_token', 'generic_secret', 'sensitive_field',
|
|
245
|
+
]);
|
|
246
|
+
const coverage = capsule.coverage && typeof capsule.coverage === 'object' && !Array.isArray(capsule.coverage)
|
|
247
|
+
? capsule.coverage : {};
|
|
248
|
+
const missingFields = Array.isArray(coverage.missingFields) ? coverage.missingFields : null;
|
|
249
|
+
if (!['codex', 'claude', 'agy'].includes(provider)
|
|
250
|
+
|| !digest.test(String(capsule.sessionId || ''))
|
|
251
|
+
|| capsule.taskId !== null
|
|
252
|
+
|| !uuid.test(String(capsule.deviceId || ''))
|
|
253
|
+
|| capsule.redactionReceipt === null
|
|
254
|
+
|| receipt.policyRevision !== policy.revision
|
|
255
|
+
|| capsule.evidenceMode !== policy.evidence.defaultMode
|
|
256
|
+
|| !['completed', 'partial'].includes(String(capsule.status || ''))
|
|
257
|
+
|| !['observed', 'partial'].includes(String(coverage.state || ''))
|
|
258
|
+
|| !Number.isSafeInteger(coverage.admittedSessions) || Number(coverage.admittedSessions) < 0
|
|
259
|
+
|| !Number.isSafeInteger(coverage.excludedSessions) || Number(coverage.excludedSessions) < 0
|
|
260
|
+
|| !missingFields || missingFields.some((value) => typeof value !== 'string' || !allowedMissingFields.has(value))) {
|
|
261
|
+
throw new Error('Reduced trajectory capsule contains unauthorized identity or policy metadata.');
|
|
262
|
+
}
|
|
263
|
+
const emptyRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
264
|
+
&& Object.keys(value).length === 0;
|
|
265
|
+
if (!emptyRecord(capsule.repoState) || !emptyRecord(capsule.skillState)
|
|
266
|
+
|| !Array.isArray(capsule.validationResults) || capsule.validationResults.length !== 0) {
|
|
267
|
+
throw new Error('Reduced trajectory capsule contains unauthorized auxiliary content.');
|
|
268
|
+
}
|
|
269
|
+
const contentIndex = Array.isArray(capsule.contentIndex) ? capsule.contentIndex : [];
|
|
270
|
+
if (contentIndex.some((entry) => !entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
271
|
+
|| !digest.test(String(entry.contentId || ''))
|
|
272
|
+
|| entry.uploaded !== false
|
|
273
|
+
|| entry.availableLocally !== true
|
|
274
|
+
|| !Number.isSafeInteger(entry.bytes)
|
|
275
|
+
|| Number(entry.bytes) < 0
|
|
276
|
+
|| entry.normalizedPath !== null
|
|
277
|
+
|| !['raw-provider-session', 'raw-provider-turn'].includes(String(entry.kind || ''))
|
|
278
|
+
|| entry.mimeType !== 'application/x-ndjson')) {
|
|
279
|
+
throw new Error('Reduced trajectory capsule contains an unauthorized content descriptor.');
|
|
280
|
+
}
|
|
281
|
+
const localEvidenceAvailable = Array.isArray(capsule.localEvidenceAvailable) ? capsule.localEvidenceAvailable : [];
|
|
282
|
+
if (localEvidenceAvailable.some((entry) => !entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
283
|
+
|| !digest.test(String(entry.contentId || ''))
|
|
284
|
+
|| !['raw-provider-session', 'raw-provider-turn'].includes(String(entry.kind || ''))
|
|
285
|
+
|| !Number.isSafeInteger(entry.bytes)
|
|
286
|
+
|| Number(entry.bytes) < 0)) {
|
|
287
|
+
throw new Error('Reduced trajectory capsule contains an unauthorized local evidence descriptor.');
|
|
288
|
+
}
|
|
289
|
+
for (const event of events) {
|
|
290
|
+
const payload = event && typeof event === 'object' && !Array.isArray(event)
|
|
291
|
+
? event.payload : null;
|
|
292
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
293
|
+
throw new Error('Reduced trajectory event payload is invalid.');
|
|
294
|
+
}
|
|
295
|
+
const record = payload;
|
|
296
|
+
const allowedKeys = new Set(['nativeKind', 'recordBytes', 'contentOmitted']);
|
|
297
|
+
if (Object.keys(record).some((key) => !allowedKeys.has(key)) || record.contentOmitted !== true
|
|
298
|
+
|| !/^[A-Za-z0-9_.:-]{1,80}$/.test(String(record.nativeKind || ''))
|
|
299
|
+
|| !Number.isSafeInteger(record.recordBytes) || Number(record.recordBytes) < 0) {
|
|
300
|
+
throw new Error('Reduced trajectory capsule contains unauthorized provider content.');
|
|
301
|
+
}
|
|
302
|
+
const eventRecord = event;
|
|
303
|
+
const allowedEventKeys = new Set([
|
|
304
|
+
'schema', 'eventId', 'organizationId', 'deviceId', 'workspaceId', 'provider', 'sessionId', 'sequence',
|
|
305
|
+
'occurredAt', 'kind', 'coverage', 'contentRefs', 'payload', 'source', 'skillBundleId', 'providerModel',
|
|
306
|
+
]);
|
|
307
|
+
const source = eventRecord.source && typeof eventRecord.source === 'object' && !Array.isArray(eventRecord.source)
|
|
308
|
+
? eventRecord.source : {};
|
|
309
|
+
const eventKind = String(eventRecord.kind || '');
|
|
310
|
+
if (Object.keys(eventRecord).some((key) => !allowedEventKeys.has(key))
|
|
311
|
+
|| eventRecord.schema !== 'dharma.agent-event/v1'
|
|
312
|
+
|| !uuid.test(String(eventRecord.eventId || ''))
|
|
313
|
+
|| eventRecord.organizationId !== capsule.organizationId
|
|
314
|
+
|| eventRecord.deviceId !== capsule.deviceId
|
|
315
|
+
|| eventRecord.workspaceId !== capsule.workspaceId
|
|
316
|
+
|| eventRecord.provider !== provider
|
|
317
|
+
|| eventRecord.sessionId !== capsule.sessionId
|
|
318
|
+
|| !Number.isSafeInteger(eventRecord.sequence) || Number(eventRecord.sequence) < 0
|
|
319
|
+
|| !Number.isFinite(Date.parse(String(eventRecord.occurredAt || '')))
|
|
320
|
+
|| !fixedEventKinds.has(eventKind)
|
|
321
|
+
|| !['observed', 'partial', 'unavailable', 'excluded', 'redacted', 'out_of_window', 'not_supported'].includes(String(eventRecord.coverage || ''))
|
|
322
|
+
|| !Array.isArray(eventRecord.contentRefs) || eventRecord.contentRefs.length !== 0
|
|
323
|
+
|| source.nativeEventId !== null || source.localLocatorId !== null || source.sourceKind !== eventKind
|
|
324
|
+
|| record.nativeKind !== eventKind
|
|
325
|
+
|| eventRecord.skillBundleId !== null || eventRecord.providerModel !== null) {
|
|
326
|
+
throw new Error('Reduced trajectory capsule contains unauthorized event descriptors.');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (receipt.consentReceiptId !== null && receipt.consentReceiptId !== undefined) {
|
|
330
|
+
throw new Error('Reduced trajectory capsule cannot claim a content consent receipt.');
|
|
331
|
+
}
|
|
332
|
+
const localAnalysis = capsule.localAnalysis;
|
|
333
|
+
if (mode === 'metadata_only' && localAnalysis !== null) {
|
|
334
|
+
throw new Error('Metadata-only trajectory capsule cannot contain local analysis.');
|
|
335
|
+
}
|
|
336
|
+
if (mode === 'local_analysis') {
|
|
337
|
+
if (!localAnalysis || typeof localAnalysis !== 'object' || Array.isArray(localAnalysis)) {
|
|
338
|
+
throw new Error('Local-analysis trajectory capsule is missing deterministic analysis.');
|
|
339
|
+
}
|
|
340
|
+
const analysis = localAnalysis;
|
|
341
|
+
const eventKinds = analysis.eventKinds && typeof analysis.eventKinds === 'object' && !Array.isArray(analysis.eventKinds)
|
|
342
|
+
? analysis.eventKinds : null;
|
|
343
|
+
const reasonCodes = Array.isArray(analysis.reasonCodes) ? analysis.reasonCodes : null;
|
|
344
|
+
const allowedReasons = new Set(['runtime_failure_signal', 'tool_call_without_result', 'tool_result_without_call', 'partial_evidence']);
|
|
345
|
+
if (analysis.schema !== 'dharma.local-trajectory-analysis/v1' || analysis.analyzer !== 'deterministic'
|
|
346
|
+
|| !eventKinds || Object.entries(eventKinds).some(([key, value]) => !fixedEventKinds.has(key)
|
|
347
|
+
|| !Number.isSafeInteger(value) || Number(value) < 0)
|
|
348
|
+
|| !reasonCodes || reasonCodes.some((value) => typeof value !== 'string' || !allowedReasons.has(value))) {
|
|
349
|
+
throw new Error('Local-analysis trajectory capsule contains invalid free-form analysis data.');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const validReceiptValues = (key, allowed) => Array.isArray(receipt[key])
|
|
353
|
+
&& receipt[key].every((value) => typeof value === 'string' && allowed.has(value));
|
|
354
|
+
if (!validReceiptValues('disclosedClasses', allowedDisclosedClasses)
|
|
355
|
+
|| !validReceiptValues('excludedClasses', allowedExcludedClasses)
|
|
356
|
+
|| !validReceiptValues('classes', allowedRedactionClasses)) {
|
|
357
|
+
throw new Error('Reduced trajectory capsule contains invalid redaction receipt classes.');
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const disclosure = policy.evidence.automaticDisclosure;
|
|
362
|
+
if (disclosure?.mode !== 'customer_authorized_content'
|
|
363
|
+
|| receipt.policyRevision !== policy.revision
|
|
364
|
+
|| receipt.consentReceiptId !== disclosure.consentReceiptId
|
|
365
|
+
|| !policy.serverAuthorization) {
|
|
366
|
+
throw new Error('Queued content capsule is no longer authorized by the current signed policy.');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function acquirePidLock(lockPath, timeoutMs, timeoutMessage) {
|
|
370
|
+
await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
371
|
+
const deadline = Date.now() + timeoutMs;
|
|
372
|
+
for (;;) {
|
|
373
|
+
const candidate = `${lockPath}.${process.pid}.${randomUUID()}.candidate`;
|
|
374
|
+
try {
|
|
375
|
+
await writeFile(candidate, `${process.pid}\n`, { mode: 0o600, flag: 'wx' });
|
|
376
|
+
await link(candidate, lockPath);
|
|
377
|
+
await unlink(candidate);
|
|
378
|
+
return async () => { await unlink(lockPath).catch(() => undefined); };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
await unlink(candidate).catch(() => undefined);
|
|
382
|
+
if (error.code !== 'EEXIST')
|
|
383
|
+
throw error;
|
|
384
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
385
|
+
const recoveryCandidate = `${recoveryPath}.${process.pid}.${randomUUID()}.candidate`;
|
|
386
|
+
try {
|
|
387
|
+
await mkdir(recoveryCandidate);
|
|
388
|
+
await writeFile(resolve(recoveryCandidate, 'owner'), `${process.pid}\n`, { mode: 0o600 });
|
|
389
|
+
await rename(recoveryCandidate, recoveryPath);
|
|
390
|
+
try {
|
|
391
|
+
let ownerPid = 0;
|
|
392
|
+
try {
|
|
393
|
+
ownerPid = Number((await readFile(lockPath, 'utf8')).trim());
|
|
394
|
+
}
|
|
395
|
+
catch (readError) {
|
|
396
|
+
if (readError.code === 'ENOENT')
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
let ownerAlive = Number.isSafeInteger(ownerPid) && ownerPid > 0;
|
|
400
|
+
if (ownerAlive) {
|
|
401
|
+
try {
|
|
402
|
+
process.kill(ownerPid, 0);
|
|
403
|
+
}
|
|
404
|
+
catch (killError) {
|
|
405
|
+
ownerAlive = killError.code === 'EPERM';
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!ownerAlive) {
|
|
409
|
+
const quarantine = `${lockPath}.dead.${randomUUID()}`;
|
|
410
|
+
try {
|
|
411
|
+
await rename(lockPath, quarantine);
|
|
412
|
+
await unlink(quarantine);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
catch (renameError) {
|
|
416
|
+
if (renameError.code !== 'ENOENT')
|
|
417
|
+
throw renameError;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await rm(recoveryPath, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (recoveryError) {
|
|
426
|
+
await rm(recoveryCandidate, { recursive: true, force: true });
|
|
427
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(recoveryError.code || ''))
|
|
428
|
+
throw recoveryError;
|
|
429
|
+
let recoveryOwner = 0;
|
|
430
|
+
try {
|
|
431
|
+
recoveryOwner = Number((await readFile(resolve(recoveryPath, 'owner'), 'utf8')).trim());
|
|
432
|
+
}
|
|
433
|
+
catch { }
|
|
434
|
+
let recoveryOwnerAlive = Number.isSafeInteger(recoveryOwner) && recoveryOwner > 0;
|
|
435
|
+
if (recoveryOwnerAlive) {
|
|
436
|
+
try {
|
|
437
|
+
process.kill(recoveryOwner, 0);
|
|
438
|
+
}
|
|
439
|
+
catch (killError) {
|
|
440
|
+
recoveryOwnerAlive = killError.code === 'EPERM';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!recoveryOwnerAlive) {
|
|
444
|
+
const quarantine = `${recoveryPath}.dead.${randomUUID()}`;
|
|
445
|
+
try {
|
|
446
|
+
await rename(recoveryPath, quarantine);
|
|
447
|
+
await rm(quarantine, { recursive: true, force: true });
|
|
448
|
+
}
|
|
449
|
+
catch (renameError) {
|
|
450
|
+
if (renameError.code !== 'ENOENT')
|
|
451
|
+
throw renameError;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (Date.now() >= deadline)
|
|
456
|
+
throw new Error(timeoutMessage);
|
|
457
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function withEvidenceLedgerLock(operation) {
|
|
462
|
+
const release = await acquirePidLock(`${evidenceUploadLedgerPath()}.lock`, 5_000, 'Timed out waiting for the evidence upload ledger lock.');
|
|
463
|
+
try {
|
|
464
|
+
return await operation();
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
await release();
|
|
128
468
|
}
|
|
129
469
|
}
|
|
470
|
+
async function writeJsonAtomic(path, value) {
|
|
471
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
472
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
473
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
474
|
+
await rename(temporary, path);
|
|
475
|
+
}
|
|
476
|
+
async function reserveDailyEvidenceBytes(key, bytes, policy, _store) {
|
|
477
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(key) || !Number.isSafeInteger(bytes) || bytes < 0) {
|
|
478
|
+
throw new Error('Evidence upload reservation is invalid.');
|
|
479
|
+
}
|
|
480
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
481
|
+
await withEvidenceLedgerLock(async () => {
|
|
482
|
+
let ledger;
|
|
483
|
+
try {
|
|
484
|
+
ledger = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
|
|
485
|
+
if (ledger.day !== day) {
|
|
486
|
+
ledger = newEvidenceUploadLedger(day);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
assertEvidenceLedger(ledger, day);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (error.code === 'ENOENT') {
|
|
494
|
+
ledger = newEvidenceUploadLedger(day);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (ledger.capsuleHashes.includes(key))
|
|
501
|
+
return;
|
|
502
|
+
if (ledger.totalBytes + bytes > policy.evidence.maximumDailyUploadBytes) {
|
|
503
|
+
throw new Error('Organization daily content upload limit would be exceeded.');
|
|
504
|
+
}
|
|
505
|
+
ledger.totalBytes += bytes;
|
|
506
|
+
ledger.capsuleHashes.push(key);
|
|
507
|
+
ledger.capsuleBytes[key] = bytes;
|
|
508
|
+
await writeJsonAtomic(evidenceUploadLedgerPath(), ledger);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
export async function reserveDailyContentUpload(capsule, policy, store) {
|
|
512
|
+
if (capsule.automaticDisclosureMode !== 'customer_authorized_content')
|
|
513
|
+
return;
|
|
514
|
+
const capsuleHash = String(capsule.capsuleHash || '');
|
|
515
|
+
assertCapsuleIntegrity(capsule);
|
|
516
|
+
const bytes = Buffer.byteLength(canonicalize(capsule));
|
|
517
|
+
await reserveDailyEvidenceBytes(capsuleHash, bytes, policy, store);
|
|
518
|
+
}
|
|
519
|
+
export async function releaseDailyContentUpload(capsule, policy, _store) {
|
|
520
|
+
if (capsule.automaticDisclosureMode !== 'customer_authorized_content')
|
|
521
|
+
return;
|
|
522
|
+
const capsuleHash = String(capsule.capsuleHash || '');
|
|
523
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(capsuleHash))
|
|
524
|
+
return;
|
|
525
|
+
await withEvidenceLedgerLock(async () => {
|
|
526
|
+
try {
|
|
527
|
+
const ledger = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
|
|
528
|
+
assertEvidenceLedger(ledger, new Date().toISOString().slice(0, 10));
|
|
529
|
+
if (!ledger.capsuleHashes.includes(capsuleHash))
|
|
530
|
+
return;
|
|
531
|
+
const reservedBytes = Number(ledger.capsuleBytes[capsuleHash] ?? Buffer.byteLength(canonicalize(capsule)));
|
|
532
|
+
ledger.capsuleHashes = ledger.capsuleHashes.filter((hash) => hash !== capsuleHash);
|
|
533
|
+
ledger.totalBytes = Math.max(0, ledger.totalBytes - (Number.isSafeInteger(reservedBytes) ? reservedBytes : 0));
|
|
534
|
+
delete ledger.capsuleBytes[capsuleHash];
|
|
535
|
+
await writeJsonAtomic(evidenceUploadLedgerPath(), ledger);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error.code !== 'ENOENT')
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
130
543
|
export async function relayProcessState(home = dharmaHome()) {
|
|
131
544
|
let pid;
|
|
132
545
|
try {
|
|
@@ -171,12 +584,13 @@ export async function materializeWorkspacePolicy(input) {
|
|
|
171
584
|
if (await pathExists(resolve(input.workspace, candidate)))
|
|
172
585
|
writePaths.push(`${candidate}/**`);
|
|
173
586
|
}
|
|
174
|
-
|
|
175
|
-
schema: 'dharma.organization-policy/
|
|
587
|
+
let policy = {
|
|
588
|
+
schema: 'dharma.organization-policy/v2',
|
|
176
589
|
organizationId: input.organizationId,
|
|
177
590
|
revision: input.revision,
|
|
178
591
|
evidence: {
|
|
179
592
|
defaultMode: 'deep',
|
|
593
|
+
automaticDisclosure: { mode: 'local_analysis' },
|
|
180
594
|
registeredWorkspaceOnly: true,
|
|
181
595
|
excludePaths: ['.env', '.env.*', '.git/**', 'node_modules/**', 'dist/**', 'build/**', '**/*.pem', '**/*.key'],
|
|
182
596
|
maximumCapsuleBytes: 1_000_000,
|
|
@@ -195,10 +609,188 @@ export async function materializeWorkspacePolicy(input) {
|
|
|
195
609
|
retention: { rawLocalDays: 30, capsuleServerDays: 90 },
|
|
196
610
|
budgets: { dailyAnalysisCents: 1_000 },
|
|
197
611
|
};
|
|
612
|
+
const existingPath = resolve(input.workspace, '.dharma', 'approved-policy.json');
|
|
613
|
+
if (await pathExists(existingPath)) {
|
|
614
|
+
const existing = await loadOrganizationPolicy(existingPath);
|
|
615
|
+
if (existing.organizationId === input.organizationId)
|
|
616
|
+
policy = existing;
|
|
617
|
+
}
|
|
618
|
+
if (input.serverPolicyAuthorization !== undefined && input.serverPolicyAuthorization !== null) {
|
|
619
|
+
if (!input.serverPublicKeyEd25519 || !input.workspaceId) {
|
|
620
|
+
throw new Error('Server policy authorization requires the enrolled server key and workspace ID.');
|
|
621
|
+
}
|
|
622
|
+
policy = applyServerEvidencePolicy(policy, input.serverPolicyAuthorization, input.serverPublicKeyEd25519, input.organizationId, input.workspaceId);
|
|
623
|
+
if (!input.dryRun) {
|
|
624
|
+
await applyWorkspaceAuthorizationAtomically({
|
|
625
|
+
workspaceId: input.workspaceId,
|
|
626
|
+
authorization: policy.serverAuthorization,
|
|
627
|
+
policyPath: existingPath,
|
|
628
|
+
policy,
|
|
629
|
+
secureStore: input.secureStore,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
await assertWorkspaceAuthorizationCurrent(input.workspaceId, policy.serverAuthorization, false);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
assertPolicy(policy);
|
|
198
637
|
const relativePath = '.dharma/approved-policy.json';
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
638
|
+
if (!input.dryRun && !policy.serverAuthorization) {
|
|
639
|
+
await mkdir(resolve(input.workspace, '.dharma'), { recursive: true, mode: 0o700 });
|
|
640
|
+
await writeJsonAtomic(resolve(input.workspace, relativePath), policy);
|
|
641
|
+
}
|
|
642
|
+
return { relativePath, policy, applied: !input.dryRun };
|
|
643
|
+
}
|
|
644
|
+
function workspaceAuthorizationStatePath(workspaceId) {
|
|
645
|
+
return resolve(dharmaHome(), 'registry', 'workspace-authorizations', `${workspaceId}.json`);
|
|
646
|
+
}
|
|
647
|
+
async function withFileLock(lockPath, operation) {
|
|
648
|
+
const release = await acquirePidLock(lockPath, 10_000, 'Timed out waiting for the workspace authorization lock.');
|
|
649
|
+
try {
|
|
650
|
+
return await operation();
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
await release();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function assertWorkspaceAuthorizationCurrent(workspaceId, authorization, requireExisting = true) {
|
|
657
|
+
const statePath = workspaceAuthorizationStatePath(workspaceId);
|
|
658
|
+
let previous = null;
|
|
659
|
+
try {
|
|
660
|
+
previous = JSON.parse(await readFile(statePath, 'utf8'));
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (error.code === 'ENOENT' && !requireExisting)
|
|
664
|
+
return;
|
|
665
|
+
throw new Error('Workspace authorization replay state is missing or invalid; apply a fresh server policy.');
|
|
666
|
+
}
|
|
667
|
+
if (previous) {
|
|
668
|
+
const incomingTime = Date.parse(authorization.issuedAt);
|
|
669
|
+
const previousTime = Date.parse(previous.issuedAt);
|
|
670
|
+
if (!Number.isFinite(previousTime)
|
|
671
|
+
|| incomingTime < previousTime
|
|
672
|
+
|| (incomingTime === previousTime && authorization.signature !== previous.signature)) {
|
|
673
|
+
throw new Error('Server workspace policy authorization is older than the last accepted authorization.');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function applyWorkspaceAuthorizationAtomically(input) {
|
|
678
|
+
const statePath = workspaceAuthorizationStatePath(input.workspaceId);
|
|
679
|
+
await withFileLock(`${statePath}.lock`, async () => {
|
|
680
|
+
await assertWorkspaceAuthorizationCurrent(input.workspaceId, input.authorization, false);
|
|
681
|
+
let previous = null;
|
|
682
|
+
try {
|
|
683
|
+
previous = JSON.parse(await readFile(statePath, 'utf8'));
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (error.code !== 'ENOENT') {
|
|
687
|
+
throw new Error('Workspace authorization replay state is missing or invalid; apply a fresh server policy.');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const contentAuthorized = input.policy.evidence.automaticDisclosure?.mode === 'customer_authorized_content';
|
|
691
|
+
const ledgerExists = await pathExists(evidenceUploadLedgerPath());
|
|
692
|
+
if (contentAuthorized && ledgerExists) {
|
|
693
|
+
const ledger = JSON.parse(await readFile(evidenceUploadLedgerPath(), 'utf8'));
|
|
694
|
+
assertEvidenceLedger(ledger, ledger.day);
|
|
695
|
+
}
|
|
696
|
+
if (contentAuthorized && !ledgerExists) {
|
|
697
|
+
const ledger = newEvidenceUploadLedger(new Date().toISOString().slice(0, 10));
|
|
698
|
+
await writeJsonAtomic(evidenceUploadLedgerPath(), ledger);
|
|
699
|
+
}
|
|
700
|
+
await writeJsonAtomic(statePath, {
|
|
701
|
+
issuedAt: input.authorization.issuedAt,
|
|
702
|
+
signature: input.authorization.signature,
|
|
703
|
+
contentLedgerInitialized: previous?.contentLedgerInitialized === true || contentAuthorized,
|
|
704
|
+
});
|
|
705
|
+
await writeJsonAtomic(input.policyPath, input.policy);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
export function applyServerEvidencePolicy(base, value, serverPublicKeyEd25519, organizationId, workspaceId, now = new Date()) {
|
|
709
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
710
|
+
throw new Error('Server workspace policy authorization must be an object.');
|
|
711
|
+
}
|
|
712
|
+
const envelope = value;
|
|
713
|
+
const { signature, ...unsigned } = envelope;
|
|
714
|
+
if (envelope.schema !== 'dharma.workspace-policy-authorization/v1'
|
|
715
|
+
|| envelope.organizationId !== organizationId
|
|
716
|
+
|| envelope.workspaceId !== workspaceId
|
|
717
|
+
|| typeof signature !== 'string'
|
|
718
|
+
|| !Number.isFinite(Date.parse(String(envelope.issuedAt || '')))
|
|
719
|
+
|| Date.parse(String(envelope.expiresAt || '')) <= now.getTime()) {
|
|
720
|
+
throw new Error('Server workspace policy authorization is invalid or expired.');
|
|
721
|
+
}
|
|
722
|
+
const serverPublicKey = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: serverPublicKeyEd25519 }, format: 'jwk' });
|
|
723
|
+
if (!verifyCanonicalObject(unsigned, signature, serverPublicKey)) {
|
|
724
|
+
throw new Error('Server workspace policy authorization signature is invalid.');
|
|
725
|
+
}
|
|
726
|
+
const fragment = envelope.policy;
|
|
727
|
+
if (!fragment || typeof fragment !== 'object' || Array.isArray(fragment)) {
|
|
728
|
+
throw new Error('Server workspace policy authorization has no policy.');
|
|
729
|
+
}
|
|
730
|
+
const policyFragment = fragment;
|
|
731
|
+
const evidence = policyFragment.evidence;
|
|
732
|
+
if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
|
|
733
|
+
throw new Error('Server workspace policy evidence must be an object.');
|
|
734
|
+
}
|
|
735
|
+
const grant = evidence;
|
|
736
|
+
const disclosure = grant.automaticDisclosure;
|
|
737
|
+
if (!disclosure || typeof disclosure !== 'object' || Array.isArray(disclosure)) {
|
|
738
|
+
throw new Error('Server workspace policy disclosure grant is required.');
|
|
739
|
+
}
|
|
740
|
+
const automaticDisclosure = disclosure;
|
|
741
|
+
const noContent = ['metadata_only', 'local_analysis'].includes(String(automaticDisclosure.mode))
|
|
742
|
+
&& automaticDisclosure.consentReceiptId === undefined
|
|
743
|
+
&& automaticDisclosure.allowedContentClasses === undefined;
|
|
744
|
+
const authorizedContent = automaticDisclosure.mode === 'customer_authorized_content'
|
|
745
|
+
&& typeof automaticDisclosure.consentReceiptId === 'string'
|
|
746
|
+
&& Array.isArray(automaticDisclosure.allowedContentClasses)
|
|
747
|
+
&& automaticDisclosure.allowedContentClasses.length === 1
|
|
748
|
+
&& automaticDisclosure.allowedContentClasses[0] === 'native_provider_payload';
|
|
749
|
+
if (!noContent && !authorizedContent) {
|
|
750
|
+
throw new Error('Server workspace policy contains an invalid content disclosure grant.');
|
|
751
|
+
}
|
|
752
|
+
const revision = policyFragment.revision;
|
|
753
|
+
const maximumCapsuleBytes = Number(grant.maximumCapsuleBytes);
|
|
754
|
+
const maximumDailyUploadBytes = Number(grant.maximumDailyUploadBytes);
|
|
755
|
+
const maximumExpansionBytes = Number(grant.maximumExpansionBytes);
|
|
756
|
+
const excludePaths = Array.isArray(grant.excludePaths) ? grant.excludePaths.map(String) : [];
|
|
757
|
+
if (typeof revision !== 'string' || !revision.trim()
|
|
758
|
+
|| !Number.isSafeInteger(maximumCapsuleBytes)
|
|
759
|
+
|| !Number.isSafeInteger(maximumDailyUploadBytes)
|
|
760
|
+
|| !Number.isSafeInteger(maximumExpansionBytes)
|
|
761
|
+
|| maximumExpansionBytes < 1 || maximumExpansionBytes > 262_144
|
|
762
|
+
|| grant.pseudonymizeIdentity !== true
|
|
763
|
+
|| excludePaths.length < 1 || excludePaths.some((item) => !item || item.length > 200)) {
|
|
764
|
+
throw new Error('Server workspace policy limits or revision are invalid.');
|
|
765
|
+
}
|
|
766
|
+
const policy = {
|
|
767
|
+
...structuredClone(base),
|
|
768
|
+
schema: 'dharma.organization-policy/v2',
|
|
769
|
+
revision,
|
|
770
|
+
evidence: {
|
|
771
|
+
...structuredClone(base.evidence),
|
|
772
|
+
automaticDisclosure: authorizedContent ? {
|
|
773
|
+
mode: 'customer_authorized_content',
|
|
774
|
+
consentReceiptId: String(automaticDisclosure.consentReceiptId),
|
|
775
|
+
allowedContentClasses: ['native_provider_payload'],
|
|
776
|
+
} : { mode: automaticDisclosure.mode },
|
|
777
|
+
maximumCapsuleBytes,
|
|
778
|
+
maximumDailyUploadBytes,
|
|
779
|
+
maximumExpansionBytes,
|
|
780
|
+
excludePaths,
|
|
781
|
+
pseudonymizeIdentity: true,
|
|
782
|
+
},
|
|
783
|
+
serverAuthorization: envelope,
|
|
784
|
+
};
|
|
785
|
+
verifyServerAuthorizedPolicy({
|
|
786
|
+
policy,
|
|
787
|
+
publicKeyEd25519: serverPublicKeyEd25519,
|
|
788
|
+
organizationId,
|
|
789
|
+
workspaceId,
|
|
790
|
+
now,
|
|
791
|
+
});
|
|
792
|
+
assertPolicy(policy);
|
|
793
|
+
return policy;
|
|
202
794
|
}
|
|
203
795
|
function providerAdapter(provider) {
|
|
204
796
|
if (provider === 'codex')
|
|
@@ -401,7 +993,7 @@ async function capture(flags, batch = false) {
|
|
|
401
993
|
}
|
|
402
994
|
const workspace = await realpath(required(flags, 'workspace'));
|
|
403
995
|
const provider = required(flags, 'provider');
|
|
404
|
-
const
|
|
996
|
+
const policyPath = required(flags, 'policy');
|
|
405
997
|
const adapter = providerAdapter(provider);
|
|
406
998
|
if (!adapter)
|
|
407
999
|
throw new Error(`Unsupported capture provider: ${provider}`);
|
|
@@ -426,8 +1018,11 @@ async function capture(flags, batch = false) {
|
|
|
426
1018
|
const registered = (await registry()).find((item) => item.path === workspace);
|
|
427
1019
|
if (!registered)
|
|
428
1020
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
1021
|
+
let policy = await loadVerifiedWorkspacePolicy(policyPath, registered.workspaceId);
|
|
429
1022
|
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
430
1023
|
const fabric = flags.has('sync') ? await client() : null;
|
|
1024
|
+
if (fabric)
|
|
1025
|
+
policy = await refreshVerifiedWorkspacePolicyForTransmission(policyPath, registered.workspaceId, fabric);
|
|
431
1026
|
const vault = await LocalVault.open({
|
|
432
1027
|
root: resolve(dharmaHome(), 'vault'),
|
|
433
1028
|
masterKey: await loadOrCreateVaultMasterKey(),
|
|
@@ -437,7 +1032,7 @@ async function capture(flags, batch = false) {
|
|
|
437
1032
|
const capsules = [];
|
|
438
1033
|
const syncResults = [];
|
|
439
1034
|
if (fabric)
|
|
440
|
-
await syncPendingRetentionCapsules(vault, fabric);
|
|
1035
|
+
await syncPendingRetentionCapsules(vault, fabric, policy, registered.workspaceId);
|
|
441
1036
|
for (const selected of batch ? sessions : [session]) {
|
|
442
1037
|
const rawTurn = Buffer.from(`${selected.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
443
1038
|
const rawContentId = sha256(rawTurn);
|
|
@@ -479,8 +1074,12 @@ async function capture(flags, batch = false) {
|
|
|
479
1074
|
});
|
|
480
1075
|
}
|
|
481
1076
|
capsules.push(capsule);
|
|
482
|
-
if (fabric)
|
|
1077
|
+
if (fabric) {
|
|
1078
|
+
await reserveDailyContentUpload(capsule, policy);
|
|
1079
|
+
// An unknown delivery outcome retains the local advisory reservation.
|
|
1080
|
+
// HQ performs the authoritative, idempotent organization/day charge.
|
|
483
1081
|
syncResults.push(await fabric.syncTrajectory(capsule));
|
|
1082
|
+
}
|
|
484
1083
|
}
|
|
485
1084
|
const output = flags.get('output');
|
|
486
1085
|
if (!batch) {
|
|
@@ -545,7 +1144,7 @@ async function evidencePreview(flags) {
|
|
|
545
1144
|
const registered = (await registry()).find((item) => item.path === workspace);
|
|
546
1145
|
if (!registered)
|
|
547
1146
|
throw new Error('Workspace is not registered locally. Run dharma workspace add.');
|
|
548
|
-
const policy = await
|
|
1147
|
+
const policy = await loadVerifiedWorkspacePolicy(policyPath, registered?.workspaceId);
|
|
549
1148
|
const capsules = sessions.map((session) => {
|
|
550
1149
|
const rawTurn = Buffer.from(`${session.records.map((record) => JSON.stringify(record.native)).join('\n')}\n`);
|
|
551
1150
|
return buildTrajectoryCapsule({
|
|
@@ -562,11 +1161,16 @@ async function evidencePreview(flags) {
|
|
|
562
1161
|
automaticDisclosure = {
|
|
563
1162
|
ready: true,
|
|
564
1163
|
disclosureClass: 'automatic_capsule',
|
|
1164
|
+
disclosureMode: capsules[0]?.automaticDisclosureMode ?? policy.evidence.automaticDisclosure?.mode ?? 'local_analysis',
|
|
1165
|
+
consentReceiptId: policy.evidence.automaticDisclosure?.consentReceiptId ?? null,
|
|
565
1166
|
disclosedClasses: [...new Set(capsules.flatMap((capsule) => capsule.redactionReceipt.disclosedClasses))].sort(),
|
|
566
1167
|
excludedClasses: [...new Set(capsules.flatMap((capsule) => capsule.redactionReceipt.excludedClasses))].sort(),
|
|
567
1168
|
capsuleBytes: capsules.reduce((total, capsule) => total + Buffer.byteLength(canonicalize(capsule)), 0),
|
|
568
1169
|
rawProviderBytesLocal: capsules.reduce((total, capsule) => total + (capsule.contentIndex[0]?.bytes || 0), 0),
|
|
569
|
-
rawProviderBytesUploaded:
|
|
1170
|
+
rawProviderBytesUploaded: capsules.reduce((total, capsule) => total + (capsule.automaticDisclosureMode === 'customer_authorized_content'
|
|
1171
|
+
? Buffer.byteLength(canonicalize(capsule.events.map((event) => event.payload.nativeProviderPayload ?? null)))
|
|
1172
|
+
: 0), 0),
|
|
1173
|
+
semanticReviewCandidates: capsules.filter((capsule) => capsule.localAnalysis?.semanticReviewRecommended).length,
|
|
570
1174
|
syncRequiresExplicitFlag: true,
|
|
571
1175
|
};
|
|
572
1176
|
}
|
|
@@ -625,15 +1229,43 @@ async function workspaceAdd(flags, positional) {
|
|
|
625
1229
|
}
|
|
626
1230
|
async function workspaceSync(flags, positional) {
|
|
627
1231
|
const workspaceId = positional[0] || required(flags, 'workspace-id');
|
|
1232
|
+
const policyRevision = required(flags, 'policy-revision');
|
|
628
1233
|
const item = (await registry()).find((candidate) => candidate.workspaceId === workspaceId);
|
|
629
1234
|
if (!item)
|
|
630
1235
|
throw new Error('Workspace is not registered locally.');
|
|
1236
|
+
if (!flags.has('apply')) {
|
|
1237
|
+
return {
|
|
1238
|
+
ok: true, workspaceId, planned: true, serverMutation: false, localMutation: false,
|
|
1239
|
+
next: `dharma workspace sync ${workspaceId} --policy-revision ${policyRevision} --apply`,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
return syncWorkspacePolicy(await client(), item, policyRevision, true);
|
|
1243
|
+
}
|
|
1244
|
+
async function syncWorkspacePolicy(fabric, item, policyRevision, apply = true) {
|
|
631
1245
|
const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
|
|
632
|
-
|
|
1246
|
+
const response = await fabric.registerWorkspace({
|
|
633
1247
|
workspaceId: item.workspaceId, name: item.name, routeHash: item.routeHash,
|
|
634
1248
|
repositoryRemoteHash: item.repositoryRemoteHash, defaultBranch: item.defaultBranch,
|
|
635
|
-
policyRevision
|
|
1249
|
+
policyRevision, providers,
|
|
636
1250
|
});
|
|
1251
|
+
const generated = await materializeWorkspacePolicy({
|
|
1252
|
+
workspace: item.path,
|
|
1253
|
+
organizationId: item.organizationId,
|
|
1254
|
+
revision: String(response.workspace?.policyRevision || policyRevision),
|
|
1255
|
+
serverPolicyAuthorization: response.organizationPolicyAuthorization,
|
|
1256
|
+
serverPublicKeyEd25519: (await readDeviceConfig())?.serverPublicKeyEd25519,
|
|
1257
|
+
workspaceId: item.workspaceId,
|
|
1258
|
+
dryRun: !apply,
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
...response,
|
|
1262
|
+
localPolicy: {
|
|
1263
|
+
path: generated.relativePath,
|
|
1264
|
+
revision: generated.policy.revision,
|
|
1265
|
+
disclosureMode: generated.policy.evidence.automaticDisclosure?.mode || 'local_analysis',
|
|
1266
|
+
applied: generated.applied,
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
637
1269
|
}
|
|
638
1270
|
async function readDeviceConfig() {
|
|
639
1271
|
try {
|
|
@@ -643,6 +1275,22 @@ async function readDeviceConfig() {
|
|
|
643
1275
|
return null;
|
|
644
1276
|
}
|
|
645
1277
|
}
|
|
1278
|
+
async function loadVerifiedWorkspacePolicy(path, workspaceId) {
|
|
1279
|
+
const policy = await loadOrganizationPolicy(path);
|
|
1280
|
+
if (policy.evidence.automaticDisclosure?.mode !== 'customer_authorized_content')
|
|
1281
|
+
return policy;
|
|
1282
|
+
const config = await readDeviceConfig();
|
|
1283
|
+
const authorizedWorkspaceId = workspaceId || policy.serverAuthorization?.workspaceId;
|
|
1284
|
+
const registered = authorizedWorkspaceId
|
|
1285
|
+
? (await registry()).some((item) => item.workspaceId === authorizedWorkspaceId && item.organizationId === policy.organizationId)
|
|
1286
|
+
: false;
|
|
1287
|
+
if (!config || policy.organizationId !== config.organizationId || !authorizedWorkspaceId || !registered) {
|
|
1288
|
+
throw new Error('Content policy does not match an enrolled organization and workspace.');
|
|
1289
|
+
}
|
|
1290
|
+
const verified = applyServerEvidencePolicy({ ...structuredClone(policy), evidence: { ...structuredClone(policy.evidence), automaticDisclosure: { mode: 'local_analysis' } }, serverAuthorization: undefined }, policy.serverAuthorization, config.serverPublicKeyEd25519, config.organizationId, authorizedWorkspaceId);
|
|
1291
|
+
await assertWorkspaceAuthorizationCurrent(authorizedWorkspaceId, verified.serverAuthorization);
|
|
1292
|
+
return verified;
|
|
1293
|
+
}
|
|
646
1294
|
export async function installRepositoryAgentFabricSkill(input) {
|
|
647
1295
|
const skillRoot = resolve(input.workspace, '.agents', 'skills', 'dharma-agent-fabric');
|
|
648
1296
|
const marker = resolve(skillRoot, '.dharma-agent-fabric.json');
|
|
@@ -670,7 +1318,7 @@ description: Connect this repository's coding agents to the organization's Dharm
|
|
|
670
1318
|
|
|
671
1319
|
# Dharma Agent Fabric
|
|
672
1320
|
|
|
673
|
-
Use the installed \`dharma\` CLI for organization-scoped agent work. Never print
|
|
1321
|
+
Use the installed \`dharma\` CLI for organization-scoped agent work. Never print or commit provider credentials, developer tokens, local paths, or raw private trajectories. Transmit content only when the organization policy contains an auditable customer-authorized content grant.
|
|
674
1322
|
|
|
675
1323
|
## Required flow
|
|
676
1324
|
|
|
@@ -681,10 +1329,11 @@ Use the installed \`dharma\` CLI for organization-scoped agent work. Never print
|
|
|
681
1329
|
Do not substitute a placeholder or shell variable. Stop unless the result reports \`ready: true\`. Restart the provider after the first installation so it discovers the native skill.
|
|
682
1330
|
2. Run \`dharma providers list\` to confirm the provider's independently tested evidence, task, continuation, skill, activation, and rollback capabilities.
|
|
683
1331
|
3. Keep \`dharma relay start --policy .dharma/approved-policy.json\` running for signed task, evidence, and skill delivery.
|
|
684
|
-
4. Preview the exact automatic disclosure with \`dharma evidence preview --workspace . --provider codex --policy .dharma/approved-policy.json --maximum-sessions 20\` for Codex, replacing only the literal provider value with \`claude\` or \`agy\` when that is the current agent.
|
|
685
|
-
5.
|
|
686
|
-
6.
|
|
687
|
-
7.
|
|
1332
|
+
4. Preview the exact automatic disclosure with \`dharma evidence preview --workspace . --provider codex --policy .dharma/approved-policy.json --maximum-sessions 20\` for Codex, replacing only the literal provider value with \`claude\` or \`agy\` when that is the current agent. Confirm the policy mode and consent receipt before syncing.
|
|
1333
|
+
5. Run local deterministic self-analysis during capture. Deliver event counts, timing, coverage, failure signals, tool-discipline results, reason codes, and content availability metadata. Do not invent a semantic judgment from metadata. Sessions flagged \`semanticReviewRecommended\` require a policy-authorized evidence request or customer-authorized content mode before server judging.
|
|
1334
|
+
6. Use only signed tasks whose organization, device, workspace, authority, budget, and skill pin pass local validation.
|
|
1335
|
+
7. For cross-agent help, ask the control plane for a structured, task-bound handoff. Do not open arbitrary chat, shell, file, merge, deploy, or secret authority.
|
|
1336
|
+
8. Install only signed skill bundles. Preserve the active bundle receipt and automatic rollback result.
|
|
688
1337
|
|
|
689
1338
|
The organization contract and API origin are recorded in \`.dharma/agent-fabric.json\`. API calls must use the published SDK and a scoped organization token supplied at runtime, never a credential committed to this repository.
|
|
690
1339
|
`;
|
|
@@ -756,19 +1405,19 @@ async function onboard(flags) {
|
|
|
756
1405
|
}
|
|
757
1406
|
if (!registered)
|
|
758
1407
|
throw new Error('Workspace registration failed.');
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1408
|
+
const synced = await workspaceSync(new Map([
|
|
1409
|
+
['policy-revision', policyRevision], ['apply', true],
|
|
1410
|
+
]), [registered.workspaceId]);
|
|
1411
|
+
const localPolicy = synced.localPolicy;
|
|
1412
|
+
const authoritativeRevision = String(localPolicy?.revision || policyRevision);
|
|
1413
|
+
const generatedPolicy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
|
|
764
1414
|
const installed = await installRepositoryAgentFabricSkill({
|
|
765
1415
|
workspace,
|
|
766
1416
|
hqUrl,
|
|
767
1417
|
organizationId,
|
|
768
1418
|
workspaceId: registered.workspaceId,
|
|
769
|
-
policyRevision,
|
|
1419
|
+
policyRevision: authoritativeRevision,
|
|
770
1420
|
});
|
|
771
|
-
const synced = await workspaceSync(new Map([['policy-revision', policyRevision]]), [registered.workspaceId]);
|
|
772
1421
|
const providers = await Promise.all(providerAdapters.map((adapter) => adapter.capability()));
|
|
773
1422
|
const nativeSkills = [];
|
|
774
1423
|
for (const capability of providers) {
|
|
@@ -790,10 +1439,11 @@ async function onboard(flags) {
|
|
|
790
1439
|
deviceId: config.deviceId,
|
|
791
1440
|
providers,
|
|
792
1441
|
organizationPolicy: {
|
|
793
|
-
path:
|
|
794
|
-
revision: generatedPolicy.
|
|
795
|
-
|
|
796
|
-
|
|
1442
|
+
path: '.dharma/approved-policy.json',
|
|
1443
|
+
revision: generatedPolicy.revision,
|
|
1444
|
+
disclosureMode: generatedPolicy.evidence.automaticDisclosure?.mode || 'local_analysis',
|
|
1445
|
+
commandIds: Object.keys(generatedPolicy.tasks.allowedCommands).sort(),
|
|
1446
|
+
writePaths: generatedPolicy.tasks.writePaths,
|
|
797
1447
|
},
|
|
798
1448
|
repositorySkill: installed,
|
|
799
1449
|
nativeSkills,
|
|
@@ -808,9 +1458,35 @@ async function onboard(flags) {
|
|
|
808
1458
|
}
|
|
809
1459
|
async function evidenceSync(flags) {
|
|
810
1460
|
const capsule = JSON.parse(await readFile(resolve(required(flags, 'file')), 'utf8'));
|
|
811
|
-
|
|
1461
|
+
const workspaceId = typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : String(capsule.workspaceId || '');
|
|
1462
|
+
const validation = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/trajectory-capsule/v1', capsule);
|
|
1463
|
+
if (!validation.ok)
|
|
1464
|
+
throw new Error(`Trajectory capsule failed schema validation: ${JSON.stringify(validation.errors)}`);
|
|
1465
|
+
assertCapsuleIntegrity(capsule);
|
|
1466
|
+
const trajectoryId = String(capsule.trajectoryId || '');
|
|
1467
|
+
const revision = Number(capsule.revision);
|
|
1468
|
+
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
1469
|
+
const vault = await LocalVault.open({
|
|
1470
|
+
root: resolve(dharmaHome(), 'vault'),
|
|
1471
|
+
masterKey: await loadOrCreateVaultMasterKey(),
|
|
1472
|
+
});
|
|
1473
|
+
try {
|
|
1474
|
+
const storedCapsule = await vault.getCapsule(trajectoryId, revision);
|
|
1475
|
+
if (canonicalize(storedCapsule) !== canonicalize(capsule)) {
|
|
1476
|
+
throw new Error('Trajectory capsule file does not exactly match the encrypted local-vault revision.');
|
|
1477
|
+
}
|
|
1478
|
+
const fabric = await client();
|
|
1479
|
+
const policy = await refreshVerifiedWorkspacePolicyForTransmission(required(flags, 'policy'), workspaceId, fabric);
|
|
1480
|
+
assertCapsuleAuthorizedByCurrentPolicy(storedCapsule, policy);
|
|
1481
|
+
await reserveDailyContentUpload(storedCapsule, policy);
|
|
1482
|
+
return fabric.syncTrajectory(storedCapsule);
|
|
1483
|
+
}
|
|
1484
|
+
finally {
|
|
1485
|
+
vault.close();
|
|
1486
|
+
}
|
|
812
1487
|
}
|
|
813
1488
|
async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
1489
|
+
assertPolicy(policy);
|
|
814
1490
|
const config = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
815
1491
|
const workspaces = (await registry()).filter((item) => !workspaceId || item.workspaceId === workspaceId);
|
|
816
1492
|
if (workspaces.length === 0)
|
|
@@ -827,7 +1503,10 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
827
1503
|
}
|
|
828
1504
|
if (!request || !workspace)
|
|
829
1505
|
return { ok: true, request: null };
|
|
830
|
-
const
|
|
1506
|
+
const contractId = request.schema === 'dharma.evidence-request/v2'
|
|
1507
|
+
? 'https://schemas.dharma-ai.io/evidence-request/v2'
|
|
1508
|
+
: 'https://schemas.dharma-ai.io/evidence-request/v1';
|
|
1509
|
+
const contract = await validateContract(resolve(import.meta.dirname, 'schemas'), contractId, request);
|
|
831
1510
|
if (!contract.ok)
|
|
832
1511
|
throw new Error(`Evidence request failed schema validation: ${JSON.stringify(contract.errors)}`);
|
|
833
1512
|
const { signature, ...unsignedRequest } = request;
|
|
@@ -840,6 +1519,27 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
840
1519
|
}
|
|
841
1520
|
if (Date.parse(request.expiresAt) <= Date.now())
|
|
842
1521
|
throw new Error('Evidence request has expired.');
|
|
1522
|
+
const requestReceipt = evidenceRequestReceiptPath(request.requestId);
|
|
1523
|
+
await mkdir(dirname(requestReceipt), { recursive: true, mode: 0o700 });
|
|
1524
|
+
let requestHandle;
|
|
1525
|
+
try {
|
|
1526
|
+
requestHandle = await open(requestReceipt, 'wx', 0o600);
|
|
1527
|
+
await requestHandle.writeFile(`${JSON.stringify({
|
|
1528
|
+
schema: 'dharma.evidence-request-receipt/v1',
|
|
1529
|
+
requestId: request.requestId,
|
|
1530
|
+
nonce: request.nonce,
|
|
1531
|
+
state: 'processing',
|
|
1532
|
+
acceptedAt: new Date().toISOString(),
|
|
1533
|
+
})}\n`);
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
if (error.code === 'EEXIST')
|
|
1537
|
+
throw new Error('Evidence request was already accepted locally.');
|
|
1538
|
+
throw error;
|
|
1539
|
+
}
|
|
1540
|
+
finally {
|
|
1541
|
+
await requestHandle?.close().catch(() => undefined);
|
|
1542
|
+
}
|
|
843
1543
|
const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
|
|
844
1544
|
const vault = await LocalVault.open({
|
|
845
1545
|
root: resolve(dharmaHome(), 'vault'),
|
|
@@ -847,8 +1547,18 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
847
1547
|
rawLocalDays: rawLocalRetentionDays(policy),
|
|
848
1548
|
});
|
|
849
1549
|
try {
|
|
850
|
-
await syncPendingRetentionCapsules(vault, fabric);
|
|
1550
|
+
await syncPendingRetentionCapsules(vault, fabric, policy, workspace.workspaceId);
|
|
851
1551
|
const capsule = await vault.getLatestCapsule(request.trajectoryId);
|
|
1552
|
+
assertCapsuleIntegrity(capsule);
|
|
1553
|
+
if (!request.capsuleRevision || !request.capsuleHash) {
|
|
1554
|
+
throw new Error('Legacy evidence request lacks a signed capsule revision and must be reissued.');
|
|
1555
|
+
}
|
|
1556
|
+
if (Number(capsule.revision) !== request.capsuleRevision || capsule.capsuleHash !== request.capsuleHash) {
|
|
1557
|
+
throw new Error('Evidence request is not bound to the current local capsule revision.');
|
|
1558
|
+
}
|
|
1559
|
+
const capsuleReceipt = capsule.redactionReceipt && typeof capsule.redactionReceipt === 'object' && !Array.isArray(capsule.redactionReceipt)
|
|
1560
|
+
? capsule.redactionReceipt : {};
|
|
1561
|
+
const expansionBlockedByExcludedPath = Number(capsuleReceipt.excludedPaths || 0) > 0;
|
|
852
1562
|
const contentIndex = Array.isArray(capsule.contentIndex) ? capsule.contentIndex : [];
|
|
853
1563
|
const available = new Set(contentIndex.flatMap((item) => {
|
|
854
1564
|
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
@@ -863,6 +1573,10 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
863
1573
|
const authorizedBytes = Math.min(request.maximumBytes, policy.evidence.maximumExpansionBytes);
|
|
864
1574
|
let bytesPrepared = 0;
|
|
865
1575
|
for (const selector of request.selectors) {
|
|
1576
|
+
if (expansionBlockedByExcludedPath) {
|
|
1577
|
+
excluded.push({ contentId: selector.contentId, reasonCode: 'configured_excluded_path' });
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
866
1580
|
if (!available.has(selector.contentId)) {
|
|
867
1581
|
excluded.push({ contentId: selector.contentId, reasonCode: 'not_available_in_capsule' });
|
|
868
1582
|
continue;
|
|
@@ -875,7 +1589,31 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
875
1589
|
excluded.push({ contentId: selector.contentId, reasonCode: 'invalid_range' });
|
|
876
1590
|
continue;
|
|
877
1591
|
}
|
|
878
|
-
const
|
|
1592
|
+
const selected = source.subarray(start, end).toString('utf8');
|
|
1593
|
+
const sourceLines = selected.split(/\r?\n/).filter(Boolean);
|
|
1594
|
+
const sourceRecords = [];
|
|
1595
|
+
let structured = sourceLines.length > 0;
|
|
1596
|
+
for (const line of sourceLines) {
|
|
1597
|
+
try {
|
|
1598
|
+
sourceRecords.push(JSON.parse(line));
|
|
1599
|
+
}
|
|
1600
|
+
catch {
|
|
1601
|
+
structured = false;
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (!structured) {
|
|
1606
|
+
excluded.push({ contentId: selector.contentId, reasonCode: 'malformed_structured_content' });
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (referencesExcludedPath(sourceRecords, policy.evidence.excludePaths)) {
|
|
1610
|
+
excluded.push({ contentId: selector.contentId, reasonCode: 'configured_excluded_path' });
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const redactedValue = sourceRecords.map((record) => redactValue(record, stats, '', {
|
|
1614
|
+
pseudonymizeIdentity: policy.evidence.pseudonymizeIdentity,
|
|
1615
|
+
})).map((record) => JSON.stringify(record)).join('\n');
|
|
1616
|
+
const redacted = Buffer.from(redactedValue, 'utf8');
|
|
879
1617
|
if (redacted.byteLength === 0) {
|
|
880
1618
|
excluded.push({ contentId: selector.contentId, reasonCode: 'redacted_empty' });
|
|
881
1619
|
continue;
|
|
@@ -905,11 +1643,22 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
905
1643
|
const responseContract = await validateContract(resolve(import.meta.dirname, 'schemas'), 'https://schemas.dharma-ai.io/evidence-response/v1', response);
|
|
906
1644
|
if (!responseContract.ok)
|
|
907
1645
|
throw new Error(`Evidence response failed schema validation: ${JSON.stringify(responseContract.errors)}`);
|
|
1646
|
+
const expansionReservation = sha256(canonicalize({ requestId: request.requestId, responseHash: response.responseHash }));
|
|
1647
|
+
await reserveDailyEvidenceBytes(expansionReservation, bytesPrepared, policy);
|
|
908
1648
|
const accepted = await fabric.postEvidenceResponse(request.requestId, response);
|
|
909
1649
|
const receipt = accepted.receipt && typeof accepted.receipt === 'object' ? accepted.receipt : {};
|
|
910
1650
|
const receiptHash = typeof receipt.hash === 'string' && /^sha256:[a-f0-9]{64}$/.test(receipt.hash)
|
|
911
1651
|
? receipt.hash : response.responseHash;
|
|
912
1652
|
vault.recordDisclosure(unsignedResponse.responseId, receiptHash, bytesPrepared);
|
|
1653
|
+
await writeJsonAtomic(requestReceipt, {
|
|
1654
|
+
schema: 'dharma.evidence-request-receipt/v1',
|
|
1655
|
+
requestId: request.requestId,
|
|
1656
|
+
nonce: request.nonce,
|
|
1657
|
+
state: 'completed',
|
|
1658
|
+
responseId: unsignedResponse.responseId,
|
|
1659
|
+
responseHash: response.responseHash,
|
|
1660
|
+
completedAt: new Date().toISOString(),
|
|
1661
|
+
});
|
|
913
1662
|
return {
|
|
914
1663
|
ok: true, requestId: request.requestId, responseId: unsignedResponse.responseId,
|
|
915
1664
|
approved: approved.length, excluded: excluded.length, bytesPrepared, receipt,
|
|
@@ -920,8 +1669,10 @@ async function processEvidenceRequest(fabric, policy, workspaceId) {
|
|
|
920
1669
|
}
|
|
921
1670
|
}
|
|
922
1671
|
async function runOneEvidenceRequest(flags) {
|
|
923
|
-
const
|
|
924
|
-
|
|
1672
|
+
const workspaceId = required(flags, 'workspace-id');
|
|
1673
|
+
const fabric = await client();
|
|
1674
|
+
const policy = await refreshVerifiedWorkspacePolicyForTransmission(required(flags, 'policy'), workspaceId, fabric);
|
|
1675
|
+
return processEvidenceRequest(fabric, policy, workspaceId);
|
|
925
1676
|
}
|
|
926
1677
|
async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
927
1678
|
const polled = await fabric.pollTask(leaseSeconds);
|
|
@@ -978,7 +1729,7 @@ async function executeOneTask(fabric, policy, leaseSeconds) {
|
|
|
978
1729
|
return { ok: true, taskId: task.taskId, receipt: summary };
|
|
979
1730
|
}
|
|
980
1731
|
async function runOneTask(flags) {
|
|
981
|
-
const policy = await
|
|
1732
|
+
const policy = await loadVerifiedWorkspacePolicy(required(flags, 'policy'), typeof flags.get('workspace-id') === 'string' ? String(flags.get('workspace-id')) : undefined);
|
|
982
1733
|
return executeOneTask(await client(), policy, Number(flags.get('lease-seconds') || 120));
|
|
983
1734
|
}
|
|
984
1735
|
export function nativeSkillDirectory(provider, env = process.env, home = homedir()) {
|
|
@@ -1006,7 +1757,7 @@ Use this skill only inside a repository containing \`.dharma/agent-fabric.json\`
|
|
|
1006
1757
|
|
|
1007
1758
|
1. Run \`dharma status\` and \`dharma skills verify --provider ${input.provider} --workspace .\` before accepting work.
|
|
1008
1759
|
2. Start \`dharma relay start --policy .dharma/approved-policy.json\` for signed task, evidence, and skill delivery.
|
|
1009
|
-
3. Preview evidence before sync.
|
|
1760
|
+
3. Preview evidence before sync. Run local deterministic self-analysis and disclose only the policy-selected metadata or customer-authorized content classes. Never expose provider credentials, hidden evaluation truth, or unrelated local files.
|
|
1010
1761
|
4. Accept only organization-scoped tasks whose workspace, path, command, network, Git, budget, expiry, replay, and skill-pin checks pass locally.
|
|
1011
1762
|
5. Treat cross-agent requests as structured, task-bound handoffs. Never infer shell, merge, deploy, secret, or unrelated-file authority.
|
|
1012
1763
|
|
|
@@ -1193,7 +1944,11 @@ async function skillSync(flags) {
|
|
|
1193
1944
|
}
|
|
1194
1945
|
}
|
|
1195
1946
|
async function relayStart(flags) {
|
|
1196
|
-
const
|
|
1947
|
+
const policyPath = resolve(required(flags, 'policy'));
|
|
1948
|
+
const canonicalWorkspace = (await registry()).find((item) => resolve(item.path, '.dharma', 'approved-policy.json') === policyPath);
|
|
1949
|
+
if (!canonicalWorkspace)
|
|
1950
|
+
throw new Error('Relay policy must be the canonical policy of one registered workspace.');
|
|
1951
|
+
let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
|
|
1197
1952
|
const fabric = await client();
|
|
1198
1953
|
const leaseSeconds = Number(flags.get('lease-seconds') || 120);
|
|
1199
1954
|
const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
|
|
@@ -1206,17 +1961,34 @@ async function relayStart(flags) {
|
|
|
1206
1961
|
process.once('SIGTERM', stop);
|
|
1207
1962
|
let tasksCompleted = 0;
|
|
1208
1963
|
let evidenceResponsesCompleted = 0;
|
|
1964
|
+
let nextPolicyRefreshAt = 0;
|
|
1965
|
+
let evidencePolicyFresh = false;
|
|
1209
1966
|
try {
|
|
1210
1967
|
do {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1968
|
+
if (Date.now() >= nextPolicyRefreshAt) {
|
|
1969
|
+
try {
|
|
1970
|
+
await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
|
|
1971
|
+
policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
|
|
1972
|
+
evidencePolicyFresh = true;
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
evidencePolicyFresh = false;
|
|
1976
|
+
}
|
|
1977
|
+
nextPolicyRefreshAt = Date.now() + 60_000;
|
|
1978
|
+
}
|
|
1979
|
+
let evidenceRequestId;
|
|
1980
|
+
if (evidencePolicyFresh) {
|
|
1981
|
+
const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
|
|
1982
|
+
evidenceRequestId = typeof evidence.requestId === 'string' ? evidence.requestId : undefined;
|
|
1983
|
+
if (evidenceRequestId)
|
|
1984
|
+
evidenceResponsesCompleted += 1;
|
|
1985
|
+
}
|
|
1214
1986
|
const result = await executeOneTask(fabric, policy, leaseSeconds);
|
|
1215
1987
|
if (result.taskId)
|
|
1216
1988
|
tasksCompleted += 1;
|
|
1217
1989
|
if (flags.has('once'))
|
|
1218
1990
|
break;
|
|
1219
|
-
if (!result.taskId && !
|
|
1991
|
+
if (!result.taskId && !evidenceRequestId)
|
|
1220
1992
|
await new Promise((accept) => setTimeout(accept, pollMs));
|
|
1221
1993
|
} while (!stopping);
|
|
1222
1994
|
}
|