@dharma-ai-labs/agent-fabric 0.2.126 → 0.2.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import { openCodexAppServerTransport } from '@dharma-ai-labs/agent-fabric-provid
61
61
  import { createNamedSessionTrust, isNamedSessionOwnerReceipt, renewNamedSessionLifetime } from './namedSessionTrust.js';
62
62
  import { startNamedCodexThread } from './namedCodexThread.js';
63
63
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
64
- const VERSION = '0.2.126';
64
+ const VERSION = '0.2.127';
65
65
  const USAGE = CLI_USAGE;
66
66
  const execFileAsync = promisify(execFile);
67
67
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -2047,6 +2047,16 @@ async function bootstrap(flags) {
2047
2047
  const resuming = flags.has('resume');
2048
2048
  const bootstrapToken = resuming ? null : required(flags, 'grant');
2049
2049
  const selectedRemote = flags.get('repository-url-base64url');
2050
+ const joinedBindingId = typeof flags.get('join-repository-binding-id') === 'string'
2051
+ ? String(flags.get('join-repository-binding-id')) : null;
2052
+ const joinedFingerprint = typeof flags.get('join-source-fingerprint') === 'string'
2053
+ ? String(flags.get('join-source-fingerprint')) : null;
2054
+ if (joinedBindingId && (!UUID_PATTERN.test(joinedBindingId) || !/^sha256:[a-f0-9]{64}$/.test(joinedFingerprint || '')
2055
+ || selectedRemote || flags.has('repository-key'))) {
2056
+ throw new Error('repository_join_selection_invalid: joining requires an exact existing binding and fingerprint, without a source selection.');
2057
+ }
2058
+ if (!joinedBindingId && joinedFingerprint)
2059
+ throw new Error('repository_join_selection_invalid');
2050
2060
  let repositorySelection;
2051
2061
  try {
2052
2062
  if (flags.has('repository-url-base64url') && typeof selectedRemote !== 'string') {
@@ -2055,16 +2065,18 @@ async function bootstrap(flags) {
2055
2065
  if (selectedRemote && flags.has('repository-key')) {
2056
2066
  throw new Error('repository_selection_invalid_url: selected remote cannot be combined with a repository key.');
2057
2067
  }
2058
- repositorySelection = typeof selectedRemote === 'string'
2059
- ? await resolveBootstrapRepositoryWorkspace({
2060
- workspace: String(flags.get('workspace') || '.'),
2061
- selectedRemoteBase64url: selectedRemote,
2062
- organizationId,
2063
- home: dharmaHome(),
2064
- normalizeRemote: normalizeGitRemoteIdentity,
2065
- withLock: (path) => acquirePidLock(path, 30_000, 'repository_checkout_busy: another setup is selecting this repository; retry after it finishes.'),
2066
- })
2067
- : { workspace: await realpath(String(flags.get('workspace') || '.')), selection: 'provided' };
2068
+ repositorySelection = joinedBindingId
2069
+ ? { workspace: resolve(dharmaHome(), 'joined-repositories', joinedBindingId), selection: 'existing_organization_binding' }
2070
+ : typeof selectedRemote === 'string'
2071
+ ? await resolveBootstrapRepositoryWorkspace({
2072
+ workspace: String(flags.get('workspace') || '.'),
2073
+ selectedRemoteBase64url: selectedRemote,
2074
+ organizationId,
2075
+ home: dharmaHome(),
2076
+ normalizeRemote: normalizeGitRemoteIdentity,
2077
+ withLock: (path) => acquirePidLock(path, 30_000, 'repository_checkout_busy: another setup is selecting this repository; retry after it finishes.'),
2078
+ })
2079
+ : { workspace: await realpath(String(flags.get('workspace') || '.')), selection: 'provided' };
2068
2080
  }
2069
2081
  catch (error) {
2070
2082
  const message = error && typeof error === 'object' && 'message' in error && typeof error.message === 'string'
@@ -2079,10 +2091,12 @@ async function bootstrap(flags) {
2079
2091
  if (selectedRemote)
2080
2092
  process.stderr.write(`Agent Fabric repository checkout: ${workspace}\n`);
2081
2093
  const policyRevision = required(flags, 'policy-revision');
2082
- const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
2094
+ const repositoryIdentity = joinedBindingId
2095
+ ? { fingerprint: joinedFingerprint }
2096
+ : await preflightBootstrapWorkspaceIdentity(workspace, typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null);
2083
2097
  const requestedProvider = String(flags.get('provider') || 'auto').trim().toLowerCase();
2084
2098
  const provider = requestedProvider === 'auto'
2085
- ? await detectBootstrapProvider(workspace)
2099
+ ? await detectBootstrapProvider(joinedBindingId ? String(flags.get('workspace') || '.') : workspace)
2086
2100
  : requestedProvider;
2087
2101
  if (!isLocalProviderId(provider)) {
2088
2102
  throw new Error('Bootstrap provider must be auto, codex, claude, agy, or hermes.');
@@ -2121,7 +2135,7 @@ async function bootstrap(flags) {
2121
2135
  name,
2122
2136
  platform: devicePlatform,
2123
2137
  publicKeyEd25519: identity.publicKeyEd25519,
2124
- repositoryFingerprint: repositoryIdentity.fingerprint,
2138
+ repositoryFingerprint: joinedBindingId ? '' : repositoryIdentity.fingerprint,
2125
2139
  onRecipientApprovalRequired: async (approval) => {
2126
2140
  recipientApproval.required = true;
2127
2141
  recipientApproval.browserOpened = flags.has('no-browser')
@@ -2204,7 +2218,9 @@ async function bootstrap(flags) {
2204
2218
  onboardFlags.set('workspace', workspace);
2205
2219
  onboardFlags.set('policy-revision', policyRevision);
2206
2220
  onboardFlags.set('provider', provider);
2207
- const onboarded = await retryBootstrapOnboarding(async () => await onboard(onboardFlags));
2221
+ const onboarded = await retryBootstrapOnboarding(async () => joinedBindingId
2222
+ ? await joinExistingRepository(onboardFlags, joinedBindingId, joinedFingerprint)
2223
+ : await onboard(onboardFlags));
2208
2224
  if (onboarded.ok !== true || onboarded.stage === 'approve_device') {
2209
2225
  return {
2210
2226
  ok: onboarded.ok === true,
@@ -2227,7 +2243,8 @@ async function bootstrap(flags) {
2227
2243
  const launcher = await installStableRepositoryLauncher(workspace);
2228
2244
  const autostart = flags.has('no-relay-daemon')
2229
2245
  ? { state: 'disabled', backend: null }
2230
- : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(async () => {
2246
+ : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`
2247
+ + (joinedBindingId ? ` --join-repository-binding-id ${joinedBindingId} --join-source-fingerprint ${joinedFingerprint}` : ''), () => withRelayStartupMutation(async () => {
2231
2248
  const home = dharmaHome();
2232
2249
  const existing = await relayAutostartStatus({ home });
2233
2250
  if (existing.backend !== null || existing.state !== 'disabled') {
@@ -2285,7 +2302,8 @@ async function bootstrap(flags) {
2285
2302
  throw new Error(`Provider skill did not become ready: ${JSON.stringify(skill)}`);
2286
2303
  const policyPath = resolve(workspace, '.dharma', 'approved-policy.json');
2287
2304
  const firstLearning = onboarded.firstLearningEvidence;
2288
- const firstLearningReady = firstLearning?.state === 'synchronized' || firstLearning?.state === 'denied_disclosure';
2305
+ const firstLearningReady = firstLearning?.state === 'synchronized' || firstLearning?.state === 'denied_disclosure'
2306
+ || joinedBindingId !== null && firstLearning?.state === 'shared_package_inherited';
2289
2307
  const relay = flags.has('no-relay-daemon')
2290
2308
  ? { started: false, ...(await waitForRelayReadiness()) }
2291
2309
  : await startRelayDaemon(policyPath);
@@ -2329,7 +2347,7 @@ async function bootstrap(flags) {
2329
2347
  usage,
2330
2348
  });
2331
2349
  const operatingContract = await loadAgentFabricOnboardingContract();
2332
- const namedSession = provider === 'codex' && process.platform === 'linux' && sharedRepositoryReady
2350
+ const namedSession = !joinedBindingId && provider === 'codex' && process.platform === 'linux' && sharedRepositoryReady
2333
2351
  ? await withOnboardingStage('named_session', String(onboarded.workspaceId), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => namedSessionCommand('start', new Map([
2334
2352
  ['name', String(flags.get('session-name') || `codex-${String(onboarded.workspaceId).slice(0, 8)}`)],
2335
2353
  ['workspace-id', String(onboarded.workspaceId)], ['apply', true],
@@ -2338,13 +2356,23 @@ async function bootstrap(flags) {
2338
2356
  ])))
2339
2357
  : null;
2340
2358
  const repositoryReceipt = onboarded;
2341
- const namedSessionReady = provider !== 'codex' || process.platform !== 'linux'
2359
+ const namedSessionReady = Boolean(joinedBindingId) || provider !== 'codex' || process.platform !== 'linux'
2342
2360
  || Boolean(namedSession?.ok === true && ['running', 'executing'].includes(String(namedSession.state)));
2343
2361
  const role = repositoryReceipt.repositoryRole;
2362
+ const roleReady = Boolean(role);
2363
+ const relayReady = relay.state === 'running';
2364
+ const startupReady = !['win32', 'linux'].includes(process.platform) || autostart.state === 'enabled';
2365
+ const complete = sharedRepositoryReady && namedSessionReady && firstLearningReady
2366
+ && roleReady && relayReady && startupReady;
2344
2367
  return {
2345
- ok: sharedRepositoryReady && namedSessionReady && firstLearningReady,
2346
- stage: sharedRepositoryReady ? namedSessionReady ? firstLearningReady ? 'complete' : 'first_learning_pending' : 'named_session_pending'
2347
- : repositoryReadiness?.outcome === 'blocked' ? 'shared_repository_blocked' : 'shared_repository_pending',
2368
+ ok: complete,
2369
+ stage: !sharedRepositoryReady
2370
+ ? repositoryReadiness?.outcome === 'blocked' ? 'shared_repository_blocked' : 'shared_repository_pending'
2371
+ : !namedSessionReady ? 'named_session_pending'
2372
+ : !firstLearningReady ? 'first_learning_pending'
2373
+ : !roleReady ? 'role_registration_pending'
2374
+ : !relayReady ? 'synchronization_pending'
2375
+ : !startupReady ? 'autostart_pending' : 'complete',
2348
2376
  repositorySelection,
2349
2377
  localStage: 'complete',
2350
2378
  sharedRepositoryReady,
@@ -2379,13 +2407,13 @@ async function bootstrap(flags) {
2379
2407
  },
2380
2408
  workflowReadiness: {
2381
2409
  sharedPackage: sharedRepositoryReady ? 'ready' : 'pending',
2382
- roleRegistration: role ? 'ready' : 'pending',
2410
+ roleRegistration: roleReady ? 'ready' : 'pending',
2383
2411
  firstLearning: firstLearning?.state === 'synchronized'
2384
2412
  ? Number(firstLearning.discovered || 0) > 0 ? 'synchronized' : 'no_eligible_history'
2385
2413
  : String(firstLearning?.state || 'pending'),
2386
2414
  synchronization: relay.state === 'running' ? 'running' : 'pending',
2387
2415
  namedSession: namedSession && namedSessionReady ? 'ready'
2388
- : provider === 'codex' && process.platform === 'linux' ? 'pending' : 'not_supported',
2416
+ : joinedBindingId ? 'not_requested' : provider === 'codex' && process.platform === 'linux' ? 'pending' : 'not_supported',
2389
2417
  },
2390
2418
  };
2391
2419
  }
@@ -3106,6 +3134,9 @@ async function evidencePreview(flags) {
3106
3134
  }
3107
3135
  async function workspaceAdd(flags, positional) {
3108
3136
  const path = await realpath(positional[0] || required(flags, 'path'));
3137
+ if ((await registry()).some(record => record.path === path && record.accessMode === 'knowledge_only')) {
3138
+ throw new Error('repository_join_source_operation_denied');
3139
+ }
3109
3140
  const device = JSON.parse(await readFile(configPath(), 'utf8'));
3110
3141
  const organizationId = String(flags.get('organization-id') || device.organizationId);
3111
3142
  if (organizationId !== device.organizationId)
@@ -3477,7 +3508,7 @@ export function canonicalEndpointRole(value) {
3477
3508
  }
3478
3509
  return { revision: Number(row.revision), profileHash: row.profileHash };
3479
3510
  }
3480
- async function bindRepositoryAgent(fabric, item) {
3511
+ async function bindRepositoryAgent(fabric, item, joinExistingBindingId) {
3481
3512
  const remote = await gitValue(item.path, ['config', '--get', 'remote.origin.url']);
3482
3513
  const currentIdentity = item.repositoryIdentityVersion === 'normalized-v1'
3483
3514
  ? item.repositoryRemoteHash
@@ -3491,6 +3522,7 @@ async function bindRepositoryAgent(fabric, item) {
3491
3522
  defaultSourceRef: item.defaultBranch,
3492
3523
  workspaceId: item.workspaceId,
3493
3524
  legacySourceFingerprint: item.repositoryIdentityVersion === 'normalized-v1' ? null : item.repositoryRemoteHash,
3525
+ ...(joinExistingBindingId ? { joinExistingBindingId } : {}),
3494
3526
  });
3495
3527
  const repositoryAgent = response.repositoryAgent && typeof response.repositoryAgent === 'object'
3496
3528
  ? response.repositoryAgent
@@ -3532,6 +3564,9 @@ async function bindRepositoryAgent(fabric, item) {
3532
3564
  || !/^agents\/[a-z0-9][a-z0-9._-]*-[a-f0-9]{8}$/.test(updated.controlBranch || '')) {
3533
3565
  throw new Error('Dharma HQ returned an invalid repository-agent binding.');
3534
3566
  }
3567
+ if (joinExistingBindingId && updated.repositoryBindingId !== joinExistingBindingId) {
3568
+ throw new Error('repository_join_binding_mismatch');
3569
+ }
3535
3570
  await saveWorkspaceRecord(updated);
3536
3571
  return updated;
3537
3572
  }
@@ -3863,6 +3898,102 @@ async function registerWorkspaceBeforeRepositoryBind(fabric, item, policyRevisio
3863
3898
  const registered = await bindRepositoryAgent(fabric, canonical);
3864
3899
  return { registered, synchronized };
3865
3900
  }
3901
+ async function joinExistingRepository(flags, bindingId, sourceFingerprint) {
3902
+ const config = await readDeviceConfig();
3903
+ if (!config || config.organizationId !== required(flags, 'organization-id')) {
3904
+ throw new Error('repository_join_enrollment_required');
3905
+ }
3906
+ const workspace = resolve(dharmaHome(), 'joined-repositories', bindingId);
3907
+ await mkdir(workspace, { recursive: true, mode: 0o700 });
3908
+ if (await realpath(workspace) !== workspace)
3909
+ throw new Error('repository_join_workspace_symlink_denied');
3910
+ const markerPath = resolve(workspace, '.dharma', 'join-identity.json');
3911
+ const expectedMarker = { organizationId: config.organizationId, bindingId, sourceFingerprint };
3912
+ const existingMarker = await readFile(markerPath, 'utf8').then(value => JSON.parse(value))
3913
+ .catch(error => { if (error?.code === 'ENOENT')
3914
+ return null; throw error; });
3915
+ if (existingMarker && canonicalize(existingMarker) !== canonicalize(expectedMarker)) {
3916
+ throw new Error('repository_join_workspace_identity_conflict');
3917
+ }
3918
+ if (!existingMarker) {
3919
+ const entries = await readdir(workspace);
3920
+ if (entries.length)
3921
+ throw new Error('repository_join_workspace_not_empty');
3922
+ await mkdir(resolve(workspace, '.dharma'), { recursive: true, mode: 0o700 });
3923
+ await writeJsonAtomic(markerPath, expectedMarker);
3924
+ }
3925
+ if (!await gitValue(workspace, ['rev-parse', '--verify', 'HEAD'])) {
3926
+ const template = resolve(workspace, '.dharma', 'empty-git-template');
3927
+ await mkdir(template, { recursive: true, mode: 0o700 });
3928
+ await execFileAsync('git', ['-c', `init.templateDir=${template}`, 'init', '--initial-branch=main', workspace]);
3929
+ await execFileAsync('git', ['-C', workspace, '-c', 'user.name=Dharma Agent Fabric',
3930
+ '-c', 'user.email=agent-fabric@localhost', '-c', 'commit.gpgsign=false', '-c', `core.hooksPath=${template}`,
3931
+ 'commit', '--allow-empty', '-m', 'Initialize isolated knowledge workspace']);
3932
+ }
3933
+ const workspaceId = workspaceIdForDevice({ organizationId: config.organizationId,
3934
+ deviceId: config.deviceId, path: workspace });
3935
+ let item = selectDeviceWorkspace(await registry(), { organizationId: config.organizationId,
3936
+ deviceId: config.deviceId, path: workspace, repositoryRemoteHash: sourceFingerprint });
3937
+ if (item && item.accessMode !== 'knowledge_only')
3938
+ throw new Error('repository_join_workspace_mode_conflict');
3939
+ if (!item) {
3940
+ item = {
3941
+ workspaceId, organizationId: config.organizationId, name: `joined-${bindingId.slice(0, 8)}`,
3942
+ path: workspace, routeHash: `sha256:${createHash('sha256').update(workspace).digest('hex')}`,
3943
+ repositoryRemoteHash: sourceFingerprint, repositoryIdentityVersion: 'normalized-v1',
3944
+ repositoryAgentId: null, repositoryBindingId: null, repositoryAgentKey: null,
3945
+ controlBranch: null, defaultBranch: null, status: 'active', accessMode: 'knowledge_only',
3946
+ };
3947
+ await saveWorkspaceRecord(item);
3948
+ }
3949
+ const provider = String(flags.get('provider') || 'codex');
3950
+ if (!isLocalProviderId(provider))
3951
+ throw new Error('repository_join_provider_invalid');
3952
+ const fabric = await client();
3953
+ const synchronized = await syncWorkspacePolicy(fabric, item, required(flags, 'policy-revision'), true, [provider]);
3954
+ item = reconcileWorkspaceRegistration(await registry(), item, synchronized);
3955
+ item = await bindRepositoryAgent(fabric, item, bindingId);
3956
+ if (item.repositoryPackage?.state !== 'published' || !item.repositoryAgentId || !item.endpointId) {
3957
+ return { ok: false, stage: 'shared_repository_pending', code: 'repository_join_package_unavailable',
3958
+ workspaceId: item.workspaceId, sharedRepositoryReady: false };
3959
+ }
3960
+ const roleInput = {
3961
+ roleName: String(flags.get('role-name') || 'Knowledge collaborator'),
3962
+ questionCategories: (typeof flags.get('question-categories') === 'string'
3963
+ ? String(flags.get('question-categories')).split(',').map(value => value.trim()).filter(Boolean)
3964
+ : ['repository-knowledge', 'lexicon', 'skills']).sort(),
3965
+ description: String(flags.get('role-description') || 'Uses the signed repository package and answers bounded knowledge questions.'),
3966
+ };
3967
+ const profileHash = sha256(canonicalize(roleInput));
3968
+ const role = item.repositoryRole?.profileHash === profileHash
3969
+ ? { stage: 'repository_role_observed', reused: true }
3970
+ : await registerRepositoryRoleMetadata(fabric, repositoryRoleScope(item), {
3971
+ expectedRevision: item.repositoryRole?.revision || 0, ...roleInput,
3972
+ });
3973
+ if (item.repositoryRole?.profileHash !== profileHash) {
3974
+ const revision = Number(role.role?.revision);
3975
+ if (!Number.isSafeInteger(revision) || revision < 1)
3976
+ throw new Error('repository_join_role_registration_failed');
3977
+ item = { ...item, repositoryRole: { revision, profileHash } };
3978
+ await saveWorkspaceRecord(item);
3979
+ }
3980
+ const providers = await receiptAwareProviderCapabilities(await Promise.all(selectedProviderAdapters([provider]).map(adapter => adapter.capability())));
3981
+ const nativeSkills = await installAvailableNativeAgentFabricBootstraps({ providers,
3982
+ workspace, workspaceId: item.workspaceId, organizationId: config.organizationId, hqUrl: config.hqUrl });
3983
+ const relay = flags.has('no-relay-daemon') ? { started: false, state: 'not_started' }
3984
+ : await startRelayDaemon(resolve(workspace, '.dharma', 'approved-policy.json'));
3985
+ const sharedRepositoryReady = await repositorySharedReady(item);
3986
+ return { ok: true, stage: sharedRepositoryReady ? 'ready' : 'shared_repository_pending',
3987
+ localStage: 'ready', sharedRepositoryReady, organizationId: config.organizationId,
3988
+ workspaceId: item.workspaceId, deviceId: config.deviceId, accessMode: 'knowledge_only',
3989
+ repositoryAgent: { id: item.repositoryAgentId, key: item.repositoryAgentKey,
3990
+ bindingId: item.repositoryBindingId, controlBranch: item.controlBranch },
3991
+ repositoryPackage: item.repositoryPackage, repositoryRole: role, providers,
3992
+ firstLearningEvidence: { state: 'shared_package_inherited', discovered: 0,
3993
+ disclosureReady: false, note: 'No local source or trajectory history was read.' },
3994
+ nativeSkills: nativeSkills.installed, nativeSkillFailures: nativeSkills.failures,
3995
+ workspaceSync: synchronized, relay };
3996
+ }
3866
3997
  async function readDeviceConfig() {
3867
3998
  try {
3868
3999
  return JSON.parse(await readFile(configPath(), 'utf8'));
@@ -4119,6 +4250,9 @@ async function repositorySnapshotCommand(flags, outputs) {
4119
4250
  }
4120
4251
  const registeredWorkspace = (await registry()).find(record => record.path === resolve(workspace)
4121
4252
  && record.organizationId === flags.get('organization-id') && record.workspaceId === flags.get('workspace-id'));
4253
+ if (registeredWorkspace?.accessMode === 'knowledge_only') {
4254
+ throw new Error('repository_join_source_operation_denied');
4255
+ }
4122
4256
  const sourceAuthorization = registeredWorkspace?.repositoryAgentId && registeredWorkspace.repositoryBindingId
4123
4257
  ? await fetchRepositorySourceAuthorization(await client(), {
4124
4258
  organizationId: registeredWorkspace.organizationId,
@@ -4175,7 +4309,6 @@ async function onboard(flags) {
4175
4309
  ? [String(flags.get('providers'))]
4176
4310
  : typeof flags.get('provider') === 'string' ? [String(flags.get('provider'))] : []);
4177
4311
  const repositoryKey = typeof flags.get('repository-key') === 'string' ? String(flags.get('repository-key')) : null;
4178
- const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, repositoryKey);
4179
4312
  const registryState = await inspectRegistryRecoveryFile(workspaceRegistryPath());
4180
4313
  if (registryState.kind === 'corrupt_zero') {
4181
4314
  const anchor = await inspectOwnedRelayAutostart({ home: dharmaHome() });
@@ -4185,6 +4318,10 @@ async function onboard(flags) {
4185
4318
  ['workspace', anchor.workspace], ['apply', true],
4186
4319
  ]));
4187
4320
  }
4321
+ if ((await registry()).some(record => record.path === workspace && record.accessMode === 'knowledge_only')) {
4322
+ throw new Error('repository_join_source_operation_denied');
4323
+ }
4324
+ const repositoryIdentity = await preflightBootstrapWorkspaceIdentity(workspace, repositoryKey);
4188
4325
  let registered = selectDeviceWorkspace(await registry(), {
4189
4326
  organizationId,
4190
4327
  deviceId: config.deviceId,
@@ -4959,6 +5096,16 @@ async function executeOneTask(fabric, leaseSeconds, exactStudyTask) {
4959
5096
  const workspace = (await registry()).find((item) => item.workspaceId === task.workspaceId);
4960
5097
  if (!workspace)
4961
5098
  throw new Error('Task workspace is not registered on this device.');
5099
+ if (workspace.accessMode === 'knowledge_only' && !(task.taskType === 'external_request'
5100
+ && task.authority.readPaths.length === 1 && task.authority.readPaths[0] === '.'
5101
+ && task.authority.writePaths.length === 0 && task.authority.commands.length === 0
5102
+ && task.authority.network === 'deny' && task.acceptance.commands.length === 0
5103
+ && task.acceptance.requiredArtifacts.length === 0)) {
5104
+ await fabric.postTaskEvent(task.taskId, 'failed', {
5105
+ phase: 'preflight', code: 'knowledge_only_task_denied',
5106
+ });
5107
+ return { ok: false, taskId: task.taskId, code: 'knowledge_only_task_denied' };
5108
+ }
4962
5109
  const taskPolicy = await refreshVerifiedWorkspacePolicyForTransmission(resolve(workspace.path, '.dharma', 'approved-policy.json'), workspace.workspaceId, fabric);
4963
5110
  assertTaskWorkspacePolicy({ task, workspace, policy: taskPolicy });
4964
5111
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -6294,7 +6441,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6294
6441
  || (canonicalWorkspace.repositoryPackage?.state === 'published'
6295
6442
  && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6296
6443
  ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6297
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6444
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && canonicalWorkspace.repositoryPackage
6445
+ && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6298
6446
  try {
6299
6447
  const recovered = await recoverPublishedLocalSourceBaseline({
6300
6448
  workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
@@ -6509,7 +6657,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6509
6657
  }
6510
6658
  nextPolicyRefreshAt = Date.now() + 60_000;
6511
6659
  }
6512
- if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6660
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && !sourceScanFlight
6661
+ && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6513
6662
  try {
6514
6663
  const current = await pollRepositoryCandidate({ transport: fabric,
6515
6664
  outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
@@ -6538,7 +6687,7 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6538
6687
  }
6539
6688
  }
6540
6689
  let evidenceRequestId;
6541
- if (evidencePolicyFresh && !stopping) {
6690
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && evidencePolicyFresh && !stopping) {
6542
6691
  const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6543
6692
  const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6544
6693
  const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
@@ -6610,7 +6759,8 @@ async function relayWorkspaceLoop(flags, signal, serialized) {
6610
6759
  }
6611
6760
  nextSkillActivationAt = performance.now() + 60_000;
6612
6761
  }
6613
- if (!stopping && !sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6762
+ if (canonicalWorkspace.accessMode !== 'knowledge_only' && !stopping && !sourceScanFlight
6763
+ && performance.now() >= nextRepositorySourceScanAt) {
6614
6764
  const sourceWorkspace = canonicalWorkspace;
6615
6765
  const sourceBaselineHash = publishedLocalSnapshotHash;
6616
6766
  sourceScanFlight = (async () => {